From c7994d6a515405941b3f147936c726af0b975a45 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 2 Mar 2026 21:42:00 +0530 Subject: [PATCH 01/23] Added persistent login flow and MFA support to all examples (#147) --- .../action_report/action_report.py | 483 ++++++++++++++-- .../sdk_examples/aging_report/aging_report.py | 477 ++++++++++++++-- .../sdk_examples/audit_alert/list_alerts.py | 493 +++++++++++++++-- .../sdk_examples/audit_alert/view_alert.py | 492 +++++++++++++++-- .../sdk_examples/audit_report/audit_log.py | 494 +++++++++++++++-- .../audit_report/audit_summary.py | 494 +++++++++++++++-- examples/sdk_examples/auth/login.py | 19 +- examples/sdk_examples/auth/logout.py | 493 +++++++++++++++-- examples/sdk_examples/auth/whoami.py | 481 ++++++++++++++-- .../sdk_examples/breachwatch/breach_status.py | 494 +++++++++++++++-- .../breachwatch/breachwatch_report.py | 486 +++++++++++++++-- .../sdk_examples/breachwatch/list_breaches.py | 494 +++++++++++++++-- .../sdk_examples/breachwatch/scan_password.py | 494 +++++++++++++++-- .../sdk_examples/breachwatch/scan_records.py | 494 +++++++++++++++-- .../compliance_record_access_report.py | 489 +++++++++++++++-- .../compliance/compliance_report.py | 494 +++++++++++++++-- .../compliance_shared_folder_report.py | 494 +++++++++++++++-- .../compliance/compliance_summary_report.py | 490 +++++++++++++++-- .../compliance/compliance_team_report.py | 489 +++++++++++++++-- .../enterprise_info/enterprise_info_node.py | 494 +++++++++++++++-- .../enterprise_info/enterprise_info_role.py | 494 +++++++++++++++-- .../enterprise_info/enterprise_info_team.py | 494 +++++++++++++++-- .../enterprise_info/enterprise_info_tree.py | 494 +++++++++++++++-- .../enterprise_info/enterprise_info_user.py | 494 +++++++++++++++-- .../enterprise_node/enterprise_node_view.py | 494 +++++++++++++++-- .../enterprise_role_membership.py | 501 +++++++++++++++-- .../enterprise_role/enterprise_role_view.py | 494 +++++++++++++++-- .../enterprise_team_membership.py | 494 +++++++++++++++-- .../enterprise_team/enterprise_team_view.py | 494 +++++++++++++++-- .../enterprise_user/device_approve.py | 495 +++++++++++++++-- .../enterprise_user/enterprise_user_view.py | 494 +++++++++++++++-- .../enterprise_user/transfer_user.py | 494 +++++++++++++++-- .../ext_shares_report.py | 485 +++++++++++++++-- examples/sdk_examples/folder/add_folder.py | 494 +++++++++++++++-- examples/sdk_examples/folder/delete_folder.py | 494 +++++++++++++++-- examples/sdk_examples/folder/list_folders.py | 495 +++++++++++++++-- .../folder/list_shared_folders.py | 494 +++++++++++++++-- examples/sdk_examples/folder/move_folder.py | 494 +++++++++++++++-- examples/sdk_examples/folder/update_folder.py | 494 +++++++++++++++-- .../import_export/export_vault.py | 480 ++++++++++++++-- .../import_export/vault_summary.py | 481 ++++++++++++++-- .../sdk_examples/miscellaneous/copy_field.py | 481 ++++++++++++++-- .../sdk_examples/miscellaneous/get_totp.py | 481 ++++++++++++++-- .../sdk_examples/miscellaneous/list_teams.py | 481 ++++++++++++++-- .../miscellaneous/password_strength.py | 481 ++++++++++++++-- .../one_time_share/list_shares.py | 481 ++++++++++++++-- .../record_types/list_record_types.py | 481 ++++++++++++++-- examples/sdk_examples/records/add_record.py | 514 +++++++++++++++--- .../sdk_examples/records/delete_attachment.py | 494 +++++++++++++++-- .../sdk_examples/records/delete_record.py | 494 +++++++++++++++-- .../records/download_attachment.py | 494 +++++++++++++++-- examples/sdk_examples/records/file_report.py | 494 +++++++++++++++-- .../sdk_examples/records/find_duplicate.py | 495 +++++++++++++++-- examples/sdk_examples/records/get_record.py | 494 +++++++++++++++-- examples/sdk_examples/records/list_records.py | 60 +- .../sdk_examples/records/record_history.py | 494 +++++++++++++++-- .../sdk_examples/records/search_record.py | 494 +++++++++++++++-- .../sdk_examples/records/update_record.py | 494 +++++++++++++++-- .../sdk_examples/records/upload_attachment.py | 494 +++++++++++++++-- .../secrets_manager/create_app.py | 495 +++++++++++++++-- .../sdk_examples/secrets_manager/get_app.py | 495 +++++++++++++++-- .../sdk_examples/secrets_manager/list_apps.py | 495 +++++++++++++++-- .../security_audit/security_audit_report.py | 492 +++++++++++++++-- .../sdk_examples/share_report/share_report.py | 470 +++++++++++++++- .../shared_records_report.py | 483 ++++++++++++++-- examples/sdk_examples/trash/list_trash.py | 495 +++++++++++++++-- .../sdk_examples/trash/restore_records.py | 495 +++++++++++++++-- .../sdk_examples/trash/view_trash_record.py | 495 +++++++++++++++-- .../sdk_examples/user_report/user_report.py | 493 +++++++++++++++-- 69 files changed, 29721 insertions(+), 3273 deletions(-) diff --git a/examples/sdk_examples/action_report/action_report.py b/examples/sdk_examples/action_report/action_report.py index b52f5ab3..3fcf0f75 100644 --- a/examples/sdk_examples/action_report/action_report.py +++ b/examples/sdk_examples/action_report/action_report.py @@ -2,52 +2,461 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import configuration, endpoint, keeper_auth, login_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import action_report, enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + TARGET_STATUS = 'no-logon' DAYS_SINCE = 30 -def login(): - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - while not login_auth_context.login_step.is_final(): - step = login_auth_context.login_step - if isinstance(step, login_auth.LoginStepDeviceApproval): - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print("Device approval request sent. Approve this device and press Enter.") - input() - elif isinstance(step, login_auth.LoginStepPassword): - step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(step, login_auth.LoginStepTwoFactor): - channel = step.get_channels()[0] - step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(step).__name__}") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + def print_report(entries, target_status, days_since): @@ -114,7 +523,7 @@ def generate_action_report(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - auth = login() + auth, _ = login() if auth: generate_action_report(auth) else: diff --git a/examples/sdk_examples/aging_report/aging_report.py b/examples/sdk_examples/aging_report/aging_report.py index daadad66..c3ad3c67 100644 --- a/examples/sdk_examples/aging_report/aging_report.py +++ b/examples/sdk_examples/aging_report/aging_report.py @@ -5,51 +5,462 @@ import sqlite3 import sys import traceback +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, aging_report from keepersdk.errors import KeeperApiError +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 140 COL_WIDTHS = (30, 30, 25, 10, 45) HEADERS = ['Owner', 'Title', 'Password Changed', 'Shared', 'Record URL'] -def login(): - config = configuration.JsonConfigurationStorage() - server = config.get().last_server or 'keepersecurity.com' - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - auth_context = login_auth.LoginAuth(keeper_endpoint) - auth_context.resume_session = True - - username = config.get().last_login - if not username: - print("Error: No saved login found. Please run with interactive login first.") - return None, None - - auth_context.login(username) - - while not auth_context.login_step.is_final(): - step = auth_context.login_step - if isinstance(step, login_auth.LoginStepDeviceApproval): - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print("Device approval required. Approve and press Enter.") - input() - elif isinstance(step, login_auth.LoginStepPassword): - step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(step, login_auth.LoginStepTwoFactor): - channel = step.get_channels()[0] - code = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - step.send_code(channel.channel_uid, code) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(step).__name__}") - - if isinstance(auth_context.login_step, login_auth.LoginStepConnected): - return auth_context.login_step.take_keeper_auth(), server - return None, None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def format_row(values): return ' '.join( diff --git a/examples/sdk_examples/audit_alert/list_alerts.py b/examples/sdk_examples/audit_alert/list_alerts.py index dd84e1c5..1a27241a 100644 --- a/examples/sdk_examples/audit_alert/list_alerts.py +++ b/examples/sdk_examples/audit_alert/list_alerts.py @@ -1,66 +1,453 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.errors import KeeperApiError +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None - + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_alerts(keeper_auth_context: keeper_auth.KeeperAuth): """ @@ -134,7 +521,7 @@ def main(): Main entry point for the list alerts script. Performs login and lists audit alerts. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_alerts(keeper_auth_context) diff --git a/examples/sdk_examples/audit_alert/view_alert.py b/examples/sdk_examples/audit_alert/view_alert.py index 2a014a0c..3132a90f 100644 --- a/examples/sdk_examples/audit_alert/view_alert.py +++ b/examples/sdk_examples/audit_alert/view_alert.py @@ -1,65 +1,453 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.errors import KeeperApiError +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_alert(keeper_auth_context: keeper_auth.KeeperAuth): @@ -156,7 +544,7 @@ def main(): Main entry point for the view alert script. Performs login and displays alert details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_alert(keeper_auth_context) diff --git a/examples/sdk_examples/audit_report/audit_log.py b/examples/sdk_examples/audit_report/audit_log.py index c179ecf5..453d69c1 100644 --- a/examples/sdk_examples/audit_report/audit_log.py +++ b/examples/sdk_examples/audit_report/audit_log.py @@ -1,66 +1,454 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS -from keepersdk.enterprise import audit_report from keepersdk.errors import KeeperApiError +from keepersdk.enterprise import audit_report + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_audit_log(keeper_auth_context: keeper_auth.KeeperAuth): @@ -147,7 +535,7 @@ def main(): Main entry point for the audit log script. Performs login and displays audit log events. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_audit_log(keeper_auth_context) diff --git a/examples/sdk_examples/audit_report/audit_summary.py b/examples/sdk_examples/audit_report/audit_summary.py index 967093eb..1eecc5fc 100644 --- a/examples/sdk_examples/audit_report/audit_summary.py +++ b/examples/sdk_examples/audit_report/audit_summary.py @@ -1,66 +1,454 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS -from keepersdk.enterprise import audit_report from keepersdk.errors import KeeperApiError +from keepersdk.enterprise import audit_report + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def generate_audit_summary(keeper_auth_context: keeper_auth.KeeperAuth): @@ -143,7 +531,7 @@ def main(): Main entry point for the audit summary script. Performs login and generates audit summary report. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_audit_summary(keeper_auth_context) diff --git a/examples/sdk_examples/auth/login.py b/examples/sdk_examples/auth/login.py index f20e8938..b6e3118d 100644 --- a/examples/sdk_examples/auth/login.py +++ b/examples/sdk_examples/auth/login.py @@ -25,7 +25,14 @@ pyperclip = None logger = utils.get_logger() -logger.setLevel(logging.WARNING) +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): @@ -71,6 +78,11 @@ def __init__(self) -> None: def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: return self._endpoint + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + def run(self) -> Optional[keeper_auth.KeeperAuth]: """ Run the login flow. @@ -139,6 +151,7 @@ def _handle_device_approval( "ask admin to approve this device and then press return/enter to resume" ) input() + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: password = getpass.getpass("Enter password: ") @@ -430,7 +443,7 @@ def login(): """ flow = LoginFlow() keeper_auth_context = flow.run() - if keeper_auth_context: + if keeper_auth_context and not flow.logged_in_with_persistent: enable_persistent_login(keeper_auth_context) keeper_endpoint = flow.endpoint if keeper_auth_context else None return keeper_auth_context, keeper_endpoint @@ -439,7 +452,7 @@ def login(): def display_login_info(keeper_auth_context: keeper_auth.KeeperAuth, keeper_endpoint: endpoint.KeeperEndpoint): """ Display login success information. - + Args: keeper_auth_context: The authenticated Keeper context. keeper_endpoint: The Keeper endpoint with server information. diff --git a/examples/sdk_examples/auth/logout.py b/examples/sdk_examples/auth/logout.py index 109bf84f..c83f8b5a 100644 --- a/examples/sdk_examples/auth/logout.py +++ b/examples/sdk_examples/auth/logout.py @@ -1,64 +1,453 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def perform_logout(keeper_auth_context: keeper_auth.KeeperAuth): @@ -89,7 +478,7 @@ def main(): Main entry point for the logout script. Performs login and then offers logout option. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: perform_logout(keeper_auth_context) diff --git a/examples/sdk_examples/auth/whoami.py b/examples/sdk_examples/auth/whoami.py index 81fe6203..2014fa44 100644 --- a/examples/sdk_examples/auth/whoami.py +++ b/examples/sdk_examples/auth/whoami.py @@ -1,53 +1,452 @@ import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk import utils +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ - Handle the login process including authentication and multi-factor authentication steps. - + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - keeper_endpoint = endpoint.KeeperEndpoint(config) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).name}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth(), keeper_endpoint - - return None, None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_whoami(keeper_auth_context: keeper_auth.KeeperAuth, keeper_endpoint: endpoint.KeeperEndpoint): diff --git a/examples/sdk_examples/breachwatch/breach_status.py b/examples/sdk_examples/breachwatch/breach_status.py index 48cc08c7..535a93d4 100644 --- a/examples/sdk_examples/breachwatch/breach_status.py +++ b/examples/sdk_examples/breachwatch/breach_status.py @@ -1,66 +1,454 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def show_breach_status(keeper_auth_context: keeper_auth.KeeperAuth): @@ -139,7 +527,7 @@ def main(): Main entry point for the breach status script. Performs login and displays BreachWatch status. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: show_breach_status(keeper_auth_context) diff --git a/examples/sdk_examples/breachwatch/breachwatch_report.py b/examples/sdk_examples/breachwatch/breachwatch_report.py index 673438c9..a21a238a 100644 --- a/examples/sdk_examples/breachwatch/breachwatch_report.py +++ b/examples/sdk_examples/breachwatch/breachwatch_report.py @@ -6,64 +6,462 @@ """ import getpass +import json import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, breachwatch_report from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 110 COL_WIDTHS = (38, 22, 14, 10, 10, 10) BANNER_WIDTH = 60 -DEFAULT_SERVER = 'keepersecurity.com' -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input(f'Enter server (default: {DEFAULT_SERVER}): ').strip() or DEFAULT_SERVER - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError( - f"Unsupported login step: {type(login_auth_context.login_step).__name__}" + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration ) - logged_in_with_persistent = False + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def format_row(values, widths=COL_WIDTHS): @@ -156,7 +554,7 @@ def main(): logger.info("=" * BANNER_WIDTH) logger.info("Generates a BreachWatch security audit report for all enterprise users.\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_breachwatch_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/breachwatch/list_breaches.py b/examples/sdk_examples/breachwatch/list_breaches.py index 3fe6a3bc..18b00b4e 100644 --- a/examples/sdk_examples/breachwatch/list_breaches.py +++ b/examples/sdk_examples/breachwatch/list_breaches.py @@ -1,66 +1,454 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_breaches(keeper_auth_context: keeper_auth.KeeperAuth): @@ -137,7 +525,7 @@ def main(): Main entry point for the list breaches script. Performs login and lists breached passwords. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_breaches(keeper_auth_context) diff --git a/examples/sdk_examples/breachwatch/scan_password.py b/examples/sdk_examples/breachwatch/scan_password.py index c10f222b..cbaf7d18 100644 --- a/examples/sdk_examples/breachwatch/scan_password.py +++ b/examples/sdk_examples/breachwatch/scan_password.py @@ -1,66 +1,454 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def scan_passwords(keeper_auth_context: keeper_auth.KeeperAuth): @@ -146,7 +534,7 @@ def main(): Main entry point for the scan password script. Performs login and scans passwords for breaches. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: scan_passwords(keeper_auth_context) diff --git a/examples/sdk_examples/breachwatch/scan_records.py b/examples/sdk_examples/breachwatch/scan_records.py index 7ea0c8e0..811a206e 100644 --- a/examples/sdk_examples/breachwatch/scan_records.py +++ b/examples/sdk_examples/breachwatch/scan_records.py @@ -1,66 +1,454 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def scan_records(keeper_auth_context: keeper_auth.KeeperAuth): @@ -168,7 +556,7 @@ def main(): Main entry point for the scan records script. Performs login and scans records for breached passwords. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: scan_records(keeper_auth_context) diff --git a/examples/sdk_examples/compliance/compliance_record_access_report.py b/examples/sdk_examples/compliance/compliance_record_access_report.py index aa53f123..914cb480 100644 --- a/examples/sdk_examples/compliance/compliance_record_access_report.py +++ b/examples/sdk_examples/compliance/compliance_record_access_report.py @@ -5,66 +5,465 @@ """ import getpass +import json import logging import os import sqlite3 import traceback +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 250 COL_WIDTHS_DEFAULT = (34, 22, 30, 14, 25, 8, 8, 34, 15, 15, 20) COL_WIDTHS_AGING = (34, 22, 30, 14, 25, 8, 8, 34, 15, 15, 20, 12, 12, 12, 12) -def login(): - """Handle login with server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -181,7 +580,7 @@ def main(aging: bool = False): logger.info("(Including aging columns)") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_record_access_report(keeper_auth_context, aging=aging) else: diff --git a/examples/sdk_examples/compliance/compliance_report.py b/examples/sdk_examples/compliance/compliance_report.py index 3b78197b..2ee03178 100644 --- a/examples/sdk_examples/compliance/compliance_report.py +++ b/examples/sdk_examples/compliance/compliance_report.py @@ -3,67 +3,465 @@ Usage: python compliance_report.py """ - -import getpass -import logging import os import sqlite3 import traceback +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 200 COL_WIDTHS = (22, 30, 15, 34, 12, 40, 10, 24) -def login(): - """Handle login with server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -173,7 +571,7 @@ def main(): logger.info("Keeper Compliance Report") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_compliance_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/compliance/compliance_shared_folder_report.py b/examples/sdk_examples/compliance/compliance_shared_folder_report.py index 52b026e7..d22dfae9 100644 --- a/examples/sdk_examples/compliance/compliance_shared_folder_report.py +++ b/examples/sdk_examples/compliance/compliance_shared_folder_report.py @@ -3,67 +3,465 @@ Usage: python compliance_shared_folder_report.py """ - -import getpass -import logging import os import sqlite3 import traceback +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 180 COL_WIDTHS = (22, 12, 12, 22, 50, 34) -def login(): - """Handle login with server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -190,7 +588,7 @@ def main(): logger.info("Keeper Compliance Shared Folder Report") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_shared_folder_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/compliance/compliance_summary_report.py b/examples/sdk_examples/compliance/compliance_summary_report.py index 48669f21..5b1523a5 100644 --- a/examples/sdk_examples/compliance/compliance_summary_report.py +++ b/examples/sdk_examples/compliance/compliance_summary_report.py @@ -9,61 +9,461 @@ import os import sqlite3 import traceback +import json +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 100 COL_WIDTHS = (40, 15, 15, 12, 12) -def login(): - """Handle login with server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -165,7 +565,7 @@ def main(): logger.info("Keeper Compliance Summary Report") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_summary_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/compliance/compliance_team_report.py b/examples/sdk_examples/compliance/compliance_team_report.py index a4472e47..7e9e0faf 100644 --- a/examples/sdk_examples/compliance/compliance_team_report.py +++ b/examples/sdk_examples/compliance/compliance_team_report.py @@ -9,61 +9,460 @@ import os import sqlite3 import traceback +import json +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 140 COL_WIDTHS = (30, 24, 35, 20, 10) -def login(): - """Handle login with server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -161,7 +560,7 @@ def main(): logger.info("Keeper Compliance Team Report") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_team_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_node.py b/examples/sdk_examples/enterprise_info/enterprise_info_node.py index bf0baa87..7290b115 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_node.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_node.py @@ -1,67 +1,455 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_enterprise_nodes(keeper_auth_context: keeper_auth.KeeperAuth): @@ -135,7 +523,7 @@ def main(): Main entry point for the enterprise info node script. Performs login and displays enterprise nodes information. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: display_enterprise_nodes(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_role.py b/examples/sdk_examples/enterprise_info/enterprise_info_role.py index 02adff4b..ba482be3 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_role.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_role.py @@ -1,67 +1,455 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_enterprise_roles(keeper_auth_context: keeper_auth.KeeperAuth): @@ -128,7 +516,7 @@ def main(): Main entry point for the enterprise info role script. Performs login and displays enterprise roles information. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: display_enterprise_roles(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_team.py b/examples/sdk_examples/enterprise_info/enterprise_info_team.py index 3015115f..a7271381 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_team.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_team.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_enterprise_teams(keeper_auth_context: keeper_auth.KeeperAuth): @@ -127,7 +515,7 @@ def main(): Main entry point for the enterprise info team script. Performs login and displays enterprise teams information. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: display_enterprise_teams(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_tree.py b/examples/sdk_examples/enterprise_info/enterprise_info_tree.py index 81d23965..cf846f30 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_tree.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_tree.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_enterprise_tree(keeper_auth_context: keeper_auth.KeeperAuth): @@ -134,7 +522,7 @@ def main(): Main entry point for the enterprise info tree script. Performs login and displays enterprise node tree structure. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: display_enterprise_tree(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_user.py b/examples/sdk_examples/enterprise_info/enterprise_info_user.py index 5224bd59..585fef23 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_user.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_user.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def display_enterprise_users(keeper_auth_context: keeper_auth.KeeperAuth): @@ -126,7 +514,7 @@ def main(): Main entry point for the enterprise info user script. Performs login and displays enterprise users information. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: display_enterprise_users(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_node/enterprise_node_view.py b/examples/sdk_examples/enterprise_node/enterprise_node_view.py index 4d2052be..d79bb08f 100644 --- a/examples/sdk_examples/enterprise_node/enterprise_node_view.py +++ b/examples/sdk_examples/enterprise_node/enterprise_node_view.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_enterprise_nodes(keeper_auth_context: keeper_auth.KeeperAuth): @@ -156,7 +544,7 @@ def main(): Main entry point for the enterprise node view script. Performs login and displays enterprise node details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_enterprise_nodes(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_role/enterprise_role_membership.py b/examples/sdk_examples/enterprise_role/enterprise_role_membership.py index dd48ba92..57252138 100644 --- a/examples/sdk_examples/enterprise_role/enterprise_role_membership.py +++ b/examples/sdk_examples/enterprise_role/enterprise_role_membership.py @@ -1,74 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: """ - Handle the login process including server selection, authentication, - and multi-factor authentication steps. - - Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - try: + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - except KeeperApiError as e: - if 'invalid_credentials' in str(e): - print("\nError: Invalid password. Please check your credentials and try again.") - else: - print(f"\nLogin error: {e}") + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + return None - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_role_membership(keeper_auth_context: keeper_auth.KeeperAuth): @@ -166,7 +547,7 @@ def main(): Main entry point for the enterprise role membership script. Performs login and displays role membership details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_role_membership(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_role/enterprise_role_view.py b/examples/sdk_examples/enterprise_role/enterprise_role_view.py index 11c0a416..59141e4b 100644 --- a/examples/sdk_examples/enterprise_role/enterprise_role_view.py +++ b/examples/sdk_examples/enterprise_role/enterprise_role_view.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_enterprise_roles(keeper_auth_context: keeper_auth.KeeperAuth): @@ -178,7 +566,7 @@ def main(): Main entry point for the enterprise role view script. Performs login and displays enterprise role details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_enterprise_roles(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_team/enterprise_team_membership.py b/examples/sdk_examples/enterprise_team/enterprise_team_membership.py index c3047a03..e7e89233 100644 --- a/examples/sdk_examples/enterprise_team/enterprise_team_membership.py +++ b/examples/sdk_examples/enterprise_team/enterprise_team_membership.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_team_membership(keeper_auth_context: keeper_auth.KeeperAuth): @@ -155,7 +543,7 @@ def main(): Main entry point for the enterprise team membership script. Performs login and displays team membership details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_team_membership(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_team/enterprise_team_view.py b/examples/sdk_examples/enterprise_team/enterprise_team_view.py index 626e99c8..e85d06a3 100644 --- a/examples/sdk_examples/enterprise_team/enterprise_team_view.py +++ b/examples/sdk_examples/enterprise_team/enterprise_team_view.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_enterprise_teams(keeper_auth_context: keeper_auth.KeeperAuth): @@ -153,7 +541,7 @@ def main(): Main entry point for the enterprise team view script. Performs login and displays enterprise team details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_enterprise_teams(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_user/device_approve.py b/examples/sdk_examples/enterprise_user/device_approve.py index 59c6e982..a5055a82 100644 --- a/examples/sdk_examples/enterprise_user/device_approve.py +++ b/examples/sdk_examples/enterprise_user/device_approve.py @@ -1,72 +1,461 @@ import getpass import sqlite3 import time +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.proto import enterprise_pb2, APIRequest_pb2 from keepersdk import utils, crypto + from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.backends import default_backend -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def approve_devices(keeper_auth_context: keeper_auth.KeeperAuth): @@ -238,7 +627,7 @@ def main(): Main entry point for the device approval script. Performs login and manages device approval requests. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: approve_devices(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_user/enterprise_user_view.py b/examples/sdk_examples/enterprise_user/enterprise_user_view.py index 8b88998c..aac07b53 100644 --- a/examples/sdk_examples/enterprise_user/enterprise_user_view.py +++ b/examples/sdk_examples/enterprise_user/enterprise_user_view.py @@ -1,67 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_enterprise_users(keeper_auth_context: keeper_auth.KeeperAuth): @@ -172,7 +560,7 @@ def main(): Main entry point for the enterprise user view script. Performs login and displays enterprise user details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_enterprise_users(keeper_auth_context) diff --git a/examples/sdk_examples/enterprise_user/transfer_user.py b/examples/sdk_examples/enterprise_user/transfer_user.py index 90f659d8..a7592a3b 100644 --- a/examples/sdk_examples/enterprise_user/transfer_user.py +++ b/examples/sdk_examples/enterprise_user/transfer_user.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, account_transfer +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, account_transfer + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def transfer_user_account(keeper_auth_context: keeper_auth.KeeperAuth): @@ -175,7 +563,7 @@ def main(): Main entry point for the user transfer script. Performs login and manages user account transfers. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: transfer_user_account(keeper_auth_context) diff --git a/examples/sdk_examples/external_shares_report/ext_shares_report.py b/examples/sdk_examples/external_shares_report/ext_shares_report.py index 9c2f3c79..995b46a1 100644 --- a/examples/sdk_examples/external_shares_report/ext_shares_report.py +++ b/examples/sdk_examples/external_shares_report/ext_shares_report.py @@ -4,15 +4,43 @@ import logging import os import sqlite3 - -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import json +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, compliance from keepersdk.errors import KeeperApiError from keepersdk.plugins.sox import compliance_storage as cs -logging.basicConfig(level=logging.INFO, format='%(message)s') -logger = logging.getLogger(__name__) +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + TABLE_WIDTH = 120 COL_WIDTHS = (22, 22, 15, 32, 18) @@ -22,51 +50,418 @@ REPORT_TITLE = 'External Shares Report' -def login(): - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - logger.info("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - logger.info(f" {region}: {host}") - server = input(f'Enter server (default: {DEFAULT_SERVER}): ').strip() or DEFAULT_SERVER - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - logger.info("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code( - channel.channel_uid, - getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" ) + self._config.get().last_server = server else: - raise NotImplementedError( - f"Unsupported login step: {type(login_auth_context.login_step).__name__}" + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration ) - logged_in_with_persistent = False + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") - if logged_in_with_persistent: - logger.info("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_compliance_storage(config_path: str, enterprise_id: int): @@ -167,7 +562,7 @@ def main(): logger.info("Keeper External Shares Report (SDK Example)") logger.info("=" * 60 + "\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if not keeper_auth_context: logger.error("Login failed.") return diff --git a/examples/sdk_examples/folder/add_folder.py b/examples/sdk_examples/folder/add_folder.py index c5dd6386..e62eeffb 100644 --- a/examples/sdk_examples/folder/add_folder.py +++ b/examples/sdk_examples/folder/add_folder.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, folder_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, folder_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def add_folder(keeper_auth_context: keeper_auth.KeeperAuth): @@ -133,7 +521,7 @@ def main(): Main entry point for the add folder script. Performs login and adds a new folder. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: add_folder(keeper_auth_context) diff --git a/examples/sdk_examples/folder/delete_folder.py b/examples/sdk_examples/folder/delete_folder.py index 94f9a9f1..0b8c15e6 100644 --- a/examples/sdk_examples/folder/delete_folder.py +++ b/examples/sdk_examples/folder/delete_folder.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def delete_folder(keeper_auth_context: keeper_auth.KeeperAuth): @@ -132,7 +520,7 @@ def main(): Main entry point for the delete folder script. Performs login and deletes a folder. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: delete_folder(keeper_auth_context) diff --git a/examples/sdk_examples/folder/list_folders.py b/examples/sdk_examples/folder/list_folders.py index 67537ee4..32f33c1e 100644 --- a/examples/sdk_examples/folder/list_folders.py +++ b/examples/sdk_examples/folder/list_folders.py @@ -1,66 +1,455 @@ import getpass import sqlite3 +import getpass +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_folders(keeper_auth_context: keeper_auth.KeeperAuth): @@ -121,7 +510,7 @@ def main(): Main entry point for the list folders script. Performs login and lists folders. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_folders(keeper_auth_context) diff --git a/examples/sdk_examples/folder/list_shared_folders.py b/examples/sdk_examples/folder/list_shared_folders.py index a743b7d4..a7b2e3bd 100644 --- a/examples/sdk_examples/folder/list_shared_folders.py +++ b/examples/sdk_examples/folder/list_shared_folders.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_shared_folders(keeper_auth_context: keeper_auth.KeeperAuth): @@ -123,7 +511,7 @@ def main(): Main entry point for the list shared folders script. Performs login and lists shared folders. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_shared_folders(keeper_auth_context) diff --git a/examples/sdk_examples/folder/move_folder.py b/examples/sdk_examples/folder/move_folder.py index 4391f428..e9416aa0 100644 --- a/examples/sdk_examples/folder/move_folder.py +++ b/examples/sdk_examples/folder/move_folder.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def move_folder(keeper_auth_context: keeper_auth.KeeperAuth): @@ -149,7 +537,7 @@ def main(): Main entry point for the move folder script. Performs login and moves a folder. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: move_folder(keeper_auth_context) diff --git a/examples/sdk_examples/folder/update_folder.py b/examples/sdk_examples/folder/update_folder.py index e7d6e5c6..9a293490 100644 --- a/examples/sdk_examples/folder/update_folder.py +++ b/examples/sdk_examples/folder/update_folder.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, folder_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, folder_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def update_folder(keeper_auth_context: keeper_auth.KeeperAuth): @@ -127,7 +515,7 @@ def main(): Main entry point for the update folder script. Performs login and updates a folder. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: update_folder(keeper_auth_context) diff --git a/examples/sdk_examples/import_export/export_vault.py b/examples/sdk_examples/import_export/export_vault.py index 2d9ede7d..29525192 100644 --- a/examples/sdk_examples/import_export/export_vault.py +++ b/examples/sdk_examples/import_export/export_vault.py @@ -1,46 +1,454 @@ import getpass import sqlite3 import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def export_vault(keeper_auth_context: keeper_auth.KeeperAuth): @@ -77,7 +485,7 @@ def export_vault(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: export_vault(keeper_auth_context) else: diff --git a/examples/sdk_examples/import_export/vault_summary.py b/examples/sdk_examples/import_export/vault_summary.py index ad0aeb67..e7df78f4 100644 --- a/examples/sdk_examples/import_export/vault_summary.py +++ b/examples/sdk_examples/import_export/vault_summary.py @@ -1,45 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def show_vault_summary(keeper_auth_context: keeper_auth.KeeperAuth): @@ -69,7 +478,7 @@ def show_vault_summary(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: show_vault_summary(keeper_auth_context) else: diff --git a/examples/sdk_examples/miscellaneous/copy_field.py b/examples/sdk_examples/miscellaneous/copy_field.py index f5e6438c..2b27db89 100644 --- a/examples/sdk_examples/miscellaneous/copy_field.py +++ b/examples/sdk_examples/miscellaneous/copy_field.py @@ -1,45 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def copy_field(keeper_auth_context: keeper_auth.KeeperAuth): @@ -108,7 +517,7 @@ def copy_field(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: copy_field(keeper_auth_context) else: diff --git a/examples/sdk_examples/miscellaneous/get_totp.py b/examples/sdk_examples/miscellaneous/get_totp.py index c5d4833d..9c14251f 100644 --- a/examples/sdk_examples/miscellaneous/get_totp.py +++ b/examples/sdk_examples/miscellaneous/get_totp.py @@ -1,46 +1,453 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record -from keepersdk import utils +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record +try: + import pyperclip +except ImportError: + pyperclip = None +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_totp(keeper_auth_context: keeper_auth.KeeperAuth): @@ -79,7 +486,7 @@ def get_totp(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: get_totp(keeper_auth_context) else: diff --git a/examples/sdk_examples/miscellaneous/list_teams.py b/examples/sdk_examples/miscellaneous/list_teams.py index ade7ebb4..692b9818 100644 --- a/examples/sdk_examples/miscellaneous/list_teams.py +++ b/examples/sdk_examples/miscellaneous/list_teams.py @@ -1,45 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_teams(keeper_auth_context: keeper_auth.KeeperAuth): @@ -65,7 +474,7 @@ def list_teams(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_teams(keeper_auth_context) else: diff --git a/examples/sdk_examples/miscellaneous/password_strength.py b/examples/sdk_examples/miscellaneous/password_strength.py index 5c28ad8a..fd07a43d 100644 --- a/examples/sdk_examples/miscellaneous/password_strength.py +++ b/examples/sdk_examples/miscellaneous/password_strength.py @@ -1,46 +1,455 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import sqlite_storage, vault_online, vault_record from keepersdk import utils -from keepersdk.constants import KEEPER_PUBLIC_HOSTS +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def password_strength_report(keeper_auth_context: keeper_auth.KeeperAuth): @@ -85,7 +494,7 @@ def password_strength_report(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: password_strength_report(keeper_auth_context) else: diff --git a/examples/sdk_examples/one_time_share/list_shares.py b/examples/sdk_examples/one_time_share/list_shares.py index 25a7dc09..7beb69d0 100644 --- a/examples/sdk_examples/one_time_share/list_shares.py +++ b/examples/sdk_examples/one_time_share/list_shares.py @@ -1,47 +1,456 @@ import getpass import sqlite3 +import json +import logging from datetime import datetime +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, ksm_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management from keepersdk import utils +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_one_time_shares(keeper_auth_context: keeper_auth.KeeperAuth): @@ -97,7 +506,7 @@ def list_one_time_shares(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_one_time_shares(keeper_auth_context) else: diff --git a/examples/sdk_examples/record_types/list_record_types.py b/examples/sdk_examples/record_types/list_record_types.py index 464da599..94c710dd 100644 --- a/examples/sdk_examples/record_types/list_record_types.py +++ b/examples/sdk_examples/record_types/list_record_types.py @@ -1,45 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - login_auth_context.resume_session = True - login_auth_context.login(username) - logged_in_with_persistent = True - 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) - print("Device approval request sent.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - login_auth_context.login_step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(login_auth_context.login_step, login_auth.LoginStepTwoFactor): - channel = login_auth_context.login_step.get_channels()[0] - login_auth_context.login_step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_record_types(keeper_auth_context: keeper_auth.KeeperAuth): @@ -79,7 +488,7 @@ def list_record_types(keeper_auth_context: keeper_auth.KeeperAuth): def main(): - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_record_types(keeper_auth_context) else: diff --git a/examples/sdk_examples/records/add_record.py b/examples/sdk_examples/records/add_record.py index 4e015030..ec61e6c4 100644 --- a/examples/sdk_examples/records/add_record.py +++ b/examples/sdk_examples/records/add_record.py @@ -1,72 +1,460 @@ import getpass +import json +import logging import sqlite3 +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def add_record(keeper_auth_context: keeper_auth.KeeperAuth): """ Add a new record to the vault. - + Args: keeper_auth_context: The authenticated Keeper context. """ @@ -75,22 +463,22 @@ def add_record(keeper_auth_context: keeper_auth.KeeperAuth): lambda: conn, vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') ) - + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) vault.sync_down() print("Adding new record to vault...") print("-" * 50) - + new_record = vault_record.PasswordRecord() new_record.title = "Example Record" new_record.login = "user@example.com" new_record.password = "SecurePassword123!" new_record.link = "https://example.com" new_record.notes = "This is an example record created using the Keeper SDK" - + folder_uid = None - + try: record_uid = record_management.add_record_to_folder(vault, new_record, folder_uid) print(f'Successfully added record!') @@ -100,16 +488,16 @@ def add_record(keeper_auth_context: keeper_auth.KeeperAuth): print(f'URL: {new_record.link}') print(f'Notes: {new_record.notes}') print("-" * 50) - + vault.sync_down() - + added_record = vault.vault_data.get_record(record_uid) if added_record: print(f'Verified: Record "{added_record.title}" exists in vault') - + except Exception as e: print(f'Error adding record: {str(e)}') - + vault.close() keeper_auth_context.close() @@ -119,8 +507,8 @@ def main(): Main entry point for the add record script. Performs login and adds a new record. """ - keeper_auth_context = login() - + keeper_auth_context, _ = login() + if keeper_auth_context: add_record(keeper_auth_context) else: diff --git a/examples/sdk_examples/records/delete_attachment.py b/examples/sdk_examples/records/delete_attachment.py index bef650c2..273cea1b 100644 --- a/examples/sdk_examples/records/delete_attachment.py +++ b/examples/sdk_examples/records/delete_attachment.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def delete_attachment(keeper_auth_context: keeper_auth.KeeperAuth): @@ -186,7 +574,7 @@ def main(): Main entry point for the delete attachment script. Performs login and deletes an attachment. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: delete_attachment(keeper_auth_context) diff --git a/examples/sdk_examples/records/delete_record.py b/examples/sdk_examples/records/delete_record.py index 64930b95..770f708d 100644 --- a/examples/sdk_examples/records/delete_record.py +++ b/examples/sdk_examples/records/delete_record.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_types, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_types, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def delete_record(keeper_auth_context: keeper_auth.KeeperAuth): @@ -139,7 +527,7 @@ def main(): Main entry point for the delete record script. Performs login and deletes a record. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: delete_record(keeper_auth_context) diff --git a/examples/sdk_examples/records/download_attachment.py b/examples/sdk_examples/records/download_attachment.py index 3d45906f..d5f459d9 100644 --- a/examples/sdk_examples/records/download_attachment.py +++ b/examples/sdk_examples/records/download_attachment.py @@ -1,67 +1,455 @@ import getpass import sqlite3 import os +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, attachment +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, attachment + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def download_attachments(keeper_auth_context: keeper_auth.KeeperAuth): @@ -155,7 +543,7 @@ def main(): Main entry point for the download attachment script. Performs login and downloads attachments. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: download_attachments(keeper_auth_context) diff --git a/examples/sdk_examples/records/file_report.py b/examples/sdk_examples/records/file_report.py index abc51751..6dd0ddc7 100644 --- a/examples/sdk_examples/records/file_report.py +++ b/examples/sdk_examples/records/file_report.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def generate_file_report(keeper_auth_context: keeper_auth.KeeperAuth): @@ -192,7 +580,7 @@ def main(): Main entry point for the file report script. Performs login and generates file attachment report. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_file_report(keeper_auth_context) diff --git a/examples/sdk_examples/records/find_duplicate.py b/examples/sdk_examples/records/find_duplicate.py index 6decc90c..56207f68 100644 --- a/examples/sdk_examples/records/find_duplicate.py +++ b/examples/sdk_examples/records/find_duplicate.py @@ -1,67 +1,454 @@ import getpass import sqlite3 -from typing import Dict, List +import json +import logging +from typing import Dict, Optional, List -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def find_duplicates(keeper_auth_context: keeper_auth.KeeperAuth): @@ -186,7 +573,7 @@ def main(): Main entry point for the find duplicate script. Performs login and finds duplicate records. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: find_duplicates(keeper_auth_context) diff --git a/examples/sdk_examples/records/get_record.py b/examples/sdk_examples/records/get_record.py index 80482996..1aed0039 100644 --- a/examples/sdk_examples/records/get_record.py +++ b/examples/sdk_examples/records/get_record.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_record(keeper_auth_context: keeper_auth.KeeperAuth): @@ -221,7 +609,7 @@ def main(): Main entry point for the get record script. Performs login and displays record details. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: get_record(keeper_auth_context) diff --git a/examples/sdk_examples/records/list_records.py b/examples/sdk_examples/records/list_records.py index 9e3ccad0..e78bb84d 100644 --- a/examples/sdk_examples/records/list_records.py +++ b/examples/sdk_examples/records/list_records.py @@ -26,9 +26,15 @@ except ImportError: pyperclip = None - logger = utils.get_logger() -logger.setLevel(logging.WARNING) +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): @@ -68,6 +74,16 @@ class LoginFlow: def __init__(self) -> None: self._config = configuration.JsonConfigurationStorage() self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent def run(self) -> Optional[keeper_auth.KeeperAuth]: """ @@ -78,6 +94,7 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: """ server = self._ensure_server() keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint login_auth_context = login_auth.LoginAuth(keeper_endpoint) username = self._config.get().last_login or input("Enter username: ") @@ -136,6 +153,7 @@ def _handle_device_approval( "ask admin to approve this device and then press return/enter to resume" ) input() + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: password = getpass.getpass("Enter password: ") @@ -403,6 +421,36 @@ def _two_factor_code_to_duration( return login_auth.TwoFactorDuration.EveryLogin +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: """ List all records in the vault. @@ -423,6 +471,8 @@ def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: print("-" * 50) for record in vault.vault_data.records(): print(f"Title: {record.title}") + print(f"Record UID: {record.record_uid}") + print(f"Record Type: {record.record_type}") if record.version == 2: legacy_record = vault.vault_data.load_record(record.record_uid) @@ -430,9 +480,6 @@ def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: print(f"Username: {legacy_record.login}") print(f"URL: {legacy_record.link}") - elif record.version >= 3: - print(f"Record Type: {record.record_type}") - print("-" * 50) vault.close() @@ -441,8 +488,7 @@ def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: def main() -> None: """Run login and list all vault records.""" - login_flow = LoginFlow() - keeper_auth_context = login_flow.run() + keeper_auth_context, _ = login() if keeper_auth_context: list_records(keeper_auth_context) diff --git a/examples/sdk_examples/records/record_history.py b/examples/sdk_examples/records/record_history.py index 3593d5c3..44b5c8f2 100644 --- a/examples/sdk_examples/records/record_history.py +++ b/examples/sdk_examples/records/record_history.py @@ -1,68 +1,456 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional from datetime import datetime -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import sqlite_storage, vault_online from keepersdk import utils -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_record_history(keeper_auth_context: keeper_auth.KeeperAuth): @@ -154,7 +542,7 @@ def main(): Main entry point for the record history script. Performs login and displays record history. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_record_history(keeper_auth_context) diff --git a/examples/sdk_examples/records/search_record.py b/examples/sdk_examples/records/search_record.py index 0045e96a..cfd16725 100644 --- a/examples/sdk_examples/records/search_record.py +++ b/examples/sdk_examples/records/search_record.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def search_records(keeper_auth_context: keeper_auth.KeeperAuth): @@ -127,7 +515,7 @@ def main(): Main entry point for the search record script. Performs login and searches for records. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: search_records(keeper_auth_context) diff --git a/examples/sdk_examples/records/update_record.py b/examples/sdk_examples/records/update_record.py index 02fc7458..eed8459f 100644 --- a/examples/sdk_examples/records/update_record.py +++ b/examples/sdk_examples/records/update_record.py @@ -1,66 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def update_record(keeper_auth_context: keeper_auth.KeeperAuth): @@ -149,7 +537,7 @@ def main(): Main entry point for the update record script. Performs login and updates a record. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: update_record(keeper_auth_context) diff --git a/examples/sdk_examples/records/upload_attachment.py b/examples/sdk_examples/records/upload_attachment.py index 71c1e63b..f6d56fb1 100644 --- a/examples/sdk_examples/records/upload_attachment.py +++ b/examples/sdk_examples/records/upload_attachment.py @@ -1,67 +1,455 @@ import getpass import sqlite3 import os +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, vault_record, attachment, record_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, vault_record, attachment, record_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth_context: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def upload_attachment(keeper_auth_context: keeper_auth.KeeperAuth): @@ -138,7 +526,7 @@ def main(): Main entry point for the upload attachment script. Performs login and uploads an attachment. """ - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: upload_attachment(keeper_auth_context) diff --git a/examples/sdk_examples/secrets_manager/create_app.py b/examples/sdk_examples/secrets_manager/create_app.py index 63ebd4ad..75e563d1 100644 --- a/examples/sdk_examples/secrets_manager/create_app.py +++ b/examples/sdk_examples/secrets_manager/create_app.py @@ -1,59 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, ksm_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def create_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAuth): @@ -98,7 +493,7 @@ def create_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAu def main(): """Main function to orchestrate login and Secrets Manager application creation.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: create_secrets_manager_application(keeper_auth_context) diff --git a/examples/sdk_examples/secrets_manager/get_app.py b/examples/sdk_examples/secrets_manager/get_app.py index a943c2d0..3c4df012 100644 --- a/examples/sdk_examples/secrets_manager/get_app.py +++ b/examples/sdk_examples/secrets_manager/get_app.py @@ -1,59 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, ksm_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def get_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAuth): @@ -126,7 +521,7 @@ def get_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAuth) def main(): """Main function to orchestrate login and get Secrets Manager application details.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: get_secrets_manager_application(keeper_auth_context) diff --git a/examples/sdk_examples/secrets_manager/list_apps.py b/examples/sdk_examples/secrets_manager/list_apps.py index a225dfbf..e40c3edc 100644 --- a/examples/sdk_examples/secrets_manager/list_apps.py +++ b/examples/sdk_examples/secrets_manager/list_apps.py @@ -1,59 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, ksm_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_secrets_manager_applications(keeper_auth_context: keeper_auth.KeeperAuth): @@ -101,7 +496,7 @@ def list_secrets_manager_applications(keeper_auth_context: keeper_auth.KeeperAut def main(): """Main function to orchestrate login and list Secrets Manager applications.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_secrets_manager_applications(keeper_auth_context) diff --git a/examples/sdk_examples/security_audit/security_audit_report.py b/examples/sdk_examples/security_audit/security_audit_report.py index 8aca4d23..e22b15b4 100644 --- a/examples/sdk_examples/security_audit/security_audit_report.py +++ b/examples/sdk_examples/security_audit/security_audit_report.py @@ -14,13 +14,44 @@ """ import getpass +import json +import logging import sqlite3 import traceback +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, security_audit_report from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) # Table formatting constants @@ -28,59 +59,418 @@ COL_WIDTHS = (35, 20, 8, 8, 8, 8, 8, 8, 8, 10, 6, 20) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + def login(): """ Handle the login process including server selection, authentication, - and multi-factor authentication steps. - + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + Returns: - keeper_auth.KeeperAuth: The authenticated Keeper context, or None if login fails. + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. """ - config = configuration.JsonConfigurationStorage() - - # Server selection - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - - 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) - print("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def format_row(values, widths=COL_WIDTHS): @@ -247,7 +637,7 @@ def main(): print("\nThis tool generates a security audit report for all enterprise users,") print("including password strength analysis and security scores.\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_security_audit_report(keeper_auth_context) diff --git a/examples/sdk_examples/share_report/share_report.py b/examples/sdk_examples/share_report/share_report.py index f664d866..3db1b811 100644 --- a/examples/sdk_examples/share_report/share_report.py +++ b/examples/sdk_examples/share_report/share_report.py @@ -3,13 +3,43 @@ import getpass import sqlite3 import traceback -from typing import Optional, List, Tuple +import json +import logging +from typing import Dict, Optional, List, Tuple -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import share_report, vault_online from keepersdk.vault.sqlite_storage import SqliteVaultStorage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) TABLE_WIDTH = 120 @@ -18,26 +48,418 @@ SUMMARY_COL_WIDTHS = (40, 15, 20) -def login() -> Optional[keeper_auth.KeeperAuth]: - """Handle the login process including server selection and authentication.""" - config = configuration.JsonConfigurationStorage() - server = _get_server(config) - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = _complete_login_steps(login_auth_context) - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def _get_server(config: configuration.JsonConfigurationStorage) -> str: @@ -219,7 +641,7 @@ def main() -> None: print("=" * 60) print("\nThis tool generates share reports for records and folders in your Keeper vault.\n") - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_share_reports(keeper_auth_context) else: diff --git a/examples/sdk_examples/shared_records_report/shared_records_report.py b/examples/sdk_examples/shared_records_report/shared_records_report.py index b78951e4..9a6984f8 100644 --- a/examples/sdk_examples/shared_records_report/shared_records_report.py +++ b/examples/sdk_examples/shared_records_report/shared_records_report.py @@ -7,51 +7,458 @@ import getpass import sqlite3 import traceback -from typing import Optional, List +import json +import logging +from typing import Dict, Optional, List -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import shared_records_report, vault_online from keepersdk.vault.sqlite_storage import SqliteVaultStorage from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS +try: + import pyperclip +except ImportError: + pyperclip = None -def login() -> Optional[keeper_auth.KeeperAuth]: - """Handle login with persistent session support.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - - keeper_endpoint = endpoint.KeeperEndpoint(config, config.get().last_server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - while not login_auth_context.login_step.is_final(): - step = login_auth_context.login_step - if isinstance(step, login_auth.LoginStepDeviceApproval): - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print("Device approval request sent. Approve and press Enter.") - input() - elif isinstance(step, login_auth.LoginStepPassword): - step.verify_password(getpass.getpass('Enter password: ')) - elif isinstance(step, login_auth.LoginStepTwoFactor): - channel = step.get_channels()[0] - step.send_code(channel.channel_uid, getpass.getpass(f'Enter 2FA code: ')) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported: {type(step).__name__}") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def print_report(entries: List[shared_records_report.SharedRecordReportEntry]) -> None: @@ -90,7 +497,7 @@ def main() -> None: keeper_auth_context = None try: - keeper_auth_context = login() + keeper_auth_context, _ = login() if not keeper_auth_context: print("Login failed.") return diff --git a/examples/sdk_examples/trash/list_trash.py b/examples/sdk_examples/trash/list_trash.py index 3aaabe64..3a03f8b1 100644 --- a/examples/sdk_examples/trash/list_trash.py +++ b/examples/sdk_examples/trash/list_trash.py @@ -1,60 +1,455 @@ import getpass import sqlite3 +import json +import logging from datetime import datetime +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, trash_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, trash_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def list_trash_items(keeper_auth_context: keeper_auth.KeeperAuth): @@ -149,7 +544,7 @@ def list_trash_items(keeper_auth_context: keeper_auth.KeeperAuth): def main(): """Main function to orchestrate login and list trash items.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: list_trash_items(keeper_auth_context) diff --git a/examples/sdk_examples/trash/restore_records.py b/examples/sdk_examples/trash/restore_records.py index 307bdfba..15fbfd17 100644 --- a/examples/sdk_examples/trash/restore_records.py +++ b/examples/sdk_examples/trash/restore_records.py @@ -1,59 +1,454 @@ import getpass import sqlite3 +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, trash_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, trash_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def restore_trash_items(keeper_auth_context: keeper_auth.KeeperAuth): @@ -131,7 +526,7 @@ def restore_trash_items(keeper_auth_context: keeper_auth.KeeperAuth): def main(): """Main function to orchestrate login and restore trash items.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: restore_trash_items(keeper_auth_context) diff --git a/examples/sdk_examples/trash/view_trash_record.py b/examples/sdk_examples/trash/view_trash_record.py index 8ce4ecf2..27888a68 100644 --- a/examples/sdk_examples/trash/view_trash_record.py +++ b/examples/sdk_examples/trash/view_trash_record.py @@ -1,60 +1,455 @@ import getpass import sqlite3 +import json +import logging from datetime import datetime +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth -from keepersdk.vault import sqlite_storage, vault_online, trash_management +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, trash_management +try: + import pyperclip +except ImportError: + pyperclip = None -def login(): - """Handle server selection, username input, and authentication steps.""" - config = configuration.JsonConfigurationStorage() - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = None - if config.get().last_login: - username = config.get().last_login - if not username: - username = input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def view_trash_record(keeper_auth_context: keeper_auth.KeeperAuth): @@ -117,7 +512,7 @@ def view_trash_record(keeper_auth_context: keeper_auth.KeeperAuth): def main(): """Main function to orchestrate login and view trash record details.""" - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: view_trash_record(keeper_auth_context) diff --git a/examples/sdk_examples/user_report/user_report.py b/examples/sdk_examples/user_report/user_report.py index 9e95d7b2..3b873c0f 100644 --- a/examples/sdk_examples/user_report/user_report.py +++ b/examples/sdk_examples/user_report/user_report.py @@ -1,62 +1,461 @@ import getpass import sqlite3 import traceback +import json +import logging +from typing import Dict, Optional -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage, user_report from keepersdk.errors import KeeperApiError -from keepersdk.constants import KEEPER_PUBLIC_HOSTS + + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) TABLE_WIDTH = 140 COL_WIDTHS = (35, 20, 10, 15, 25, 25, 30, 30) -def login(): - """Handle login with server selection, authentication, and MFA.""" - config = configuration.JsonConfigurationStorage() - - if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - config.get().last_server = server - else: - server = config.get().last_server - - keeper_endpoint = endpoint.KeeperEndpoint(config, server) - login_auth_context = login_auth.LoginAuth(keeper_endpoint) - - username = config.get().last_login or input('Enter username: ') - - login_auth_context.resume_session = True - login_auth_context.login(username) - - logged_in_with_persistent = True - 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) - print("Device approval request sent. Approve this device and press Enter to continue.") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server else: - raise NotImplementedError(f"Unsupported login step: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - - if logged_in_with_persistent: - print("Successfully logged in with persistent login") - - if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - return login_auth_context.login_step.take_keeper_auth() - - return None + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint def format_row(values): @@ -142,7 +541,7 @@ def main(): print("Keeper Enterprise User Report Generator") print("=" * 60) - keeper_auth_context = login() + keeper_auth_context, _ = login() if keeper_auth_context: generate_user_report(keeper_auth_context) From e5d4a405d39d096a496aba7da30f2f66868b12a8 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 6 Mar 2026 03:23:17 +0530 Subject: [PATCH 02/23] Updated Readme sample scripts (#148) --- README.md | 1067 ++++++++++++++++++++++++++++++---- keepersdk-package/README.md | 1078 +++++++++++++++++++++++++++++++---- 2 files changed, 1916 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index 91ba1ec2..2586b587 100644 --- a/README.md +++ b/README.md @@ -201,63 +201,492 @@ This example demonstrates how to enable persistent login on a new device. Run th ```python import getpass - -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS -# Initialize configuration and authentication context -config = configuration.JsonConfigurationStorage() -if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - - config.get().last_server = server -else: - server = config.get().last_server -keeper_endpoint = endpoint.KeeperEndpoint(config, server) -login_auth_context = login_auth.LoginAuth(keeper_endpoint) - -# Authenticate user -username = None -if config.get().last_login: - username = config.get().last_login -if not username: - username = input('Enter username: ') -login_auth_context.resume_session = True -login_auth_context.login(username) - -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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) -# Check if login was successful -if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - # Obtain authenticated session - keeper_auth_context = login_auth_context.login_step.take_keeper_auth() + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") - # Enable persistent login and register data key for device for the first time + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') keeper_auth.register_data_key_for_device(keeper_auth_context) - mins_per_day = 60*24 - timeout_in_minutes = mins_per_day*30 # 30 days + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) - print("Persistent login turned on successfully and device registered") + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def display_login_info(keeper_auth_context: keeper_auth.KeeperAuth, keeper_endpoint: endpoint.KeeperEndpoint): + """ + Display login success information. + + Args: + keeper_auth_context: The authenticated Keeper context. + keeper_endpoint: The Keeper endpoint with server information. + """ + print("\n" + "=" * 50) + print("LOGIN SUCCESSFUL") + print("=" * 50) + print(f"Username: {keeper_auth_context.auth_context.username}") + print(f"Server: {keeper_endpoint.server}") + print(f"Enterprise Admin: {keeper_auth_context.auth_context.is_enterprise_admin}") + if keeper_auth_context.auth_context.enterprise_id: + print(f"Enterprise ID: {keeper_auth_context.auth_context.enterprise_id}") + print("=" * 50) + keeper_auth_context.close() + + +def main(): + """ + Main entry point for the login script. + Performs login and displays login information. + """ + keeper_auth_context, keeper_endpoint = login() + + if keeper_auth_context: + display_login_info(keeper_auth_context, keeper_endpoint) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() ``` ### SDK Usage Example @@ -266,91 +695,505 @@ Below is a complete example demonstrating authentication, vault synchronization, ```python import getpass +import json +import logging import sqlite3 - -from keepersdk.authentication import login_auth, configuration, endpoint +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import sqlite_storage, vault_online, vault_record -# Initialize configuration and authentication context -config = configuration.JsonConfigurationStorage() -if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - - config.get().last_server = server -else: - server = config.get().last_server -keeper_endpoint = endpoint.KeeperEndpoint(config, server) -login_auth_context = login_auth.LoginAuth(keeper_endpoint) - -# Authenticate user -username = None -if config.get().last_login: - username = config.get().last_login -if not username: - username = input('Enter username: ') -login_auth_context.resume_session = True -login_auth_context.login(username) - -logged_in_with_persistent = True -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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - -if logged_in_with_persistent: - print("Succesfully logged in with persistent login") - -# Check if login was successful -if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - # Obtain authenticated session - keeper_auth_context = login_auth_context.login_step.take_keeper_auth() - - # Set up vault storage (using SQLite in-memory database) - conn = sqlite3.Connection('file::memory:', uri=True) + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + List all records in the vault. + + Args: + keeper_auth_context: The authenticated Keeper context. + """ + conn = sqlite3.Connection("file::memory:", uri=True) vault_storage = sqlite_storage.SqliteVaultStorage( lambda: conn, - vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), ) - - # Initialize vault and synchronize with Keeper servers + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) vault.sync_down() - # 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 + print(f"Title: {record.title}") + print(f"Record UID: {record.record_uid}") + print(f"Record Type: {record.record_type}") + 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}') - print(f'URL: {legacy_record.link}') - - # Handle modern (v3+) records - elif record.version >= 3: - print(f'Record Type: {record.record_type}') - + print(f"Username: {legacy_record.login}") + print(f"URL: {legacy_record.link}") + print("-" * 50) + vault.close() keeper_auth_context.close() + + +def main() -> None: + """Run login and list all vault records.""" + keeper_auth_context, _ = login() + + if keeper_auth_context: + list_records(keeper_auth_context) + else: + print("Login failed. Unable to list records.") + + +if __name__ == "__main__": + main() ``` **Important Security Notes:** diff --git a/keepersdk-package/README.md b/keepersdk-package/README.md index 785b4723..fb830426 100644 --- a/keepersdk-package/README.md +++ b/keepersdk-package/README.md @@ -108,7 +108,7 @@ pip install -e keepersdk-package **Step 4: Install keepersdk into the venv** ```bash -pip install -e keepercli-package +python keepersdk-package/setup.py install ``` Your environment is now ready for SDK development. @@ -123,9 +123,11 @@ The Keeper SDK uses a configuration storage system to manage authentication sett #### **Requirement for client** -If you are accessing keepersdk from a new device, you need to ensure that there is a config.json file present from which the sdk reads credentials. This ensures that the client doesn't contain any hardcoded credentials. Create the .json file in .keeper folder of current user, you might need to create a .keeper folder. +You can create a config.json file to load default credentials safely. This ensures that the client code doesn't contain any hardcoded credentials. Create the .json file in .keeper folder of current user (you might also need to create a .keeper folder if it is not present). -Alternatively you can run the sample login script to give username and password during execution as an alternate to keeping it stored. This will turn on persistent login and would not require re-login for the timeout duration. +Path - Users//keeper/config.json + +Alternatively you can run the sample login script shown in the next section to enter server, username and password during execution. A sample showing the structure of the config.json needed is shown below: @@ -164,7 +166,7 @@ A sample showing the structure of the config.json needed is shown below: } ``` ### SDK Persistent Login Flow -The persistent login flow allows you to authenticate once and remain logged in for a specified timeout period without requiring session refresh. This is particularly useful for automated scripts and long-running applications. +The persistent login flow allows you to authenticate once and remain logged in for a specified timeout period without requiring re-authentication. This is particularly useful for automated scripts and long-running applications. **Key Features:** - **One-time setup**: Configure persistent login on a new device with a single execution @@ -189,65 +191,493 @@ The persistent login flow allows you to authenticate once and remain logged in f This example demonstrates how to enable persistent login on a new device. Run this script once to configure persistent login for subsequent sessions: ```python -import logging import getpass - -from keepersdk.authentication import login_auth, configuration, endpoint, keeper_auth +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS -# Initialize configuration and authentication context -config = configuration.JsonConfigurationStorage() -if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - - config.get().last_server = server -else: - server = config.get().last_server -keeper_endpoint = endpoint.KeeperEndpoint(config, server) -login_auth_context = login_auth.LoginAuth(keeper_endpoint) - -# Authenticate user -username = None -if config.get().last_login: - username = config.get().last_login -if not username: - username = input('Enter username: ') -login_auth_context.resume_session = True -login_auth_context.login(username) - -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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") - input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} -# Check if login was successful -if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - # Obtain authenticated session - keeper_auth_context = login_auth_context.login_step.take_keeper_auth() - # Enable persistent login and register data key for device for the first time +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') keeper_auth.register_data_key_for_device(keeper_auth_context) - mins_per_day = 60*24 - timeout_in_minutes = mins_per_day*30 # 30 days + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) - print("Persistent login turned on successfully and device registered") + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def display_login_info(keeper_auth_context: keeper_auth.KeeperAuth, keeper_endpoint: endpoint.KeeperEndpoint): + """ + Display login success information. + + Args: + keeper_auth_context: The authenticated Keeper context. + keeper_endpoint: The Keeper endpoint with server information. + """ + print("\n" + "=" * 50) + print("LOGIN SUCCESSFUL") + print("=" * 50) + print(f"Username: {keeper_auth_context.auth_context.username}") + print(f"Server: {keeper_endpoint.server}") + print(f"Enterprise Admin: {keeper_auth_context.auth_context.is_enterprise_admin}") + if keeper_auth_context.auth_context.enterprise_id: + print(f"Enterprise ID: {keeper_auth_context.auth_context.enterprise_id}") + print("=" * 50) + keeper_auth_context.close() + + +def main(): + """ + Main entry point for the login script. + Performs login and displays login information. + """ + keeper_auth_context, keeper_endpoint = login() + + if keeper_auth_context: + display_login_info(keeper_auth_context, keeper_endpoint) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() ``` ### SDK Usage Example @@ -256,91 +686,505 @@ Below is a complete example demonstrating authentication, vault synchronization, ```python import getpass +import json +import logging import sqlite3 - -from keepersdk.authentication import login_auth, configuration, endpoint +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) from keepersdk.constants import KEEPER_PUBLIC_HOSTS from keepersdk.vault import sqlite_storage, vault_online, vault_record -# Initialize configuration and authentication context -config = configuration.JsonConfigurationStorage() -if not config.get().last_server: - print("Available server options:") - for region, host in KEEPER_PUBLIC_HOSTS.items(): - print(f" {region}: {host}") - server = input('Enter server (default: keepersecurity.com): ').strip() or 'keepersecurity.com' - - config.get().last_server = server -else: - server = config.get().last_server -keeper_endpoint = endpoint.KeeperEndpoint(config, server) -login_auth_context = login_auth.LoginAuth(keeper_endpoint) - -# Authenticate user -username = None -if config.get().last_login: - username = config.get().last_login -if not username: - username = input('Enter username: ') -login_auth_context.resume_session = True -login_auth_context.login(username) - -logged_in_with_persistent = True -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) - print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) input() - elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): - 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 = getpass.getpass(f'Enter 2FA code for {channel.channel_name}: ') - login_auth_context.login_step.send_code(channel.channel_uid, code) - else: - raise NotImplementedError(f"Unsupported login step type: {type(login_auth_context.login_step).__name__}") - logged_in_with_persistent = False - -if logged_in_with_persistent: - print("Succesfully logged in with persistent login") - -# Check if login was successful -if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - # Obtain authenticated session - keeper_auth_context = login_auth_context.login_step.take_keeper_auth() - - # Set up vault storage (using SQLite in-memory database) - conn = sqlite3.Connection('file::memory:', uri=True) + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + List all records in the vault. + + Args: + keeper_auth_context: The authenticated Keeper context. + """ + conn = sqlite3.Connection("file::memory:", uri=True) vault_storage = sqlite_storage.SqliteVaultStorage( lambda: conn, - vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), ) - - # Initialize vault and synchronize with Keeper servers + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) vault.sync_down() - # 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 + print(f"Title: {record.title}") + print(f"Record UID: {record.record_uid}") + print(f"Record Type: {record.record_type}") + 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}') - print(f'URL: {legacy_record.link}') - - # Handle modern (v3+) records - elif record.version >= 3: - print(f'Record Type: {record.record_type}') - + print(f"Username: {legacy_record.login}") + print(f"URL: {legacy_record.link}") + print("-" * 50) + vault.close() keeper_auth_context.close() + + +def main() -> None: + """Run login and list all vault records.""" + keeper_auth_context, _ = login() + + if keeper_auth_context: + list_records(keeper_auth_context) + else: + print("Login failed. Unable to list records.") + + +if __name__ == "__main__": + main() ``` **Important Security Notes:** From dc4643e69d3b364325d5f418876a4b8a4f49e9b3 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Fri, 6 Mar 2026 03:23:59 +0530 Subject: [PATCH 03/23] Added sdk examples for sharing and ksm commands (#149) --- .../secrets_manager/app_clients.py | 624 ++++++++++++++++++ .../secrets_manager/app_secrets.py | 609 +++++++++++++++++ .../sdk_examples/secrets_manager/share_app.py | 573 ++++++++++++++++ .../sharing_commands/share_folder.py | 576 ++++++++++++++++ .../sharing_commands/share_record.py | 560 ++++++++++++++++ 5 files changed, 2942 insertions(+) create mode 100644 examples/sdk_examples/secrets_manager/app_clients.py create mode 100644 examples/sdk_examples/secrets_manager/app_secrets.py create mode 100644 examples/sdk_examples/secrets_manager/share_app.py create mode 100644 examples/sdk_examples/sharing_commands/share_folder.py create mode 100644 examples/sdk_examples/sharing_commands/share_record.py diff --git a/examples/sdk_examples/secrets_manager/app_clients.py b/examples/sdk_examples/secrets_manager/app_clients.py new file mode 100644 index 00000000..03ccd025 --- /dev/null +++ b/examples/sdk_examples/secrets_manager/app_clients.py @@ -0,0 +1,624 @@ +import getpass +import json +import logging +import sqlite3 +import time +from typing import Dict, List, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import ksm_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +MILLISECONDS_PER_SECOND = 1000 +MILLISECONDS_PER_MINUTE = 60 * 1000 +DEFAULT_FIRST_ACCESS_EXPIRES_MINUTES = 60 + + +def _vault_and_app_uid(keeper_auth_context, app_uid_or_name: str): + """ + Create an in-memory vault, sync, and resolve the KSM app. + Caller must call vault.close() and keeper_auth_context.close() when done. + + Returns: + tuple: (vault, app_uid) for the given app_uid_or_name. + """ + conn = sqlite3.connect("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + app = ksm_management.get_secrets_manager_app(vault, app_uid_or_name) + return vault, app.uid + + +def add_client_to_ksm_app( + keeper_auth_context, + app_uid_or_name: str, + client_name: str, + count: int = 1, + unlock_ip: bool = False, + first_access_expires_in_minutes: int = DEFAULT_FIRST_ACCESS_EXPIRES_MINUTES, + access_expire_in_minutes: Optional[int] = None, +) -> None: + """ + Add a client device to a KSM app using + ksm_management.KSMClientManagement.add_client_to_ksm_app. + Prints the one-time token and device info for each client added. + + Args: + keeper_auth_context: Authenticated Keeper context. + app_uid_or_name: UID or name of the KSM application. + client_name: Optional display name for the client. + count: Number of client tokens to generate (default 1). + unlock_ip: If True, do not lock the client to the current IP. + first_access_expires_in_minutes: Minutes until the one-time token expires (default 60). + access_expire_in_minutes: Optional minutes until app access expires (None = never). + """ + vault, app_uid = _vault_and_app_uid(keeper_auth_context, app_uid_or_name) + try: + master_key = vault.vault_data.get_record_key(record_uid=app_uid) + if not master_key: + raise ValueError(f"Could not retrieve app key for application {app_uid}") + + server = keeper_auth_context.keeper_endpoint.server + current_time_ms = int(time.time() * MILLISECONDS_PER_SECOND) + first_access_expire_duration_ms = ( + current_time_ms + first_access_expires_in_minutes * MILLISECONDS_PER_MINUTE + ) + access_expire_in_ms = ( + access_expire_in_minutes * MILLISECONDS_PER_MINUTE + if access_expire_in_minutes else None + ) + + for i in range(count): + result = ksm_management.KSMClientManagement.add_client_to_ksm_app( + vault=vault, + uid=app_uid, + client_name=client_name or "", + 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, + ) + print(result["output_string"]) + if result.get("token_info"): + print(f" One-Time Token: {result['token_info'].get('oneTimeToken', '')}") + + print(f"Successfully added {count} client(s) to KSM app '{app_uid_or_name}'.") + finally: + vault.close() + keeper_auth_context.close() + + +def remove_clients_from_ksm_app( + keeper_auth_context, + app_uid_or_name: str, + client_names_or_ids: List[str], + force: bool = False, +) -> None: + """ + Remove client devices from a KSM app using + ksm_management.KSMClientManagement.remove_clients_from_ksm_app. + Clients can be identified by display name or by client ID (full or short prefix). + + Args: + keeper_auth_context: Authenticated Keeper context. + app_uid_or_name: UID or name of the KSM application. + client_names_or_ids: List of client display names or client IDs to remove. + force: If True, skip confirmation prompt. + """ + if not client_names_or_ids: + raise ValueError("client_names_or_ids cannot be empty.") + + def confirm_remove(n: int) -> bool: + if force: + return True + answer = input(f"Remove {n} client(s) from this app? [y/N]: ").strip().lower() + return answer in ("y", "yes") + + vault, app_uid = _vault_and_app_uid(keeper_auth_context, app_uid_or_name) + try: + ksm_management.KSMClientManagement.remove_clients_from_ksm_app( + vault=vault, + uid=app_uid, + client_names_and_ids=client_names_or_ids, + callable=confirm_remove, + ) + print(f"Successfully removed client(s) from KSM app '{app_uid_or_name}'.") + finally: + vault.close() + keeper_auth_context.close() + + +def main() -> None: + """ + Main entry point. Logs in, then adds or removes clients to/from a KSM app. + """ + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to manage KSM app clients.") + return + + # Fill in your values here. + app_uid_or_name = "" + action = "add" # Use "add" to add client(s), "remove" to remove client(s) + + if action == "add": + client_name = "My KSM Client" + count = 1 + unlock_ip = False # Set True to allow config from any IP + first_access_expires_in_minutes = 60 # Token validity (max 1440 = 24h) + access_expire_in_minutes = None # None = never expire app access + add_client_to_ksm_app( + keeper_auth_context, + app_uid_or_name, + client_name=client_name, + count=count, + unlock_ip=unlock_ip, + first_access_expires_in_minutes=first_access_expires_in_minutes, + access_expire_in_minutes=access_expire_in_minutes, + ) + elif action == "remove": + client_names_or_ids = ["", ""] # Client ID(s) + force = True # Set True to skip confirmation + remove_clients_from_ksm_app( + keeper_auth_context, + app_uid_or_name, + client_names_or_ids, + force=force, + ) + else: + print(f"Unknown action: {action}. Use 'add' or 'remove'.") + keeper_auth_context.close() + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/secrets_manager/app_secrets.py b/examples/sdk_examples/secrets_manager/app_secrets.py new file mode 100644 index 00000000..e249b187 --- /dev/null +++ b/examples/sdk_examples/secrets_manager/app_secrets.py @@ -0,0 +1,609 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, List, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage +from keepersdk.vault import ksm_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _vault_and_app_uid(keeper_auth_context, app_uid_or_name: str): + """ + Create an in-memory vault, sync, and resolve the KSM app. + Caller must call vault.close() and keeper_auth_context.close() when done. + + Returns: + tuple: (vault, app_uid) for the given app_uid_or_name. + """ + conn = sqlite3.connect("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + app = ksm_management.get_secrets_manager_app(vault, app_uid_or_name) + return vault, app.uid + + +def add_secrets_to_ksm_app( + keeper_auth_context, + app_uid_or_name: str, + secret_uids: List[str], + is_editable: bool = False, + enterprise_data=None, +) -> None: + """ + Add secrets (records or shared folders) to a KSM app using + ksm_management.KSMShareManagement.add_secrets_to_ksm_app. + + Args: + keeper_auth_context: Authenticated Keeper context. + app_uid_or_name: UID or name of the KSM application. + secret_uids: List of record UIDs or shared folder UIDs to add. + is_editable: Whether the app can edit these secrets (default False). + enterprise_data: Enterprise data from EnterpriseLoader. Required for add. + """ + if not secret_uids: + raise ValueError("secret_uids cannot be empty.") + if enterprise_data is None: + raise ValueError( + "Enterprise data is required to add secrets to a KSM app. " + "Use an enterprise admin account and load enterprise data." + ) + + vault, app_uid = _vault_and_app_uid(keeper_auth_context, app_uid_or_name) + try: + master_key = vault.vault_data.get_record_key(record_uid=app_uid) + if not master_key: + raise ValueError(f"Could not retrieve app key for application {app_uid}") + + added = ksm_management.KSMShareManagement.add_secrets_to_ksm_app( + vault=vault, + enterprise=enterprise_data, + app_uid=app_uid, + master_key=master_key, + secret_uids=secret_uids, + is_editable=is_editable, + ) + print(f"Added {len(added)} secret(s) to KSM app '{app_uid_or_name}' (editable={is_editable}):") + for secret_uid, secret_type in added: + print(f" {secret_uid} ({secret_type})") + finally: + vault.close() + keeper_auth_context.close() + + +def remove_secrets_from_ksm_app( + keeper_auth_context, + app_uid_or_name: str, + secret_uids: List[str], +) -> None: + """ + Remove secrets from a KSM app using + ksm_management.KSMShareManagement.remove_secrets_from_ksm_app. + + Args: + keeper_auth_context: Authenticated Keeper context. + app_uid_or_name: UID or name of the KSM application. + secret_uids: List of record or shared folder UIDs to remove from the app. + """ + if not secret_uids: + raise ValueError("secret_uids cannot be empty.") + + vault, app_uid = _vault_and_app_uid(keeper_auth_context, app_uid_or_name) + try: + ksm_management.KSMShareManagement.remove_secrets_from_ksm_app( + vault=vault, + app_uid=app_uid, + secret_uids=secret_uids, + ) + print(f"Removed {len(secret_uids)} secret(s) from KSM app '{app_uid_or_name}'.") + finally: + vault.close() + keeper_auth_context.close() + + +def main() -> None: + """ + Main entry point. Logs in, then adds or removes secrets to/from a KSM app. + Add requires enterprise data (enterprise admin). + """ + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to manage KSM app secrets.") + return + + # Fill in your values here. + app_uid_or_name = "" + secret_uids = ["", ""] # Record or shared folder UIDs + action = "add" # Use "add" to add secrets, "remove" to remove secrets + is_editable = False # Only used when action == "add" + + enterprise_data = None + enterprise = None + if keeper_auth_context.auth_context.is_enterprise_admin: + enterprise_conn = sqlite3.connect("file::memory:", uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + enterprise_storage = sqlite_enterprise_storage.SqliteEnterpriseStorage( + lambda: enterprise_conn, enterprise_id + ) + enterprise = enterprise_loader.EnterpriseLoader( + keeper_auth_context, enterprise_storage + ) + enterprise_data = enterprise.enterprise_data + + try: + if action == "add": + if enterprise_data is None: + print("Add secrets requires an enterprise admin account. Skipping.") + keeper_auth_context.close() + return + add_secrets_to_ksm_app( + keeper_auth_context, + app_uid_or_name, + secret_uids, + is_editable=is_editable, + enterprise_data=enterprise_data, + ) + elif action == "remove": + remove_secrets_from_ksm_app( + keeper_auth_context, + app_uid_or_name, + secret_uids, + ) + else: + print(f"Unknown action: {action}. Use 'add' or 'remove'.") + keeper_auth_context.close() + finally: + if enterprise is not None: + enterprise.close() + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/secrets_manager/share_app.py b/examples/sdk_examples/secrets_manager/share_app.py new file mode 100644 index 00000000..5c1d4b1f --- /dev/null +++ b/examples/sdk_examples/secrets_manager/share_app.py @@ -0,0 +1,573 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, List, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage +from keepersdk.vault import ksm_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def share_ksm_app_with_users( + keeper_auth_context, + app_uid_or_name: str, + user_emails: List[str], + action: str = "grant", + can_edit: bool = True, + can_share: bool = True, + enterprise_data=None, +) -> None: + """ + Share or unshare a Keeper Secrets Manager (KSM) app with users using + ksm_management.share_secrets_manager_app. + + Args: + keeper_auth_context: Authenticated Keeper context. + app_uid_or_name: UID or name of the KSM application. + user_emails: List of user emails to share with or remove access from. + action: 'grant' to share, 'remove' to unshare (revoke access). + can_edit: Allow users to edit the app's shared secrets (default True). + can_share: Allow users to share the app (default True). + enterprise_data: Enterprise data from EnterpriseLoader. Required for + KSM app share/unshare (enterprise admin account). + """ + if not user_emails: + raise ValueError("user_emails cannot be empty.") + if enterprise_data is None: + raise ValueError( + "Enterprise data is required for KSM app share/unshare. " + "Use an enterprise admin account and load enterprise data." + ) + + conn = sqlite3.connect("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + success_responses, failed_responses = ksm_management.share_secrets_manager_app( + vault=vault, + enterprise=enterprise_data, + app_uid=app_uid_or_name, + emails=user_emails, + action=action, + can_edit=can_edit, + can_share=can_share, + ) + + # Note: SDK may return (None, None) due to list.extend() in return; treat as success. + if success_responses: + print(f"KSM app '{app_uid_or_name}': {action} succeeded for {len(success_responses)} user(s).") + if failed_responses: + for r in failed_responses: + print(f" Failed: {r}") + if (not success_responses or success_responses is None) and ( + not failed_responses or failed_responses is None + ): + print(f"KSM app '{app_uid_or_name}': {action} completed for {len(user_emails)} user(s).") + + vault.close() + keeper_auth_context.close() + + +def main() -> None: + """ + Main entry point. Logs in, loads enterprise data if admin, then shares or + unshares a KSM app with the given users. + """ + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to share/unshare KSM app.") + return + + # Fill in your values here. + app_uid_or_name = "" + user_emails = ["user1@example.com", "user2@example.com"] + action = "grant" # Use "grant" to share, "remove" to unshare + can_edit = True + can_share = True + + # KSM app share/unshare requires enterprise data (enterprise admin). + enterprise_data = None + enterprise = None + if keeper_auth_context.auth_context.is_enterprise_admin: + enterprise_conn = sqlite3.connect("file::memory:", uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + enterprise_storage = sqlite_enterprise_storage.SqliteEnterpriseStorage( + lambda: enterprise_conn, enterprise_id + ) + enterprise = enterprise_loader.EnterpriseLoader( + keeper_auth_context, enterprise_storage + ) + enterprise_data = enterprise.enterprise_data + else: + print( + "KSM app share/unshare requires an enterprise admin account. " + "Skipping operation." + ) + keeper_auth_context.close() + return + + try: + share_ksm_app_with_users( + keeper_auth_context, + app_uid_or_name, + user_emails, + action=action, + can_edit=can_edit, + can_share=can_share, + enterprise_data=enterprise_data, + ) + finally: + if enterprise is not None: + enterprise.close() + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/sharing_commands/share_folder.py b/examples/sdk_examples/sharing_commands/share_folder.py new file mode 100644 index 00000000..d6d0b22e --- /dev/null +++ b/examples/sdk_examples/sharing_commands/share_folder.py @@ -0,0 +1,576 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import shares_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _build_shared_folder_info(vault: vault_online.VaultOnline, shared_folder_uid: str): + """ + Build shared folder info dict for FolderShares.prepare_request, like + keepercli ShareFolderCommand._build_shared_folder_info. + Loads the shared folder, revision, and key from the vault. + + Returns: + dict with shared_folder_uid, users, teams, records, + shared_folder_key_unencrypted, default_manage_users, default_manage_records, revision; + or None if the shared folder is not found. + """ + sh_fol = vault.vault_data.load_shared_folder(shared_folder_uid) + if not sh_fol: + return None + sf_uid = shared_folder_uid + sf_entity = vault.vault_data.storage.shared_folders.get_entity(sf_uid) + shared_folder_revision = sf_entity.revision if sf_entity else 0 + sf_unencrypted_key = vault.vault_data.get_shared_folder_key(shared_folder_uid=sf_uid) + if not sf_unencrypted_key: + return None + sf_info = { + "shared_folder_uid": sf_uid, + "users": sh_fol.user_permissions, + "teams": [], + "records": [{"record_uid": r.record_uid} for r in sh_fol.record_permissions], + "shared_folder_key_unencrypted": sf_unencrypted_key, + "default_manage_users": sh_fol.default_manage_users, + "default_manage_records": sh_fol.default_manage_records, + "revision": shared_folder_revision, + } + return sf_info + + +def share_shared_folder_with_users_and_teams( + keeper_auth_context, shared_folder_uid, users, teams, + record_uids=None, manage_records=True, manage_users=True, + can_edit=True, can_share=True, default_record=True, default_account=True, + share_expiration=None): + """ + Share a shared folder with specified users and teams using FolderShares.prepare_request and send_requests. + Args: + keeper_auth_context: Authenticated Keeper context + shared_folder_uid: UID of the shared folder (str) + users: list of user emails + teams: list of team UIDs + record_uids: list of record UIDs to share (optional) + manage_records: allow managing records (default True) + manage_users: allow managing users (default True) + can_edit: allow editing records (default True) + can_share: allow sharing records (default True) + default_record: set default record permissions (default True) + default_account: set default account permissions (default True) + share_expiration: expiration in seconds (optional) + """ + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + sf_info = _build_shared_folder_info(vault, shared_folder_uid) + if not sf_info: + raise ValueError(f"Shared folder not found: {shared_folder_uid}") + + # + # FolderShares.prepare_request parameters: + # action: Type of sharing action. Usually 'grant' to share, 'remove' to unshare. Use ShareAction.GRANT.value to grant access. + # manage_records: Permission for user/team to add/remove/edit records in the shared folder. ON = allow, OFF = deny. + # manage_users: Permission for user/team to add/remove other users/teams from the shared folder. ON = allow, OFF = deny. + # can_edit: Permission for user/team to edit records in the shared folder. ON = allow, OFF = deny. + # can_share: Permission for user/team to share records from the shared folder. ON = allow, OFF = deny. + # default_record: If True, applies can_edit/can_share as default for all records in the folder. + # default_account: If True, applies manage_records/manage_users as default for all users/teams. + # share_expiration: Optional expiration (in seconds) for the share. + # + request = shares_management.FolderShares.prepare_request( + vault=vault, + kwargs={ + "action": shares_management.ShareAction.GRANT.value, # 'grant' to share, 'remove' to unshare + "manage_records": shares_management.ManagePermission.ON.value if manage_records else shares_management.ManagePermission.OFF.value, # Allow managing records + "manage_users": shares_management.ManagePermission.ON.value if manage_users else shares_management.ManagePermission.OFF.value, # Allow managing users/teams + "can_edit": shares_management.ManagePermission.ON.value if can_edit else shares_management.ManagePermission.OFF.value, # Allow editing records + "can_share": shares_management.ManagePermission.ON.value if can_share else shares_management.ManagePermission.OFF.value, # Allow sharing records + }, + curr_sf=sf_info, + users=users, + teams=teams, + rec_uids=record_uids or [], + default_record=default_record, + default_account=default_account, + share_expiration=share_expiration, + ) + + partitioned_requests = [[request]] + shares_management.FolderShares.send_requests(vault, partitioned_requests) + print("Shared folder successfully shared with users and teams.") + vault.close() + keeper_auth_context.close() + + +def main() -> None: + """ + Main entry point for the share folder script. + Performs login and shares a shared folder with users and teams. + """ + keeper_auth_context, _ = login() + if keeper_auth_context: + # Fill in your values here. Set to users or teams as None if not required. + shared_folder_uid = "" + users = ["", ""] + teams = ["", ""] + record_uids = None # Optional. Set to None if not required. + share_shared_folder_with_users_and_teams( + keeper_auth_context, shared_folder_uid, users, teams, record_uids + ) + else: + print("Login failed. Unable to share folder.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/sharing_commands/share_record.py b/examples/sdk_examples/sharing_commands/share_record.py new file mode 100644 index 00000000..8707933b --- /dev/null +++ b/examples/sdk_examples/sharing_commands/share_record.py @@ -0,0 +1,560 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, sqlite_enterprise_storage +from keepersdk.vault import shares_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Device approval request sent. Login to existing vault/console or " + "ask admin to approve this device and then press return/enter to resume" + ) + input() + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + password = getpass.getpass("Enter password: ") + step.verify_password(password) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def share_record_with_user( + keeper_auth_context, + record_uid, + user_email, + can_edit=True, + can_share=True, + share_expiration=None, + enterprise_data=None, +): + """ + Share a record with a user using RecordShares.prep_request and send_requests. + Args: + keeper_auth_context: Authenticated Keeper context + record_uid: UID of the record to share (str) + user_email: Email of the user to share with (str) + can_edit: Allow editing the record (default True) + can_share: Allow sharing the record (default True) + share_expiration: Optional expiration (in seconds) for the share + enterprise_data: Optional IEnterpriseData for enterprise context (e.g. from + enterprise_loader.EnterpriseLoader(keeper_auth_context, enterprise_storage).enterprise_data). + Required for enterprise_access or when resolving share-admin records by name. + """ + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + # RecordShares.prep_request parameters: + # action: ShareAction — 'grant' (share), 'revoke' (remove access), 'owner' (transfer ownership), + # 'cancel' (cancel pending share), 'remove' (revoke share). + # can_edit: Permission for the recipient to edit the record. True = allow, False = view only. + # can_share: Permission for the recipient to share the record. True = allow, False = cannot reshare. + # share_expiration: Optional expiration time for the share (seconds). None = no expiration. + # enterprise: IEnterpriseData for enterprise context; use enterprise_data when sharing in + # an enterprise or when using enterprise_access (e.g. share-admin by name). + # enterprise_access: If True, resolve uid_or_name as share-admin record (requires enterprise). + # recursive: If True and uid_or_name is a folder/shared folder, share all records inside. + # dry_run: If True, validate and return request without sending. + request = shares_management.RecordShares.prep_request( + vault=vault, + emails=[user_email], + action=shares_management.ShareAction.GRANT.value, + uid_or_name=record_uid, + share_expiration=share_expiration, + dry_run=False, + enterprise=enterprise_data, + enterprise_access=False, + recursive=False, + can_edit=can_edit, + can_share=can_share, + ) + + # Send the request + shares_management.RecordShares.send_requests(vault, [request]) + print(f"Record {record_uid} shared with {user_email}.") + vault.close() + keeper_auth_context.close() + + +def main() -> None: + """ + Main entry point for the share record script. + Performs login and shares a record with a user. + """ + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to share record.") + return + + # Fill in your values here + record_uid = "" + user_email = "user1@example.com" + + # Optional: initialize enterprise data (for enterprise accounts or enterprise_access). + # Pass enterprise_data to share_record_with_user when using share-admin features + # or when uid_or_name must be resolved in enterprise context. + enterprise_data = None + enterprise = None + if keeper_auth_context.auth_context.is_enterprise_admin: + enterprise_conn = sqlite3.Connection("file::memory:", uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + enterprise_storage = sqlite_enterprise_storage.SqliteEnterpriseStorage( + lambda: enterprise_conn, enterprise_id + ) + enterprise = enterprise_loader.EnterpriseLoader( + keeper_auth_context, enterprise_storage + ) + enterprise_data = enterprise.enterprise_data + + try: + share_record_with_user( + keeper_auth_context, + record_uid, + user_email, + enterprise_data=enterprise_data, + ) + finally: + if enterprise is not None: + enterprise.close() + + +if __name__ == "__main__": + main() From 984379cb39b798afd237c420a36cef84fd8db634 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 10 Mar 2026 16:56:31 +0530 Subject: [PATCH 04/23] Added email support in examples device approval (#151) --- .../action_report/action_report.py | 70 +++++++++++++++---- .../sdk_examples/aging_report/aging_report.py | 70 +++++++++++++++---- .../sdk_examples/audit_alert/list_alerts.py | 70 +++++++++++++++---- .../sdk_examples/audit_alert/view_alert.py | 70 +++++++++++++++---- .../sdk_examples/audit_report/audit_log.py | 70 +++++++++++++++---- .../audit_report/audit_summary.py | 70 +++++++++++++++---- examples/sdk_examples/auth/login.py | 70 +++++++++++++++---- examples/sdk_examples/auth/logout.py | 70 +++++++++++++++---- examples/sdk_examples/auth/whoami.py | 70 +++++++++++++++---- .../sdk_examples/breachwatch/breach_status.py | 70 +++++++++++++++---- .../breachwatch/breachwatch_report.py | 70 +++++++++++++++---- .../sdk_examples/breachwatch/list_breaches.py | 70 +++++++++++++++---- .../sdk_examples/breachwatch/scan_password.py | 70 +++++++++++++++---- .../sdk_examples/breachwatch/scan_records.py | 70 +++++++++++++++---- .../compliance_record_access_report.py | 70 +++++++++++++++---- .../compliance/compliance_report.py | 70 +++++++++++++++---- .../compliance_shared_folder_report.py | 70 +++++++++++++++---- .../compliance/compliance_summary_report.py | 70 +++++++++++++++---- .../compliance/compliance_team_report.py | 70 +++++++++++++++---- .../enterprise_info/enterprise_info_node.py | 70 +++++++++++++++---- .../enterprise_info/enterprise_info_role.py | 70 +++++++++++++++---- .../enterprise_info/enterprise_info_team.py | 70 +++++++++++++++---- .../enterprise_info/enterprise_info_tree.py | 70 +++++++++++++++---- .../enterprise_info/enterprise_info_user.py | 70 +++++++++++++++---- .../enterprise_node/enterprise_node_view.py | 70 +++++++++++++++---- .../enterprise_role_membership.py | 70 +++++++++++++++---- .../enterprise_role/enterprise_role_view.py | 70 +++++++++++++++---- .../enterprise_team_membership.py | 70 +++++++++++++++---- .../enterprise_team/enterprise_team_view.py | 70 +++++++++++++++---- .../enterprise_user/device_approve.py | 70 +++++++++++++++---- .../enterprise_user/enterprise_user_view.py | 70 +++++++++++++++---- .../enterprise_user/transfer_user.py | 70 +++++++++++++++---- .../ext_shares_report.py | 70 +++++++++++++++---- examples/sdk_examples/folder/add_folder.py | 70 +++++++++++++++---- examples/sdk_examples/folder/delete_folder.py | 70 +++++++++++++++---- examples/sdk_examples/folder/list_folders.py | 70 +++++++++++++++---- .../folder/list_shared_folders.py | 70 +++++++++++++++---- examples/sdk_examples/folder/move_folder.py | 70 +++++++++++++++---- examples/sdk_examples/folder/update_folder.py | 70 +++++++++++++++---- .../import_export/export_vault.py | 70 +++++++++++++++---- .../import_export/vault_summary.py | 70 +++++++++++++++---- .../sdk_examples/miscellaneous/copy_field.py | 70 +++++++++++++++---- .../sdk_examples/miscellaneous/get_totp.py | 70 +++++++++++++++---- .../sdk_examples/miscellaneous/list_teams.py | 70 +++++++++++++++---- .../miscellaneous/password_strength.py | 70 +++++++++++++++---- .../one_time_share/list_shares.py | 70 +++++++++++++++---- .../record_types/list_record_types.py | 70 +++++++++++++++---- examples/sdk_examples/records/add_record.py | 70 +++++++++++++++---- .../sdk_examples/records/delete_attachment.py | 70 +++++++++++++++---- .../sdk_examples/records/delete_record.py | 70 +++++++++++++++---- .../records/download_attachment.py | 70 +++++++++++++++---- examples/sdk_examples/records/file_report.py | 70 +++++++++++++++---- .../sdk_examples/records/find_duplicate.py | 70 +++++++++++++++---- examples/sdk_examples/records/get_record.py | 70 +++++++++++++++---- examples/sdk_examples/records/list_records.py | 70 +++++++++++++++---- .../sdk_examples/records/record_history.py | 70 +++++++++++++++---- .../sdk_examples/records/search_record.py | 70 +++++++++++++++---- .../sdk_examples/records/update_record.py | 70 +++++++++++++++---- .../sdk_examples/records/upload_attachment.py | 70 +++++++++++++++---- .../secrets_manager/app_clients.py | 70 +++++++++++++++---- .../secrets_manager/app_secrets.py | 70 +++++++++++++++---- .../secrets_manager/create_app.py | 70 +++++++++++++++---- .../sdk_examples/secrets_manager/get_app.py | 70 +++++++++++++++---- .../sdk_examples/secrets_manager/list_apps.py | 70 +++++++++++++++---- .../sdk_examples/secrets_manager/share_app.py | 70 +++++++++++++++---- .../security_audit/security_audit_report.py | 70 +++++++++++++++---- .../sdk_examples/share_report/share_report.py | 70 +++++++++++++++---- .../shared_records_report.py | 70 +++++++++++++++---- .../sharing_commands/share_folder.py | 70 +++++++++++++++---- .../sharing_commands/share_record.py | 70 +++++++++++++++---- examples/sdk_examples/trash/list_trash.py | 70 +++++++++++++++---- .../sdk_examples/trash/restore_records.py | 70 +++++++++++++++---- .../sdk_examples/trash/view_trash_record.py | 70 +++++++++++++++---- .../sdk_examples/user_report/user_report.py | 70 +++++++++++++++---- 74 files changed, 4218 insertions(+), 962 deletions(-) diff --git a/examples/sdk_examples/action_report/action_report.py b/examples/sdk_examples/action_report/action_report.py index 3fcf0f75..53dccad3 100644 --- a/examples/sdk_examples/action_report/action_report.py +++ b/examples/sdk_examples/action_report/action_report.py @@ -112,14 +112,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -154,17 +154,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/aging_report/aging_report.py b/examples/sdk_examples/aging_report/aging_report.py index c3ad3c67..9b082977 100644 --- a/examples/sdk_examples/aging_report/aging_report.py +++ b/examples/sdk_examples/aging_report/aging_report.py @@ -117,14 +117,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -159,17 +159,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/audit_alert/list_alerts.py b/examples/sdk_examples/audit_alert/list_alerts.py index 1a27241a..000e4bbe 100644 --- a/examples/sdk_examples/audit_alert/list_alerts.py +++ b/examples/sdk_examples/audit_alert/list_alerts.py @@ -104,14 +104,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -146,17 +146,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/audit_alert/view_alert.py b/examples/sdk_examples/audit_alert/view_alert.py index 3132a90f..c52e8f5d 100644 --- a/examples/sdk_examples/audit_alert/view_alert.py +++ b/examples/sdk_examples/audit_alert/view_alert.py @@ -104,14 +104,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -146,17 +146,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/audit_report/audit_log.py b/examples/sdk_examples/audit_report/audit_log.py index 453d69c1..fe84d1ce 100644 --- a/examples/sdk_examples/audit_report/audit_log.py +++ b/examples/sdk_examples/audit_report/audit_log.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/audit_report/audit_summary.py b/examples/sdk_examples/audit_report/audit_summary.py index 1eecc5fc..e4776d2a 100644 --- a/examples/sdk_examples/audit_report/audit_summary.py +++ b/examples/sdk_examples/audit_report/audit_summary.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/auth/login.py b/examples/sdk_examples/auth/login.py index b6e3118d..1110ac99 100644 --- a/examples/sdk_examples/auth/login.py +++ b/examples/sdk_examples/auth/login.py @@ -103,14 +103,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -145,17 +145,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/auth/logout.py b/examples/sdk_examples/auth/logout.py index c83f8b5a..63806d4e 100644 --- a/examples/sdk_examples/auth/logout.py +++ b/examples/sdk_examples/auth/logout.py @@ -104,14 +104,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -146,17 +146,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/auth/whoami.py b/examples/sdk_examples/auth/whoami.py index 2014fa44..9cd5413c 100644 --- a/examples/sdk_examples/auth/whoami.py +++ b/examples/sdk_examples/auth/whoami.py @@ -103,14 +103,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -145,17 +145,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/breachwatch/breach_status.py b/examples/sdk_examples/breachwatch/breach_status.py index 535a93d4..a0f0a917 100644 --- a/examples/sdk_examples/breachwatch/breach_status.py +++ b/examples/sdk_examples/breachwatch/breach_status.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/breachwatch/breachwatch_report.py b/examples/sdk_examples/breachwatch/breachwatch_report.py index a21a238a..e499e025 100644 --- a/examples/sdk_examples/breachwatch/breachwatch_report.py +++ b/examples/sdk_examples/breachwatch/breachwatch_report.py @@ -118,14 +118,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -160,17 +160,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/breachwatch/list_breaches.py b/examples/sdk_examples/breachwatch/list_breaches.py index 18b00b4e..f5c1745d 100644 --- a/examples/sdk_examples/breachwatch/list_breaches.py +++ b/examples/sdk_examples/breachwatch/list_breaches.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/breachwatch/scan_password.py b/examples/sdk_examples/breachwatch/scan_password.py index cbaf7d18..3e846df7 100644 --- a/examples/sdk_examples/breachwatch/scan_password.py +++ b/examples/sdk_examples/breachwatch/scan_password.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/breachwatch/scan_records.py b/examples/sdk_examples/breachwatch/scan_records.py index 811a206e..b0f0a373 100644 --- a/examples/sdk_examples/breachwatch/scan_records.py +++ b/examples/sdk_examples/breachwatch/scan_records.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/compliance/compliance_record_access_report.py b/examples/sdk_examples/compliance/compliance_record_access_report.py index 914cb480..c24da7b1 100644 --- a/examples/sdk_examples/compliance/compliance_record_access_report.py +++ b/examples/sdk_examples/compliance/compliance_record_access_report.py @@ -120,14 +120,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -162,17 +162,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/compliance/compliance_report.py b/examples/sdk_examples/compliance/compliance_report.py index 2ee03178..9461baf4 100644 --- a/examples/sdk_examples/compliance/compliance_report.py +++ b/examples/sdk_examples/compliance/compliance_report.py @@ -118,14 +118,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -160,17 +160,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/compliance/compliance_shared_folder_report.py b/examples/sdk_examples/compliance/compliance_shared_folder_report.py index d22dfae9..f483d562 100644 --- a/examples/sdk_examples/compliance/compliance_shared_folder_report.py +++ b/examples/sdk_examples/compliance/compliance_shared_folder_report.py @@ -118,14 +118,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -160,17 +160,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/compliance/compliance_summary_report.py b/examples/sdk_examples/compliance/compliance_summary_report.py index 5b1523a5..4d2f02c1 100644 --- a/examples/sdk_examples/compliance/compliance_summary_report.py +++ b/examples/sdk_examples/compliance/compliance_summary_report.py @@ -120,14 +120,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -162,17 +162,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/compliance/compliance_team_report.py b/examples/sdk_examples/compliance/compliance_team_report.py index 7e9e0faf..8e0676b5 100644 --- a/examples/sdk_examples/compliance/compliance_team_report.py +++ b/examples/sdk_examples/compliance/compliance_team_report.py @@ -119,14 +119,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -161,17 +161,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_node.py b/examples/sdk_examples/enterprise_info/enterprise_info_node.py index 7290b115..ae65ae53 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_node.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_node.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_role.py b/examples/sdk_examples/enterprise_info/enterprise_info_role.py index ba482be3..670e1077 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_role.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_role.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_team.py b/examples/sdk_examples/enterprise_info/enterprise_info_team.py index a7271381..503033fa 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_team.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_team.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_tree.py b/examples/sdk_examples/enterprise_info/enterprise_info_tree.py index cf846f30..ba4c5632 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_tree.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_tree.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_info/enterprise_info_user.py b/examples/sdk_examples/enterprise_info/enterprise_info_user.py index 585fef23..cc905c6f 100644 --- a/examples/sdk_examples/enterprise_info/enterprise_info_user.py +++ b/examples/sdk_examples/enterprise_info/enterprise_info_user.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_node/enterprise_node_view.py b/examples/sdk_examples/enterprise_node/enterprise_node_view.py index d79bb08f..bb8ed43e 100644 --- a/examples/sdk_examples/enterprise_node/enterprise_node_view.py +++ b/examples/sdk_examples/enterprise_node/enterprise_node_view.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_role/enterprise_role_membership.py b/examples/sdk_examples/enterprise_role/enterprise_role_membership.py index 57252138..c9593eea 100644 --- a/examples/sdk_examples/enterprise_role/enterprise_role_membership.py +++ b/examples/sdk_examples/enterprise_role/enterprise_role_membership.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_role/enterprise_role_view.py b/examples/sdk_examples/enterprise_role/enterprise_role_view.py index 59141e4b..7acdccee 100644 --- a/examples/sdk_examples/enterprise_role/enterprise_role_view.py +++ b/examples/sdk_examples/enterprise_role/enterprise_role_view.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_team/enterprise_team_membership.py b/examples/sdk_examples/enterprise_team/enterprise_team_membership.py index e7e89233..3f715a0c 100644 --- a/examples/sdk_examples/enterprise_team/enterprise_team_membership.py +++ b/examples/sdk_examples/enterprise_team/enterprise_team_membership.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_team/enterprise_team_view.py b/examples/sdk_examples/enterprise_team/enterprise_team_view.py index e85d06a3..63ba9126 100644 --- a/examples/sdk_examples/enterprise_team/enterprise_team_view.py +++ b/examples/sdk_examples/enterprise_team/enterprise_team_view.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_user/device_approve.py b/examples/sdk_examples/enterprise_user/device_approve.py index a5055a82..a0c9b689 100644 --- a/examples/sdk_examples/enterprise_user/device_approve.py +++ b/examples/sdk_examples/enterprise_user/device_approve.py @@ -112,14 +112,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -154,17 +154,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_user/enterprise_user_view.py b/examples/sdk_examples/enterprise_user/enterprise_user_view.py index aac07b53..bc2799ef 100644 --- a/examples/sdk_examples/enterprise_user/enterprise_user_view.py +++ b/examples/sdk_examples/enterprise_user/enterprise_user_view.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/enterprise_user/transfer_user.py b/examples/sdk_examples/enterprise_user/transfer_user.py index a7592a3b..9b1413dc 100644 --- a/examples/sdk_examples/enterprise_user/transfer_user.py +++ b/examples/sdk_examples/enterprise_user/transfer_user.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/external_shares_report/ext_shares_report.py b/examples/sdk_examples/external_shares_report/ext_shares_report.py index 995b46a1..4baaf8ca 100644 --- a/examples/sdk_examples/external_shares_report/ext_shares_report.py +++ b/examples/sdk_examples/external_shares_report/ext_shares_report.py @@ -118,14 +118,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -160,17 +160,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/add_folder.py b/examples/sdk_examples/folder/add_folder.py index e62eeffb..98c6eee7 100644 --- a/examples/sdk_examples/folder/add_folder.py +++ b/examples/sdk_examples/folder/add_folder.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/delete_folder.py b/examples/sdk_examples/folder/delete_folder.py index 0b8c15e6..acca7b6c 100644 --- a/examples/sdk_examples/folder/delete_folder.py +++ b/examples/sdk_examples/folder/delete_folder.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/list_folders.py b/examples/sdk_examples/folder/list_folders.py index 32f33c1e..8f7f0e98 100644 --- a/examples/sdk_examples/folder/list_folders.py +++ b/examples/sdk_examples/folder/list_folders.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/list_shared_folders.py b/examples/sdk_examples/folder/list_shared_folders.py index a7b2e3bd..0053db03 100644 --- a/examples/sdk_examples/folder/list_shared_folders.py +++ b/examples/sdk_examples/folder/list_shared_folders.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/move_folder.py b/examples/sdk_examples/folder/move_folder.py index e9416aa0..17123186 100644 --- a/examples/sdk_examples/folder/move_folder.py +++ b/examples/sdk_examples/folder/move_folder.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/folder/update_folder.py b/examples/sdk_examples/folder/update_folder.py index 9a293490..01b8ac71 100644 --- a/examples/sdk_examples/folder/update_folder.py +++ b/examples/sdk_examples/folder/update_folder.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/import_export/export_vault.py b/examples/sdk_examples/import_export/export_vault.py index 29525192..8167d99d 100644 --- a/examples/sdk_examples/import_export/export_vault.py +++ b/examples/sdk_examples/import_export/export_vault.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/import_export/vault_summary.py b/examples/sdk_examples/import_export/vault_summary.py index e7df78f4..67c3a43a 100644 --- a/examples/sdk_examples/import_export/vault_summary.py +++ b/examples/sdk_examples/import_export/vault_summary.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/miscellaneous/copy_field.py b/examples/sdk_examples/miscellaneous/copy_field.py index 2b27db89..7c6ece13 100644 --- a/examples/sdk_examples/miscellaneous/copy_field.py +++ b/examples/sdk_examples/miscellaneous/copy_field.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/miscellaneous/get_totp.py b/examples/sdk_examples/miscellaneous/get_totp.py index 9c14251f..e7b8c84d 100644 --- a/examples/sdk_examples/miscellaneous/get_totp.py +++ b/examples/sdk_examples/miscellaneous/get_totp.py @@ -104,14 +104,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -146,17 +146,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/miscellaneous/list_teams.py b/examples/sdk_examples/miscellaneous/list_teams.py index 692b9818..a2a186c6 100644 --- a/examples/sdk_examples/miscellaneous/list_teams.py +++ b/examples/sdk_examples/miscellaneous/list_teams.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/miscellaneous/password_strength.py b/examples/sdk_examples/miscellaneous/password_strength.py index fd07a43d..c6141a34 100644 --- a/examples/sdk_examples/miscellaneous/password_strength.py +++ b/examples/sdk_examples/miscellaneous/password_strength.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/one_time_share/list_shares.py b/examples/sdk_examples/one_time_share/list_shares.py index 7beb69d0..a9b626ec 100644 --- a/examples/sdk_examples/one_time_share/list_shares.py +++ b/examples/sdk_examples/one_time_share/list_shares.py @@ -107,14 +107,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -149,17 +149,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/record_types/list_record_types.py b/examples/sdk_examples/record_types/list_record_types.py index 94c710dd..f22d2ed2 100644 --- a/examples/sdk_examples/record_types/list_record_types.py +++ b/examples/sdk_examples/record_types/list_record_types.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/add_record.py b/examples/sdk_examples/records/add_record.py index ec61e6c4..ce0b6e3a 100644 --- a/examples/sdk_examples/records/add_record.py +++ b/examples/sdk_examples/records/add_record.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/delete_attachment.py b/examples/sdk_examples/records/delete_attachment.py index 273cea1b..da034870 100644 --- a/examples/sdk_examples/records/delete_attachment.py +++ b/examples/sdk_examples/records/delete_attachment.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/delete_record.py b/examples/sdk_examples/records/delete_record.py index 770f708d..887e663b 100644 --- a/examples/sdk_examples/records/delete_record.py +++ b/examples/sdk_examples/records/delete_record.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/download_attachment.py b/examples/sdk_examples/records/download_attachment.py index d5f459d9..4c491046 100644 --- a/examples/sdk_examples/records/download_attachment.py +++ b/examples/sdk_examples/records/download_attachment.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/file_report.py b/examples/sdk_examples/records/file_report.py index 6dd0ddc7..1e84f3b6 100644 --- a/examples/sdk_examples/records/file_report.py +++ b/examples/sdk_examples/records/file_report.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/find_duplicate.py b/examples/sdk_examples/records/find_duplicate.py index 56207f68..ac603785 100644 --- a/examples/sdk_examples/records/find_duplicate.py +++ b/examples/sdk_examples/records/find_duplicate.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/get_record.py b/examples/sdk_examples/records/get_record.py index 1aed0039..be0a5f75 100644 --- a/examples/sdk_examples/records/get_record.py +++ b/examples/sdk_examples/records/get_record.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/list_records.py b/examples/sdk_examples/records/list_records.py index e78bb84d..1679d3bf 100644 --- a/examples/sdk_examples/records/list_records.py +++ b/examples/sdk_examples/records/list_records.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/record_history.py b/examples/sdk_examples/records/record_history.py index 44b5c8f2..b68d767e 100644 --- a/examples/sdk_examples/records/record_history.py +++ b/examples/sdk_examples/records/record_history.py @@ -107,14 +107,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -149,17 +149,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/search_record.py b/examples/sdk_examples/records/search_record.py index cfd16725..12de67ae 100644 --- a/examples/sdk_examples/records/search_record.py +++ b/examples/sdk_examples/records/search_record.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/update_record.py b/examples/sdk_examples/records/update_record.py index eed8459f..0ed58473 100644 --- a/examples/sdk_examples/records/update_record.py +++ b/examples/sdk_examples/records/update_record.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/records/upload_attachment.py b/examples/sdk_examples/records/upload_attachment.py index f6d56fb1..0a700541 100644 --- a/examples/sdk_examples/records/upload_attachment.py +++ b/examples/sdk_examples/records/upload_attachment.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/app_clients.py b/examples/sdk_examples/secrets_manager/app_clients.py index 03ccd025..e3df4095 100644 --- a/examples/sdk_examples/secrets_manager/app_clients.py +++ b/examples/sdk_examples/secrets_manager/app_clients.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/app_secrets.py b/examples/sdk_examples/secrets_manager/app_secrets.py index e249b187..cd62b111 100644 --- a/examples/sdk_examples/secrets_manager/app_secrets.py +++ b/examples/sdk_examples/secrets_manager/app_secrets.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/create_app.py b/examples/sdk_examples/secrets_manager/create_app.py index 75e563d1..a1cf25dc 100644 --- a/examples/sdk_examples/secrets_manager/create_app.py +++ b/examples/sdk_examples/secrets_manager/create_app.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/get_app.py b/examples/sdk_examples/secrets_manager/get_app.py index 3c4df012..e75d99ed 100644 --- a/examples/sdk_examples/secrets_manager/get_app.py +++ b/examples/sdk_examples/secrets_manager/get_app.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/list_apps.py b/examples/sdk_examples/secrets_manager/list_apps.py index e40c3edc..69569d8c 100644 --- a/examples/sdk_examples/secrets_manager/list_apps.py +++ b/examples/sdk_examples/secrets_manager/list_apps.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/secrets_manager/share_app.py b/examples/sdk_examples/secrets_manager/share_app.py index 5c1d4b1f..ae412567 100644 --- a/examples/sdk_examples/secrets_manager/share_app.py +++ b/examples/sdk_examples/secrets_manager/share_app.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/security_audit/security_audit_report.py b/examples/sdk_examples/security_audit/security_audit_report.py index e22b15b4..c8db0618 100644 --- a/examples/sdk_examples/security_audit/security_audit_report.py +++ b/examples/sdk_examples/security_audit/security_audit_report.py @@ -127,14 +127,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -169,17 +169,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/share_report/share_report.py b/examples/sdk_examples/share_report/share_report.py index 3db1b811..ca6a3d83 100644 --- a/examples/sdk_examples/share_report/share_report.py +++ b/examples/sdk_examples/share_report/share_report.py @@ -116,14 +116,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -158,17 +158,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/shared_records_report/shared_records_report.py b/examples/sdk_examples/shared_records_report/shared_records_report.py index 9a6984f8..27b3138e 100644 --- a/examples/sdk_examples/shared_records_report/shared_records_report.py +++ b/examples/sdk_examples/shared_records_report/shared_records_report.py @@ -115,14 +115,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -157,17 +157,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/sharing_commands/share_folder.py b/examples/sdk_examples/sharing_commands/share_folder.py index d6d0b22e..c761e559 100644 --- a/examples/sdk_examples/sharing_commands/share_folder.py +++ b/examples/sdk_examples/sharing_commands/share_folder.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/sharing_commands/share_record.py b/examples/sdk_examples/sharing_commands/share_record.py index 8707933b..af6eb4d3 100644 --- a/examples/sdk_examples/sharing_commands/share_record.py +++ b/examples/sdk_examples/sharing_commands/share_record.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/trash/list_trash.py b/examples/sdk_examples/trash/list_trash.py index 3a03f8b1..995253a6 100644 --- a/examples/sdk_examples/trash/list_trash.py +++ b/examples/sdk_examples/trash/list_trash.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/trash/restore_records.py b/examples/sdk_examples/trash/restore_records.py index 15fbfd17..0461a2cc 100644 --- a/examples/sdk_examples/trash/restore_records.py +++ b/examples/sdk_examples/trash/restore_records.py @@ -105,14 +105,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -147,17 +147,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/trash/view_trash_record.py b/examples/sdk_examples/trash/view_trash_record.py index 27888a68..cec61893 100644 --- a/examples/sdk_examples/trash/view_trash_record.py +++ b/examples/sdk_examples/trash/view_trash_record.py @@ -106,14 +106,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -148,17 +148,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ diff --git a/examples/sdk_examples/user_report/user_report.py b/examples/sdk_examples/user_report/user_report.py index 3b873c0f..3daf91f7 100644 --- a/examples/sdk_examples/user_report/user_report.py +++ b/examples/sdk_examples/user_report/user_report.py @@ -112,14 +112,14 @@ def run(self) -> Optional[keeper_auth.KeeperAuth]: step = login_auth_context.login_step if isinstance(step, login_auth.LoginStepDeviceApproval): self._handle_device_approval(step) - elif isinstance(step, login_auth.LoginStepPassword): - self._handle_password(step) elif isinstance(step, login_auth.LoginStepTwoFactor): self._handle_two_factor(step) - elif isinstance(step, login_auth.LoginStepSsoDataKey): - self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) elif isinstance(step, login_auth.LoginStepSsoToken): self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) elif isinstance(step, login_auth.LoginStepError): print(f"Login error: ({step.code}) {step.message}") return None @@ -154,17 +154,61 @@ def _ensure_server(self) -> str: def _handle_device_approval( self, step: login_auth.LoginStepDeviceApproval ) -> None: - step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) - print( - "Device approval request sent. Login to existing vault/console or " - "ask admin to approve this device and then press return/enter to resume" - ) - input() - step.resume() + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() def _handle_password(self, step: login_auth.LoginStepPassword) -> None: - password = getpass.getpass("Enter password: ") - step.verify_password(password) + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: channels = [ From e1390d13ef1dcabace9610ea519c796f02b6c716 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 11 Mar 2026 23:56:14 +0530 Subject: [PATCH 05/23] Added support for Commander config in sdk (#150) --- .../keepersdk/authentication/configuration.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/keepersdk-package/src/keepersdk/authentication/configuration.py b/keepersdk-package/src/keepersdk/authentication/configuration.py index dcf2c9d8..2e982692 100644 --- a/keepersdk-package/src/keepersdk/authentication/configuration.py +++ b/keepersdk-package/src/keepersdk/authentication/configuration.py @@ -627,6 +627,99 @@ def last_device(self): return self.get(self.LAST_DEVICE) +def _is_commander_config(obj: Optional[dict]) -> bool: + """Return True if the JSON object looks like commander_config (flat single-device format).""" + if not isinstance(obj, dict) or not obj: + return False + # Commander config has top-level device_token and user, and no "users" array + has_commander_keys = 'device_token' in obj and 'user' in obj and 'server' in obj and 'private_key' in obj and 'clone_code' in obj + has_keeper_structure = isinstance(obj.get('users'), list) and isinstance(obj.get('servers'), list) and isinstance(obj.get('devices'), list) + return bool(has_commander_keys and not has_keeper_structure) + + +def _is_keepersdk_config(obj: Optional[dict]) -> bool: + """Return True if the JSON object looks like keepersdk (users/servers/devices) format.""" + if not isinstance(obj, dict) or not obj: + return False + # Keepersdk config has "users", "servers", and "devices" arrays + has_keeper_structure = isinstance(obj.get('users'), list) and isinstance(obj.get('servers'), list) and isinstance(obj.get('devices'), list) + has_commander_keys = 'device_token' in obj and 'user' in obj and 'server' in obj and 'private_key' in obj and 'clone_code' in obj + return bool(not has_commander_keys and has_keeper_structure) + + +def _keepersdk_config_to_commander_config(data: dict) -> dict: + """Convert keepersdk (users/servers/devices) format to commander_config (flat single-device format). + Keeper format uses lists for users and devices; look up by last_login and device_token.""" + user = data.get('last_login') + server = data.get('last_server') + users_list = data.get('users') or [] + devices_list = data.get('devices') or [] + + device_token = None + if user and isinstance(users_list, list): + for u in users_list: + if isinstance(u, dict) and u.get('user') == user: + last_device = u.get('last_device') + if isinstance(last_device, dict): + device_token = last_device.get('device_token') + break + + private_key = None + clone_code = None + if device_token and isinstance(devices_list, list): + for d in devices_list: + if isinstance(d, dict) and d.get('device_token') == device_token: + private_key = d.get('private_key') + clone_code = d.get('clone_code') + if clone_code is None and d.get('server_info'): + si = d['server_info'] + if isinstance(si, list) and si and isinstance(si[0], dict): + clone_code = si[0].get('clone_code') + break + + commander_data = { + 'server': server, + 'user': user, + 'device_token': device_token, + 'private_key': private_key, + 'clone_code': clone_code, + } + return {**data, **commander_data} + + +def _commander_config_to_keeper_config(data: dict) -> dict: + """Convert commander_config (flat) format to keepersdk (users/servers/devices) format. + Original top-level keys are preserved in the returned dict.""" + server = adjust_servername(data.get('server') or '') + user = adjust_username(data.get('user') or '') + device_token = data.get('device_token') or '' + private_key = data.get('private_key') or '' + clone_code = data.get('clone_code') or '' + + keeper_format = { + 'users': [ + { + 'user': user, + 'server': server, + 'last_device': {'device_token': device_token} if device_token else None, + } + ], + 'servers': [ + {'server': server, 'server_key_id': data.get('server_key_id', 1)}, + ], + 'devices': [ + { + 'device_token': device_token, + 'private_key': private_key, + 'server_info': [{'server': server, **({'clone_code': clone_code} if clone_code else {})}], + } + ] if device_token else [], + 'last_login': user, + 'last_server': server, + } + return {**data, **keeper_format} + + class JsonKeeperConfiguration(dict, IKeeperConfiguration): LAST_SERVER = 'last_server' LAST_LOGIN = 'last_login' @@ -764,6 +857,10 @@ def get(self) -> IKeeperConfiguration: json_conf = json.load(fp) if not isinstance(json_conf, dict): raise Exception('JSON configuration should be an object') + if _is_commander_config(json_conf): + json_conf = _commander_config_to_keeper_config(json_conf) + elif _is_keepersdk_config(json_conf): + json_conf = _keepersdk_config_to_commander_config(json_conf) except Exception as e: json_conf = None logger.debug('Load JSON configuration', exc_info=e) From 2f8094e29345f742e7c40f15f5f61d375b1a7b4e Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Fri, 20 Mar 2026 12:00:06 +0530 Subject: [PATCH 06/23] Kepm policy bugfixes (#153) --- .../src/keepercli/commands/pedm_admin.py | 152 +++++++++++++++--- 1 file changed, 131 insertions(+), 21 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/pedm_admin.py b/keepercli-package/src/keepercli/commands/pedm_admin.py index d354b9b4..1faf7780 100644 --- a/keepercli-package/src/keepercli/commands/pedm_admin.py +++ b/keepercli-package/src/keepercli/commands/pedm_admin.py @@ -59,11 +59,24 @@ def resolve_existing_policies(pedm: admin_plugin.PedmPlugin, policy_names: Any) found_policies: Dict[str, admin_types.PedmPolicy] = {} p: Optional[admin_types.PedmPolicy] if isinstance(policy_names, list): + policy_name_lookup = PedmUtils.get_policy_name_lookup(pedm) for policy_name in policy_names: p = pedm.policies.get_entity(policy_name) if p is None: - raise base.CommandError(f'Policy name "{policy_name}" is not found') - found_policies[p.policy_uid] = p + name_lower = (policy_name.strip().lower() if isinstance(policy_name, str) else '') + match = policy_name_lookup.get(name_lower) + if match is None: + raise base.CommandError(f'Policy name "{policy_name}" is not found') + if isinstance(match, list): + if len(match) > 1: + raise base.CommandError( + f'Policy name "{policy_name}" is not unique. Please use Policy UID' + ) + p = match[0] + else: + p = match + if p is not None: + found_policies[p.policy_uid] = p if len(found_policies) == 0: raise base.CommandError('No policies were found') return list(found_policies.values()) @@ -76,7 +89,17 @@ def resolve_single_policy(pedm: admin_plugin.PedmPlugin, policy_uid: Any) -> adm if isinstance(policy, admin_types.PedmPolicy): return policy - raise base.CommandError(f'Policy UID \"{policy_uid}\" does not exist') + name_lower = policy_uid.strip().lower() + match = PedmUtils.get_policy_name_lookup(pedm).get(name_lower) + if match is not None: + if isinstance(match, list): + if len(match) > 1: + raise base.CommandError( + f'Policy name "{policy_uid}" is not unique. Please use Policy UID' + ) + return match[0] + return match + raise base.CommandError(f'Policy "{policy_uid}" is not found') @staticmethod def get_collection_name_lookup( @@ -108,6 +131,27 @@ def get_orphan_resources(pedm: admin_plugin.PedmPlugin) -> List[str]: links = {x.collection_uid for x in pedm.storage.collection_links.get_all_links() if x.link_type == pedm_pb2.CollectionLinkType.CLT_AGENT} return list(collections.difference(links)) + @staticmethod + def get_policy_name_lookup( + pedm: admin_plugin.PedmPlugin + ) -> Dict[str, Union[admin_types.PedmPolicy, List[admin_types.PedmPolicy]]]: + policy_lookup: Dict[str, Union[admin_types.PedmPolicy, List[admin_types.PedmPolicy]]] = {} + for policy in pedm.policies.get_all_entities(): + if not isinstance(policy.data, dict): + continue + name = policy.data.get('PolicyName') + if not isinstance(name, str) or not name.strip(): + continue + name_lower = name.strip().lower() + existing = policy_lookup.get(name_lower) + if existing is None: + policy_lookup[name_lower] = policy + elif isinstance(existing, list): + existing.append(policy) + else: + policy_lookup[name_lower] = [existing, policy] + return policy_lookup + @staticmethod def resolve_existing_collections( pedm: admin_plugin.PedmPlugin, @@ -478,8 +522,8 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: class PedmAgentDeleteCommand(base.ArgparseCommand): def __init__(self): - parser = argparse.ArgumentParser(prog='update', description='Delete PEDM agents') - parser.add_argument('--force', dest='force', action='store_true', + parser = argparse.ArgumentParser(prog='delete', description='Delete PEDM agents') + parser.add_argument('-f', '--force', dest='force', action='store_true', help='do not prompt for confirmation') parser.add_argument('agent', nargs='+', help='Agent UID(s)') @@ -493,20 +537,36 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: if isinstance(agents, str): agents = [agents] agent_uid_list: List[str] = [] + agent_names: List[str] = [] if isinstance(agents, list): for agent_name in agents: agent = PedmUtils.resolve_single_agent(plugin, agent_name) agent_uid_list.append(agent.agent_uid) + name = (agent.properties or {}).get('MachineName') or agent.agent_uid + agent_names.append(name) if len(agent_uid_list) == 0: return - statuses = plugin.modify_agents( remove_agents=agent_uid_list) + force = kwargs.get('force') is True + if not force: + prompt_msg = f'Do you want to delete {len(agent_uid_list)} agent(s)?' + if len(agent_names) <= 3: + prompt_msg += f' ({", ".join(agent_names)})' + answer = prompt_utils.user_choice(prompt_msg, 'yN') + if answer.lower() not in {'y', 'yes'}: + return + + statuses = plugin.modify_agents(remove_agents=agent_uid_list) if isinstance(statuses.remove, list): for status in statuses.remove: if isinstance(status, admin_types.EntityStatus) and not status.success: utils.get_logger().warning(f'Failed to remove agent "{status.entity_uid}": {status.message}') + if len(agent_names) == 1: + return f'Successfully deleted agent: {agent_names[0]}' + return 'Successfully deleted agents: ' + ', '.join(agent_names) + class PedmAgentEditCommand(base.ArgparseCommand): def __init__(self): @@ -523,13 +583,10 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: base.require_enterprise_admin(context) plugin = context.pedm_plugin - deployment_uid = kwargs.get('deployment') - if deployment_uid: - deployment = plugin.deployments.get_entity(deployment_uid) - if not deployment: - raise base.CommandError(f'Deployment "{deployment_uid}" does not exist') - else: - deployment_uid = None + deployment_uid = None + if kwargs.get('deployment'): + deployment = PedmUtils.resolve_single_deployment(plugin, kwargs.get('deployment')) + deployment_uid = deployment.deployment_uid disabled: Optional[bool] = None enable = kwargs.get('enable') @@ -542,6 +599,7 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: raise base.CommandError(f'"enable" argument must be "on" or "off"') update_agents: List[admin_types.UpdateAgent] = [] + agent_names: List[str] = [] agents = kwargs['agent'] if isinstance(agents, str): agents = [agents] @@ -555,12 +613,17 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: deployment_uid=deployment_uid, disabled=disabled, )) + name = (agent.properties or {}).get('MachineName') or agent.agent_uid + agent_names.append(name) if len(update_agents) > 0: statuses = plugin.modify_agents(update_agents=update_agents) if isinstance(statuses.update, list): for status in statuses.update: if isinstance(status, admin_types.EntityStatus) and not status.success: utils.get_logger().warning(f'Failed to update agent "{status.entity_uid}": {status.message}') + if len(agent_names) == 1: + return f'Successfully updated agent: {agent_names[0]}' + return 'Successfully updated agents: ' + ', '.join(agent_names) class PedmAgentListCommand(base.ArgparseCommand): @@ -1125,15 +1188,57 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: plugin = context.pedm_plugin policy = PedmUtils.resolve_single_policy(plugin, kwargs.get('policy')) - - body = json.dumps(policy.data, indent=4) - filename = kwargs.get('output') - if kwargs.get('format') == 'json' and filename: - with open(filename, 'w') as f: - f.write(body) - else: + data = policy.data or {} + + if kwargs.get('format') == 'json': + body = json.dumps(data, indent=4) + filename = kwargs.get('output') + if filename: + with open(filename, 'w') as f: + f.write(body) return body + # Table format: same columns as policy list - single row summary + def _cell_str(v: Any) -> str: + if v is None: + return '' + if isinstance(v, list): + return ', '.join(str(x) for x in v) if v else '' + return str(v) + + all_agents = utils.base64_url_encode(plugin.all_agents) + actions = data.get('Actions') or {} + on_success = actions.get('OnSuccess') or {} + controls = on_success.get('Controls') or [] + status = data.get('Status') or '' + if policy.disabled: + status = 'off' + + collections = [ + x.collection_uid for x in plugin.storage.collection_links.get_links_by_object(policy.policy_uid) + ] + collections = ['*' if x == all_agents else x for x in collections] + collections.sort() + + headers = ['policy_uid', 'policy_name', 'policy_type', 'status', 'controls', 'users', 'machines', 'applications', 'collections'] + row = [ + policy.policy_uid, + data.get('PolicyName') or '', + data.get('PolicyType') or '', + status, + _cell_str(controls), + _cell_str(data.get('UserCheck')), + _cell_str(data.get('MachineCheck')), + _cell_str(data.get('ApplicationCheck')), + _cell_str(collections), + ] + fmt = kwargs.get('format', 'table') + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + return report_utils.dump_report_data( + [row], headers, fmt=fmt, filename=kwargs.get('output') + ) + class PedmPolicyDeleteCommand(base.ArgparseCommand): def __init__(self): @@ -1141,7 +1246,7 @@ def __init__(self): parser.add_argument('policy', type=str, nargs='+', help='Policy UID or name') super().__init__(parser) - def execute(self, context: KeeperParams, **kwargs) -> None: + def execute(self, context: KeeperParams, **kwargs) -> Any: plugin = context.pedm_plugin policies = PedmUtils.resolve_existing_policies(plugin, kwargs.get('policy')) @@ -1153,6 +1258,11 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if isinstance(status, admin_types.EntityStatus) and not status.success: raise base.CommandError(f'Failed to delete policy "{status.entity_uid}": {status.message}') + names = [(p.data or {}).get('PolicyName') or p.policy_uid for p in policies] + if len(names) == 1: + return f'Successfully deleted policy: {names[0]}' + return '\n'.join(f'Successfully deleted policy: {name}' for name in names) + class PedmPolicyAgentsCommand(base.ArgparseCommand): def __init__(self): From 04008dbaefa1e024a5b5eb5152a4997f0f53041a Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 19 Mar 2026 23:53:40 +0530 Subject: [PATCH 07/23] One time share and record-permission sdk support added --- .../sdk_examples/one_time_share/create_ots.py | 618 +++++++++++++ .../one_time_share/list_shares.py | 77 +- .../sdk_examples/one_time_share/remove_ots.py | 598 +++++++++++++ .../update_record_permissions.py | 611 +++++++++++++ .../src/keepercli/commands/record_edit.py | 4 +- .../commands/record_handling_commands.py | 843 +++++------------- .../src/keepercli/commands/shares.py | 201 +---- .../src/keepercli/helpers/record_utils.py | 47 +- .../src/keepersdk/vault/one_time_share.py | 277 ++++++ .../keepersdk/vault/share_management_utils.py | 412 ++++++++- 10 files changed, 2825 insertions(+), 863 deletions(-) create mode 100644 examples/sdk_examples/one_time_share/create_ots.py create mode 100644 examples/sdk_examples/one_time_share/remove_ots.py create mode 100644 examples/sdk_examples/sharing_commands/update_record_permissions.py create mode 100644 keepersdk-package/src/keepersdk/vault/one_time_share.py diff --git a/examples/sdk_examples/one_time_share/create_ots.py b/examples/sdk_examples/one_time_share/create_ots.py new file mode 100644 index 00000000..b655fbcc --- /dev/null +++ b/examples/sdk_examples/one_time_share/create_ots.py @@ -0,0 +1,618 @@ +from datetime import timedelta +import getpass +import sqlite3 +import json +import logging + +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import one_time_share, sqlite_storage, vault_online +from keepersdk import utils + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _resolve_record_uid( + vault: vault_online.VaultOnline, + record_title_or_uid: str, +) -> str: + """ + Resolve record title or UID to a record UID. + + If record_title_or_uid matches a record UID (record exists), return it. + Otherwise find the first record whose title equals or contains the given string. + """ + if not record_title_or_uid or not record_title_or_uid.strip(): + raise ValueError("Record title or UID must be non-empty.") + + candidate = record_title_or_uid.strip() + + # Try as record UID first + if vault.vault_data.get_record(candidate) is not None: + return candidate + + # Search by title (first match, case-insensitive) + candidate_lower = candidate.lower() + for record_info in vault.vault_data.records(): + if record_info.version not in (2, 3): + continue + if ( + record_info.title.lower() == candidate_lower + or candidate_lower in record_info.title.lower() + ): + return record_info.record_uid + + raise ValueError( + f"No record found matching {record_title_or_uid!r}. " + "Use an existing record title or record UID." + ) + + +def create_one_time_share_example( + vault: vault_online.VaultOnline, + record_title_or_uid: str, + ots_name: str, + expiration_days: int = 7, + is_editable: bool = False, + is_self_destruct: bool = False, +) -> Optional[str]: + """ + Create a one-time share for the given record using the SDK. + + Args: + vault: Initialized VaultOnline instance. + record_title_or_uid: Record title or record UID to share. + ots_name: Label for the one-time share link. + expiration_days: Number of days the share link is valid (max 182). + is_editable: If True, the recipient can edit the shared record. + is_self_destruct: If True, the share is invalidated after first open. + + Returns: + The one-time share URL, or None on error. + """ + record_uid = _resolve_record_uid(vault, record_title_or_uid) + expiration_period = timedelta(days=expiration_days) + + try: + url = one_time_share.create_one_time_share( + vault=vault, + record_uid=record_uid, + expiration_period=expiration_period, + name=ots_name or None, + is_editable=is_editable, + is_self_destruct=is_self_destruct, + ) + print(f"One-time share created for record {record_title_or_uid!r}.") + print(f"Share name: {ots_name or '(unnamed)'}") + print(f"Expires in: {expiration_days} day(s)") + print(f"URL: {url}") + return url + except ValueError as e: + print(f"Error creating one-time share: {e}") + return None + + +def create_one_time_share_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """Build vault from auth context, create one-time share with configured variables, then close.""" + # Record and one-time share parameters + RECORD_TITLE_OR_UID = "My Login" # Record title or record UID to create one-time share for + OTS_NAME = "Share for contractor" # Label for the one-time share link + EXPIRATION_DAYS = 7 # Link valid for 7 days (max 182) + + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + try: + create_one_time_share_example( + vault, + RECORD_TITLE_OR_UID, + OTS_NAME, + expiration_days=EXPIRATION_DAYS, + ) + except Exception as e: + print(f"Error: {e}") + finally: + vault.close() + keeper_auth_context.close() + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + create_one_time_share_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/one_time_share/list_shares.py b/examples/sdk_examples/one_time_share/list_shares.py index a9b626ec..ac695e47 100644 --- a/examples/sdk_examples/one_time_share/list_shares.py +++ b/examples/sdk_examples/one_time_share/list_shares.py @@ -2,7 +2,7 @@ import sqlite3 import json import logging -from datetime import datetime + from typing import Dict, Optional import fido2 @@ -20,7 +20,7 @@ yubikey_authenticate, ) from keepersdk.constants import KEEPER_PUBLIC_HOSTS -from keepersdk.vault import sqlite_storage, vault_online, ksm_management +from keepersdk.vault import one_time_share, sqlite_storage, vault_online from keepersdk import utils try: @@ -497,51 +497,60 @@ def login(): return keeper_auth_context, keeper_endpoint -def list_one_time_shares(keeper_auth_context: keeper_auth.KeeperAuth): - conn = sqlite3.Connection('file::memory:', uri=True) - vault_storage = sqlite_storage.SqliteVaultStorage(lambda: conn, vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8')) +def list_one_time_shares(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) vault.sync_down() try: - record_search = input('Enter record name/UID to check for shares (or leave empty for all records): ').strip() + record_search = input( + "Enter record name/UID to check for shares (or leave empty for all records): " + ).strip() record_uids = [] for record_info in vault.vault_data.records(): if record_info.version not in (2, 3): continue - if not record_search or record_search.lower() in record_info.title.lower() or record_search == record_info.record_uid: + if ( + not record_search + or record_search.lower() in record_info.title.lower() + or record_search == record_info.record_uid + ): record_uids.append(record_info.record_uid) if not record_uids: print("\nNo records found to check for one-time shares") else: - app_infos = ksm_management.get_app_info(vault=vault, app_uid=record_uids[:100]) - shares_found = [] - now = utils.current_milli_time() - for app_info in app_infos: - if not app_info.isExternalShare: - continue - record_uid = utils.base64_url_encode(app_info.appRecordUid) - record_info = vault.vault_data.get_record(record_uid) - record_title = record_info.title if record_info else 'Unknown' - for client in app_info.clients: - shares_found.append({ - 'record_title': record_title, - 'share_name': client.id if client.id else 'Unnamed', - 'created': datetime.fromtimestamp(client.createdOn / 1000) if client.createdOn else None, - 'expires': datetime.fromtimestamp(client.accessExpireOn / 1000) if client.accessExpireOn else None, - 'expired': now > client.accessExpireOn if client.accessExpireOn else False, - 'opened': datetime.fromtimestamp(client.firstAccess / 1000) if client.firstAccess else None - }) - if not shares_found: + shares: list[one_time_share.OneTimeShare] = one_time_share.list_one_time_shares( + vault=vault, + record_uid=record_uids[:1000], + include_expired=True, + ) + if not shares: print("\nNo one-time shares found") else: - print(f"\nOne-Time Shares ({len(shares_found)})\n{'=' * 130}") - print(f"{'Record Title':<30} {'Share Name':<20} {'Created':<20} {'Expires':<20} {'Status':<15}\n{'-' * 130}") - for share in shares_found: - status = 'Expired' if share['expired'] else ('Opened' if share['opened'] else 'Active') - created = share['created'].strftime('%Y-%m-%d %H:%M') if share['created'] else 'N/A' - expires = share['expires'].strftime('%Y-%m-%d %H:%M') if share['expires'] else 'N/A' - print(f"{share['record_title'][:29]:<30} {share['share_name'][:19]:<20} {created:<20} {expires:<20} {status:<15}") - print(f"{'-' * 130}\nTotal: {len(shares_found)}") + print(f"\nOne-Time Shares ({len(shares)})\n{'=' * 130}") + print( + f"{'Record Title':<30} {'Share Name':<20} {'Created':<20} {'Expires':<20} {'Status':<15}\n{'-' * 130}" + ) + for share in shares: + record_info = vault.vault_data.get_record(share.record_uid) + record_title = record_info.title if record_info else "Unknown" + created = ( + share.generated.strftime("%Y-%m-%d %H:%M") + if share.generated + else "N/A" + ) + expires = ( + share.expires.strftime("%Y-%m-%d %H:%M") + if share.expires + else "N/A" + ) + print( + f"{record_title[:29]:<30} {share.share_link_name[:19]:<20} {created:<20} {expires:<20} {share.status:<15}" + ) + print(f"{'-' * 130}\nTotal: {len(shares)}") print("=" * 130) except Exception as e: print(f"Error retrieving one-time shares: {e}") diff --git a/examples/sdk_examples/one_time_share/remove_ots.py b/examples/sdk_examples/one_time_share/remove_ots.py new file mode 100644 index 00000000..21ee011d --- /dev/null +++ b/examples/sdk_examples/one_time_share/remove_ots.py @@ -0,0 +1,598 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import one_time_share, sqlite_storage, vault_online +from keepersdk import utils + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _resolve_record_uid( + vault: vault_online.VaultOnline, + record_title_or_uid: str, +) -> str: + """ + Resolve record title or UID to a record UID. + + If record_title_or_uid matches a record UID (record exists), return it. + Otherwise find the first record whose title equals or contains the given string. + """ + if not record_title_or_uid or not record_title_or_uid.strip(): + raise ValueError("Record title or UID must be non-empty.") + + candidate = record_title_or_uid.strip() + + if vault.vault_data.get_record(candidate) is not None: + return candidate + + candidate_lower = candidate.lower() + for record_info in vault.vault_data.records(): + if record_info.version not in (2, 3): + continue + if ( + record_info.title.lower() == candidate_lower + or candidate_lower in record_info.title.lower() + ): + return record_info.record_uid + + raise ValueError( + f"No record found matching {record_title_or_uid!r}. " + "Use an existing record title or record UID." + ) + + +def remove_one_time_share_example( + vault: vault_online.VaultOnline, + record_title_or_uid: str, + share_identifier: str, +) -> bool: + """ + Remove a one-time share for the given record using the SDK. + + Args: + vault: Initialized VaultOnline instance. + record_title_or_uid: Record title or record UID that has the one-time share. + share_identifier: Full share link ID, or unique prefix. + + Returns: + True if the share was removed, False on error. + """ + record_uid = _resolve_record_uid(vault, record_title_or_uid) + try: + one_time_share.remove_one_time_share( + vault=vault, + record_uid=record_uid, + share_identifier=share_identifier, + ) + print(f"One-time share {share_identifier!r} removed for record {record_title_or_uid!r}.") + return True + except ValueError as e: + print(f"Error removing one-time share: {e}") + return False + + +def remove_one_time_share_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """Build vault from auth context, remove one-time share with configured variables, then close.""" + # Record and share to remove + RECORD_TITLE_OR_UID = "My Login" # Record title or record UID that has the share + SHARE_IDENTIFIER = "Share for contractor" # Share link ID / prefix + + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + try: + remove_one_time_share_example( + vault, + RECORD_TITLE_OR_UID, + SHARE_IDENTIFIER, + ) + except Exception as e: + print(f"Error: {e}") + finally: + vault.close() + keeper_auth_context.close() + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + remove_one_time_share_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/sharing_commands/update_record_permissions.py b/examples/sdk_examples/sharing_commands/update_record_permissions.py new file mode 100644 index 00000000..f35b9a9f --- /dev/null +++ b/examples/sdk_examples/sharing_commands/update_record_permissions.py @@ -0,0 +1,611 @@ +""" +Sample script demonstrating update_record_permissions from share_management_utils. + +Updates record-level permissions (can_edit / can_share) for records in a folder, +either for direct record shares, shared folder record permissions, or both. +Uses the same login and vault setup pattern as share_folder.py. +""" +import getpass +import json +import logging +import sqlite3 +from typing import Any, Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import share_management_utils, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """Enable persistent login and register data key for device.""" + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process. Returns (keeper_auth_context, keeper_endpoint) on success, + or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def update_record_permissions_in_folder( + keeper_auth_context: keeper_auth.KeeperAuth, + action: str, + can_share: bool = False, + can_edit: bool = False, + folder_uid_or_path: Optional[str] = None, + recursive: bool = False, + share_record: bool = True, + share_folder: bool = True, + dry_run: bool = False, + sync_after: bool = True, +) -> Dict[str, Any]: + """ + Update record permissions (can_edit / can_share) in a folder using + share_management_utils.update_record_permissions. + + Args: + keeper_auth_context: Authenticated Keeper context. + action: 'grant' or 'revoke'. + can_share: Whether to change the "can share" permission. + can_edit: Whether to change the "can edit" permission. + folder_uid_or_path: Folder UID or path; None or empty = root. + recursive: If True, include all subfolders. + share_record: If True, update direct record shares. + share_folder: If True, update shared folder record permissions. + dry_run: If True, only compute and return planned updates; do not apply. + sync_after: If True and changes were applied, sync vault down after updates. + + Returns: + Result dict from update_record_permissions (direct_share_updates, + shared_folder_updates, direct_share_errors, shared_folder_errors, + skipped_shared_folders). + """ + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + try: + result = share_management_utils.update_record_permissions( + vault=vault, + action=action, + can_share=can_share, + can_edit=can_edit, + folder_uid_or_path=folder_uid_or_path, + recursive=recursive, + share_record=share_record, + share_folder=share_folder, + dry_run=dry_run, + sync_after=sync_after, + ) + _print_result(result, action, dry_run) + return result + except share_management_utils.ShareValidationError as e: + print(f"Validation error: {e}") + raise + except share_management_utils.ShareNotFoundError as e: + print(f"Folder not found: {e}") + raise + finally: + vault.close() + keeper_auth_context.close() + + +def _print_result(result: Dict[str, Any], action: str, dry_run: bool) -> None: + """Print a summary of update_record_permissions result.""" + prefix = "[dry run] " if dry_run else "" + direct = result.get("direct_share_updates") or [] + sf_updates = result.get("shared_folder_updates") or {} + direct_errors = result.get("direct_share_errors") or [] + sf_errors = result.get("shared_folder_errors") or [] + skipped = result.get("skipped_shared_folders") or {} + + if direct: + print(f"{prefix}Direct share updates: {len(direct)}") + if sf_updates: + total_sf = sum(len(v) for v in sf_updates.values()) + print(f"{prefix}Shared folder record updates: {total_sf} (across {len(sf_updates)} shared folder(s))") + if not direct and not sf_updates and not direct_errors and not sf_errors: + print(f"{prefix}No permission changes to apply for action={action!r}.") + if direct_errors: + print(f"Direct share errors: {len(direct_errors)}") + if sf_errors: + print(f"Shared folder errors: {len(sf_errors)}") + if skipped: + print(f"Skipped shared folders (permissions): {len(skipped)}") + + +def main() -> None: + """ + Main entry point. Logs in and updates record permissions in a folder. + Edit the variables below to match your folder and desired permissions. + """ + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to update record permissions.") + return + + # Configure these for your run: + action = "grant" # or "revoke" + can_share = True + can_edit = True + folder_uid_or_path = "My Folder" # Use folder UID / path like "My Folder" + recursive = False + share_record = True # update direct record shares + share_folder = True # update shared folder record permissions + dry_run = False # set to False to apply changes + + update_record_permissions_in_folder( + keeper_auth_context=keeper_auth_context, + action=action, + can_share=can_share, + can_edit=can_edit, + folder_uid_or_path=folder_uid_or_path, + recursive=recursive, + share_record=share_record, + share_folder=share_folder, + dry_run=dry_run, + sync_after=not dry_run, + ) + if dry_run: + print("Run with dry_run=False to apply the changes.") + + +if __name__ == "__main__": + main() diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index 99bc79c3..ee75d8bf 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -11,7 +11,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 -from keepersdk.vault import (record_types, typed_field_utils, vault_record, attachment, record_facades, +from keepersdk.vault import (record_types, typed_field_utils, vault_record, attachment, record_facades, one_time_share, record_management, vault_online, vault_data, vault_types, vault_utils, vault_extensions, share_management_utils) from keepersdk import crypto, generator @@ -714,7 +714,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: 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) + url = one_time_share.create_one_time_share(context.vault, record.record_uid, expiration_period, is_self_destruct=True) 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}' diff --git a/keepercli-package/src/keepercli/commands/record_handling_commands.py b/keepercli-package/src/keepercli/commands/record_handling_commands.py index a17bc980..af8642db 100644 --- a/keepercli-package/src/keepercli/commands/record_handling_commands.py +++ b/keepercli-package/src/keepercli/commands/record_handling_commands.py @@ -25,13 +25,7 @@ # 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' @@ -40,15 +34,12 @@ 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.' @@ -1111,492 +1102,137 @@ def _format_url(self, url, include_in_output): 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_management_utils.get_record_shares(self.vault, list(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.auth.auth_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.auth.auth_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 +def _report_skipped_shared_folders(vault, skipped_sf: dict) -> None: + """Print table of shared folder records skipped (insufficient permission).""" + table = [] + for shared_folder_uid in skipped_sf: + sf = vault.vault_data.get_shared_folder(shared_folder_uid=shared_folder_uid) + uid, name = shared_folder_uid, (sf.name[:32] if sf else '') + for record_uid in skipped_sf[shared_folder_uid]: + rec = vault.vault_data.get_record(record_uid=record_uid) + table.append([uid, name, record_uid, (rec.title[:32] if rec else '')]) + uid, name = '', '' + if table: + report_utils.dump_report_data( + table, + ['Shared Folder UID', 'Shared Folder Name', 'Record UID', 'Record Title'], + title='SKIP Shared Folder Record Share permission(s). Not permitted', + row_number=True, + ) + logger.info('\n') -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_owner = record.flags.IsOwner - 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') - +def _report_direct_share_plan( + vault, + direct_updates: list, + action_label: str, + change_edit: bool, + change_share: bool, +) -> None: + """Print table of planned direct record share updates.""" + table = [] + for cmd in direct_updates: + rec = vault.vault_data.get_record(record_uid=cmd['record_uid']) + row = [cmd['record_uid'], (rec.title[:32] if rec else ''), cmd['to_username']] + if change_edit: + row.append('Y' if cmd['editable'] else 'N') + if change_share: + row.append('Y' if cmd['shareable'] else 'N') + table.append(row) + headers = ['Record UID', 'Title', 'Email'] + if change_edit: + headers.append('Can Edit') + if change_share: + headers.append('Can Share') + report_utils.dump_report_data( + table, headers, + title=f'{action_label} Direct Record Share permission(s)', + row_number=True, + group_by=0, + ) + logger.info('\n') + + +def _report_shared_folder_plan( + vault, + sf_updates: dict, + action_label: str, + change_edit: bool, + change_share: bool, +) -> None: + """Print table of planned shared folder record updates.""" + table = [] + for shared_folder_uid in sf_updates: + sf = vault.vault_data.get_shared_folder(shared_folder_uid=shared_folder_uid) + uid, name = shared_folder_uid, (sf.name[:32] if sf else '') + for record_uid in sf_updates[shared_folder_uid]: + cmd = sf_updates[shared_folder_uid][record_uid] + rec = vault.vault_data.get_record(record_uid=record_uid) + row = [uid, name, record_uid, (rec.title[:32] if rec else '')] + if change_edit: + row.append('Y' if cmd.canEdit == folder_pb2.BOOLEAN_TRUE else 'N') + if change_share: + row.append('Y' if cmd.canShare == folder_pb2.BOOLEAN_TRUE else 'N') table.append(row) - - headers = ['Record UID', 'Title', 'Email'] - if self.config.change_edit: + uid, name = '', '' + if table: + headers = ['Shared Folder UID', 'Shared Folder Name', 'Record UID', 'Record Title'] + if change_edit: headers.append('Can Edit') - if self.config.change_share: + if 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) + report_utils.dump_report_data( + table, headers, + title=f'{action_label} Shared Folder Record Share permission(s)', + row_number=True, + ) 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 +def _report_record_permission_result( + vault, + result: dict, + *, + should_have: bool, + change_edit: bool, + change_share: bool, + force: bool, + dry_run: bool, +) -> None: + """Report planned updates and skipped items from SDK result. Shows plan when dry_run or not force.""" + direct = result.get('direct_share_updates') or [] + sf_updates = result.get('shared_folder_updates') or {} + skipped_sf = result.get('skipped_shared_folders') or {} + show_plan = dry_run or not force + action_label = 'GRANT' if should_have else 'REVOKE' + + if skipped_sf and dry_run: + _report_skipped_shared_folders(vault, skipped_sf) + if direct and show_plan: + _report_direct_share_plan(vault, direct, action_label, change_edit, change_share) + if sf_updates and show_plan: + _report_shared_folder_plan(vault, sf_updates, action_label, change_edit, change_share) + + +def _report_record_permission_errors(result: dict, action_label: str) -> None: + """Print error tables from apply result (direct share and shared folder).""" + direct_errors = result.get('direct_share_errors') or [] + sf_errors = result.get('shared_folder_errors') or [] + if direct_errors: + report_utils.dump_report_data( + direct_errors, + ['Record UID', 'Email', 'Error Code', 'Message'], + title=f'Failed to {action_label} Direct Record Share permission(s)', + row_number=True, + ) + logger.info('\n') + if sf_errors: + report_utils.dump_report_data( + sf_errors, + ['Shared Folder UID', 'Record UID', 'Error Code'], + title=f'Failed to {action_label} Shared Folder Record Share permission(s)', + ) + logger.info('\n') class RecordPermissionCommand(base.ArgparseCommand): @@ -1610,163 +1246,118 @@ 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') + help='Display the permissions changes without committing them') parser.add_argument('--force', dest='force', action='store_true', - help='Apply permission changes without any confirmation') + 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') + 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') + help='Change a records sharing permissions') parser.add_argument('--share-folder', dest='share_folder', action='store_true', - help='Change a folders sharing permissions') + 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') + 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') + 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') + 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: KeeperParams, folder_name: str): - """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.""" + """Return (share_record, share_folder); default both True if neither set.""" 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 (True, True) if (not share_record and not share_folder) else (share_record, share_folder) + + def _resolve_folder_display_name(self, vault, folder_arg: str) -> str: + """Resolve folder by UID/path; return display name. Raises CommandError if not found.""" + if not folder_arg: + return 'Root' + folder, remaining = share_management_utils.try_resolve_path(vault, folder_arg) + if remaining: + raise base.CommandError(f'Folder "{folder_arg}" not found') + return folder.name or folder_arg + + def _log_permission_request(self, folder_name: str, action_label: str, recursive: bool, + change_edit: bool, change_share: bool, force: bool) -> None: + if 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]}' - ) - + scope = 'recursively' if recursive else 'only' + parts = [] + if change_edit: + parts.append('"Can Edit"') + if change_share: + parts.append('"Can Share"') + logger.info(f'\nRequest to {action_label} {" & ".join(parts)} permission(s) in "{folder_name}" folder {scope}') + def execute(self, context: KeeperParams, **kwargs): - """Execute record permission changes.""" + """Execute record permission changes using SDK update_record_permissions.""" 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) - + vault = context.vault + + 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) 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) + folder_arg = (kwargs.get('folder') or '').strip() + + if not can_share and not can_edit: + raise base.CommandError('Please choose at least one of the following options: can-edit, can-share') + + folder_name = self._resolve_folder_display_name(vault, folder_arg) + action_label = 'GRANT' if action == 'grant' else 'REVOKE' + self._log_permission_request(folder_name, action_label, recursive, can_edit, can_share, force) + + sdk_kw = { + 'can_share': can_share, + 'can_edit': can_edit, + 'folder_uid_or_path': folder_arg or None, + 'recursive': recursive, + 'share_record': share_record, + 'share_folder': share_folder, + } + + try: + result = share_management_utils.update_record_permissions( + vault, action, dry_run=True, sync_after=False, **sdk_kw + ) + except share_management_utils.ShareValidationError as e: + raise base.CommandError(str(e)) + except share_management_utils.ShareNotFoundError as e: + raise base.CommandError(str(e)) + + should_have = action == 'grant' + _report_record_permission_result( + vault, result, + should_have=should_have, + change_edit=can_edit, + change_share=can_share, + force=force, + dry_run=dry_run, + ) + + has_updates = result.get('direct_share_updates') or result.get('shared_folder_updates') + if dry_run or not has_updates: + return + + if not force: + answer = prompt_utils.user_choice( + 'Do you want to proceed with these permission changes?', 'yn', 'n' + ) + if answer.lower() != 'y': + return + + result = share_management_utils.update_record_permissions( + vault, action, dry_run=False, sync_after=True, **sdk_kw + ) + _report_record_permission_errors(result, action_label) diff --git a/keepercli-package/src/keepercli/commands/shares.py b/keepercli-package/src/keepercli/commands/shares.py index 2794969e..1ec67b41 100644 --- a/keepercli-package/src/keepercli/commands/shares.py +++ b/keepercli-package/src/keepercli/commands/shares.py @@ -7,8 +7,8 @@ from keepersdk import utils from keepersdk.authentication import keeper_auth -from keepersdk.proto import record_pb2, APIRequest_pb2 -from keepersdk.vault import ksm_management, vault_online, vault_utils, share_management_utils +from keepersdk.proto import record_pb2 +from keepersdk.vault import one_time_share, share_management_utils, vault_online, vault_utils from keepersdk.vault.shares_management import RecordShares, FolderShares from . import base @@ -21,7 +21,6 @@ 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): @@ -42,27 +41,14 @@ class ManagePermission(Enum): # Constants TIMESTAMP_MILLISECONDS_FACTOR = 1000 TRUNCATE_SUFFIX = '...' -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'] CHUNK_SIZE = 500 -MAX_BATCH_SIZE = 1000 SHARE_LINK_TRUNCATE_LENGTH = 20 SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 TEAMS_THRESHOLD = 500 -ALL_FOLDERS_WILDCARD = '*' ALL_USERS_WILDCARD = '@existing' ALL_USERS_WILDCARD_ALT = '@current' -DEFAULT_ACCOUNT_WILDCARD = '*' -DEFAULT_RECORD_WILDCARD = '*' +DEFAULT_WILDCARD = '*' def set_expiration_fields(obj, expiration): """Set expiration and timerNotificationType fields on proto object if expiration is provided.""" @@ -325,9 +311,9 @@ def _normalize_folder_names(self, folder_names) -> List: def _resolve_shared_folder_uids(self, vault: vault_online.VaultOnline, names: List) -> Set: """Resolve folder names to shared folder UIDs.""" - all_folders = any(x == ALL_FOLDERS_WILDCARD for x in names) + all_folders = any(x == DEFAULT_WILDCARD for x in names) if all_folders: - names = [x for x in names if x != ALL_FOLDERS_WILDCARD] + names = [x for x in names if x != DEFAULT_WILDCARD] 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()} @@ -444,7 +430,7 @@ def _parse_user_arguments(self, vault, kwargs: Dict) -> Dict: } for u in (kwargs.get('user') or []): - if u == DEFAULT_ACCOUNT_WILDCARD: + if u == DEFAULT_WILDCARD: default_account = True elif u in (ALL_USERS_WILDCARD, ALL_USERS_WILDCARD_ALT): all_users = True @@ -502,7 +488,7 @@ def _parse_record_arguments(self, vault, kwargs: Dict) -> Dict: unresolved_names = [] for r in records: - if r == DEFAULT_RECORD_WILDCARD: + if r == DEFAULT_WILDCARD: default_record = True elif r in (ALL_USERS_WILDCARD, ALL_USERS_WILDCARD_ALT): all_records = True @@ -736,9 +722,10 @@ def execute(self, context: KeeperParams, **kwargs): if not record_uids: raise base.CommandError('No records found') - applications = self._get_applications(vault, record_uids) - table_data = self._build_share_table(applications, kwargs) - + shares = one_time_share.list_one_time_shares( + vault, list(record_uids), include_expired=kwargs.get('show_all', False) + ) + table_data = self._build_share_table_from_sdk(shares, kwargs) return self._format_output(table_data, kwargs) def _resolve_record_uids(self, context: KeeperParams, vault, records: List, recursive: bool) -> Set: @@ -790,69 +777,32 @@ def on_folder(f): 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) - 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.""" + def _build_share_table_from_sdk(self, shares: List[one_time_share.OneTimeShare], kwargs): + """Build table data from OneTimeShare list.""" show_all = kwargs.get('show_all', False) verbose = kwargs.get('verbose', False) - now = utils.current_milli_time() - + output_format = kwargs.get('format') 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]) - + for s in shares: + share_link_id = s.share_link_id + if output_format == 'table' and not verbose and len(share_link_id) > SHARE_LINK_TRUNCATE_LENGTH: + share_link_id = share_link_id[:SHARE_LINK_TRUNCATE_LENGTH] + TRUNCATE_SUFFIX + row = [ + s.record_uid, + s.share_link_name, + share_link_id, + s.generated or '', + s.opened or '', + s.expires or '', + ] + if show_all: + row.append(s.status) + table.append(row) return table, fields - def _create_share_link_data(self, app_info, client, verbose: bool, output_format: str, now: int): - """Create share link data dictionary.""" - encoded_client_id = utils.base64_url_encode(client.clientId) - link = { - 'record_uid': utils.base64_url_encode(app_info.appRecordUid), - 'name': client.id, - 'share_link_id': encoded_client_id, - 'generated': datetime.datetime.fromtimestamp(client.createdOn / TIMESTAMP_MILLISECONDS_FACTOR), - 'expires': datetime.datetime.fromtimestamp(client.accessExpireOn / TIMESTAMP_MILLISECONDS_FACTOR), - } - - if output_format == 'table' and not verbose: - link['share_link_id'] = encoded_client_id[:SHARE_LINK_TRUNCATE_LENGTH] + TRUNCATE_SUFFIX - else: - link['share_link_id'] = encoded_client_id - - 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 @@ -914,7 +864,7 @@ def execute(self, context: KeeperParams, **kwargs): raise base.CommandError('URL expiration period parameter \"--expire\" is required.') 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) @@ -931,11 +881,15 @@ def _create_share_urls(self, context: KeeperParams, vault, record_names: List, p 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 + url = one_time_share.create_one_time_share( + vault=vault, + record_uid=record_uid, + expiration_period=period, + name=name or None, + is_editable=is_editable, + is_self_destruct=False, ) - urls[record_uid] = str(url) + urls[record_uid] = url return urls def _handle_output(self, context: KeeperParams, urls: Dict, kwargs): @@ -992,83 +946,24 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): def execute(self, context: KeeperParams, **kwargs): if not context.vault: raise ValueError('Vault is not initialized.') - - vault = context.vault + 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) + try: + record_uid = record_utils.resolve_record(context=context, name=record_name) + one_time_share.remove_one_time_share(vault=vault, record_uid=record_uid, share_identifier=share_name) + logger.info(f'One-time share "{share_name}" is removed from record "{record_name}"') + except ValueError as e: + if 'no one-time shares' in str(e).lower() or 'there are no' in str(e).lower(): + logger.error(f'{str(e)}') + else: + logger.warning(f'{str(e)}') diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 614129c9..fb5e7bd5 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -4,13 +4,10 @@ import hashlib import hmac import re -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 import utils from keepersdk.proto.enterprise_pb2 import GetSharingAdminsRequest, GetSharingAdminsResponse from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils @@ -22,8 +19,6 @@ 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]: @@ -82,46 +77,6 @@ def default_confirm(prompt: str) -> bool: 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, - name: Optional[str] = None, is_editable: bool = False, - is_self_destruct: Optional[bool] = True) -> 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) - - if name: - request.id = name - - request.isSelfDestruct = is_self_destruct - request.isEditable = is_editable - - vault.keeper_auth.execute_auth_rest( - rest_endpoint=EXTERNAL_SHARE_ADD_URL, - request=request, - response_type=Device - ) - - url = urlunparse(( - 'https', - context.auth.keeper_endpoint.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': diff --git a/keepersdk-package/src/keepersdk/vault/one_time_share.py b/keepersdk-package/src/keepersdk/vault/one_time_share.py new file mode 100644 index 00000000..81449c49 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/one_time_share.py @@ -0,0 +1,277 @@ +"""One-time share operations for records.""" + +import dataclasses +import datetime +from typing import List, Optional, Union +from urllib.parse import urlunparse + +from .. import crypto, utils +from ..proto.APIRequest_pb2 import AddExternalShareRequest, Device, RemoveAppClientsRequest +from . import ksm_management, vault_online + + +TIMESTAMP_MILLISECONDS_FACTOR = 1000 +MAX_BATCH_SIZE = 1000 +SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 +EXTERNAL_SHARE_ADD_URL = "vault/external_share_add" +REMOVE_EXTERNAL_SHARE_URL = "vault/external_share_remove" +KEEPER_SECRETS_MANAGER_CLIENT_ID = "KEEPER_SECRETS_MANAGER_CLIENT_ID" +TRUNCATE_SUFFIX = "..." + + +@dataclasses.dataclass +class OneTimeShare: + """One-time share link for a record.""" + + record_uid: str + share_link_name: str + share_link_id: str + generated: Optional[datetime.datetime] + expires: Optional[datetime.datetime] + opened: Optional[datetime.datetime] + accessed: Optional[datetime.datetime] + status: str # 'Expired' | 'Opened' | 'Generated' + + +def list_one_time_shares( + vault: vault_online.VaultOnline, + record_uid: Union[str, List[str]], + include_expired: bool = False, +) -> List[OneTimeShare]: + """ + List one-time shares for the given record UID(s). + + Args: + vault: Initialized VaultOnline instance. + record_uid: Single record UID (str) or list of record UIDs. + include_expired: If True, include shares that have already expired. + Default False returns only active or not-yet-opened shares. + + Returns: + List of OneTimeShare instances. + """ + if vault is None: + raise ValueError("Vault is not initialized.") + + uids = [record_uid] if isinstance(record_uid, str) else list(record_uid) + if not uids: + return [] + + if len(uids) > MAX_BATCH_SIZE: + uids = uids[:MAX_BATCH_SIZE] + + app_infos = ksm_management.get_app_info(vault=vault, app_uid=uids) + now = utils.current_milli_time() + result: List[OneTimeShare] = [] + + for app_info in app_infos: + if not getattr(app_info, "isExternalShare", False): + continue + + record_uid_str = utils.base64_url_encode(app_info.appRecordUid) + + for client in getattr(app_info, "clients", []): + if not include_expired and now > getattr(client, "accessExpireOn", 0): + continue + + _append_share_link(result, app_info, client, record_uid_str, now) + + return result + + +def _append_share_link( + result: List[OneTimeShare], + app_info, + client, + record_uid_str: str, + now: int, +) -> None: + """Build OneTimeShare from app_info/client and append to result.""" + created_ts = getattr(client, "createdOn", 0) or 0 + expires_ts = getattr(client, "accessExpireOn", 0) or 0 + first_access_ts = getattr(client, "firstAccess", 0) or 0 + last_access_ts = getattr(client, "lastAccess", 0) or 0 + + if now > expires_ts: + status = "Expired" + elif first_access_ts > 0: + status = "Opened" + else: + status = "Generated" + + result.append( + OneTimeShare( + record_uid=record_uid_str, + share_link_name=getattr(client, "id", "") or "", + share_link_id=utils.base64_url_encode(client.clientId), + generated=_ms_to_datetime(created_ts), + expires=_ms_to_datetime(expires_ts), + opened=_ms_to_datetime(first_access_ts) if first_access_ts else None, + accessed=_ms_to_datetime(last_access_ts) if last_access_ts else None, + status=status, + ) + ) + + +def _ms_to_datetime(ms: int) -> Optional[datetime.datetime]: + """Convert millisecond timestamp to datetime.""" + if not ms or ms <= 0: + return None + return datetime.datetime.fromtimestamp(ms / TIMESTAMP_MILLISECONDS_FACTOR) + + +def create_one_time_share( + vault: vault_online.VaultOnline, + record_uid: str, + expiration_period: datetime.timedelta, + name: Optional[str] = None, + is_editable: bool = False, + is_self_destruct: bool = False, +) -> str: + """ + Create a one-time share URL for a record. + + Args: + vault: Initialized VaultOnline instance. + record_uid: Record UID to share. + expiration_period: How long the share link is valid (e.g. timedelta(days=7)). + Cannot exceed 6 months. + name: Optional label for the share link. + is_editable: If True, the recipient can edit the shared record. + is_self_destruct: If True, the share is invalidated after first open. + + Returns: + The one-time share URL (string). The recipient opens this URL to access the record. + + Raises: + ValueError: If vault is not initialized, record is not found, or + expiration_period exceeds 6 months. + """ + if vault is None: + raise ValueError("Vault is not initialized.") + + if expiration_period.total_seconds() > SIX_MONTHS_IN_SECONDS: + raise ValueError( + "Expiration period cannot be greater than 6 months." + ) + + record_key = vault.vault_data.get_record_key(record_uid=record_uid) + if record_key is None: + raise ValueError(f"Record not found: {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 + ) + if name: + request.id = name + request.isSelfDestruct = is_self_destruct + request.isEditable = is_editable + + vault.keeper_auth.execute_auth_rest( + rest_endpoint=EXTERNAL_SHARE_ADD_URL, + request=request, + response_type=Device, + ) + + server = vault.keeper_auth.keeper_endpoint.server + url = urlunparse( + ( + "https", + server, + "/vault/share", + None, + None, + utils.base64_url_encode(client_key), + ) + ) + return url + + +def remove_one_time_share( + vault: vault_online.VaultOnline, + record_uid: str, + share_identifier: str, +) -> None: + """ + Remove a one-time share for a record. + + Args: + vault: Initialized VaultOnline instance. + record_uid: Record UID that has the one-time share. + share_identifier: One-time share name (client.id), full share link ID + (base64-encoded client ID), or a unique prefix of the share link ID. + + Raises: + ValueError: If vault is not initialized, no one-time shares exist for + the record, no share matches the identifier, or multiple shares + match a partial identifier. + """ + if vault is None: + raise ValueError("Vault is not initialized.") + + app_infos = ksm_management.get_app_info(vault=vault, app_uid=record_uid) + if not app_infos: + raise ValueError( + f"There are no one-time shares for record {record_uid!r}." + ) + + client_id = _find_client_id(app_infos, share_identifier) + if client_id is None: + raise ValueError( + f'No one-time share found matching {share_identifier!r} for record {record_uid!r}.' + ) + + request = RemoveAppClientsRequest() + request.appRecordUid = utils.base64_url_decode(record_uid) + request.clients.append(client_id) + + vault.keeper_auth.execute_auth_rest( + request=request, + rest_endpoint=REMOVE_EXTERNAL_SHARE_URL, + ) + + +def _find_client_id(app_infos, share_identifier: str) -> Optional[bytes]: + """ + Resolve share name or ID to a single client ID (bytes). + + Matches by exact share name (client.id), exact base64 clientId, or + unique prefix of base64 clientId. + """ + cleaned = ( + share_identifier[: -len(TRUNCATE_SUFFIX)] + if share_identifier.endswith(TRUNCATE_SUFFIX) + else share_identifier + ) + cleaned_lower = cleaned.lower() + partial_matches: List[bytes] = [] + + for app_info in app_infos: + if not getattr(app_info, "isExternalShare", False): + continue + for client in getattr(app_info, "clients", []): + if (getattr(client, "id", "") or "").lower() == cleaned_lower: + return client.clientId + encoded = utils.base64_url_encode(client.clientId) + if encoded == cleaned: + return client.clientId + if encoded.startswith(cleaned): + partial_matches.append(client.clientId) + + if not partial_matches: + return None + if len(partial_matches) == 1: + return partial_matches[0] + raise ValueError( + f'Multiple one-time shares match {share_identifier!r}. Use a more specific identifier.' + ) + diff --git a/keepersdk-package/src/keepersdk/vault/share_management_utils.py b/keepersdk-package/src/keepersdk/vault/share_management_utils.py index 5b106907..f30ccbf3 100644 --- a/keepersdk-package/src/keepersdk/vault/share_management_utils.py +++ b/keepersdk-package/src/keepersdk/vault/share_management_utils.py @@ -5,7 +5,7 @@ from typing import Optional, Dict, List, Any, Generator, Iterable, Set, Tuple, Union from .. import crypto, utils -from ..proto import enterprise_pb2, record_pb2 +from ..proto import enterprise_pb2, folder_pb2, record_pb2 from ..vault import vault_online, vault_record, vault_types, vault_utils from ..enterprise import enterprise_data @@ -956,4 +956,412 @@ def parse_timeout(timeout_input: str) -> datetime.timedelta: return datetime.timedelta(**{TIMEOUT_DEFAULT_UNIT: int(timeout_input)}) tdelta_kwargs = _parse_timeout_units(timeout_input) - return datetime.timedelta(**tdelta_kwargs) \ No newline at end of file + return datetime.timedelta(**tdelta_kwargs) + + +_SHARED_FOLDER_TYPES: Tuple[str, ...] = ('shared_folder', 'shared_folder_folder') +_AM_I_SHARE_ADMIN_ENDPOINT = 'vault/am_i_share_admin' +_RECORDS_SHARE_UPDATE_ENDPOINT = 'vault/records_share_update' +_SHARED_FOLDER_UPDATE_V3_ENDPOINT = 'vault/shared_folder_update_v3' +_DIRECT_SHARE_BATCH_SIZE = 900 +_SHARED_FOLDER_RECORD_BATCH_SIZE = 490 +_SHARED_FOLDER_CHUNK_ELEMENT_LIMIT = 500 + + +def _resolve_folder_for_permission( + vault: vault_online.VaultOnline, + folder_uid_or_path: Optional[str] +) -> vault_types.Folder: + """Resolve folder from UID or path. Returns root folder if folder_uid_or_path is None or empty.""" + if not folder_uid_or_path or not folder_uid_or_path.strip(): + return vault.vault_data.root_folder + name = folder_uid_or_path.strip() + if name in vault.vault_data._folders: + folder = vault.vault_data.get_folder(name) + if folder: + return folder + folder, remaining = try_resolve_path(vault, name) + if remaining: + raise ShareNotFoundError(f'Folder "{folder_uid_or_path}" not found') + return folder + + +def _get_folders_to_process( + vault: vault_online.VaultOnline, + start_folder: vault_types.Folder, + recursive: bool +) -> List[vault_types.Folder]: + """Return list of folders to process, optionally including all subfolders.""" + folders = [start_folder] + if not recursive: + return folders + visited: Set[str] = {start_folder.folder_uid} + pos = 0 + while pos < len(folders): + folder = folders[pos] + if folder.subfolders: + for sub_uid in folder.subfolders: + if sub_uid not in visited: + sub = vault.vault_data.get_folder(sub_uid) + if sub: + folders.append(sub) + visited.add(sub_uid) + pos += 1 + return folders + + +def _get_share_admin_folders( + vault: vault_online.VaultOnline, + folders: List[vault_types.Folder] +) -> Set[str]: + """Return set of shared folder UIDs where the current user is share admin.""" + shared_folder_uids: Set[str] = set() + for folder in folders: + uid = None + if folder.folder_type == 'shared_folder': + uid = folder.folder_uid + elif folder.folder_type == 'shared_folder_folder': + uid = folder.folder_scope_uid + if uid and uid not in shared_folder_uids and uid in vault.vault_data._shared_folders: + shared_folder_uids.add(uid) + if not shared_folder_uids: + return set() + try: + rq = record_pb2.AmIShareAdmin() + for sf_uid in shared_folder_uids: + osa = record_pb2.IsObjectShareAdmin() + osa.uid = utils.base64_url_decode(sf_uid) + osa.objectType = record_pb2.CHECK_SA_ON_SF + rq.isObjectShareAdmin.append(osa) + rs = vault.keeper_auth.execute_auth_rest( + rest_endpoint=_AM_I_SHARE_ADMIN_ENDPOINT, + request=rq, + response_type=record_pb2.AmIShareAdmin + ) + return {utils.base64_url_encode(osa.uid) for osa in rs.isObjectShareAdmin if osa.isAdmin} + except Exception: + return set() + + +def _get_shared_folder_uid(folder: vault_types.Folder) -> Optional[str]: + """Get shared folder UID from a folder (for shared_folder or shared_folder_folder).""" + if folder.folder_type == 'shared_folder': + return folder.folder_uid + if folder.folder_type == 'shared_folder_folder': + return folder.folder_scope_uid + return None + + +def _has_manage_records_permission( + vault: vault_online.VaultOnline, + shared_folder: vault_types.SharedFolder, + shared_folder_uid: str, + is_share_admin: bool +) -> bool: + """Return True if current user can manage records in this shared folder.""" + if is_share_admin: + return True + account_uid = utils.base64_url_encode(vault.keeper_auth.auth_context.account_uid) + username = vault.keeper_auth.auth_context.username + if shared_folder.user_permissions: + if shared_folder.user_permissions[0].user_uid == account_uid: + return True + user = next( + (u for u in shared_folder.user_permissions if u.name == username), + None + ) + if user and user.manage_records: + return True + return False + + +def _needs_shared_folder_record_update( + rp: vault_types.SharedFolderRecord, + should_have: bool, + change_edit: bool, + change_share: bool +) -> bool: + """Return True if this shared folder record permission should be updated.""" + if change_edit and (should_have != rp.can_edit): + return True + if change_share and (should_have != rp.can_share): + return True + return False + + +def _build_shared_folder_record_update( + record_uid: str, + shared_folder_uid: str, + should_have: bool, + change_edit: bool, + change_share: bool +) -> Any: + """Build SharedFolderUpdateRecord protobuf for one record in a shared folder.""" + 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 should_have else folder_pb2.BOOLEAN_FALSE + ) if change_edit else folder_pb2.BOOLEAN_NO_CHANGE + cmd.canShare = ( + folder_pb2.BOOLEAN_TRUE if should_have else folder_pb2.BOOLEAN_FALSE + ) if change_share else folder_pb2.BOOLEAN_NO_CHANGE + return cmd + + +def _process_direct_share_updates( + vault: vault_online.VaultOnline, + folders: List[vault_types.Folder], + should_have: bool, + change_edit: bool, + change_share: bool +) -> List[Dict[str, Any]]: + """Collect direct record-share permission updates (record shared to users).""" + record_uids: Set[str] = set() + for folder in folders: + if folder.records: + record_uids.update(folder.records) + if not record_uids: + return [] + shared_records = get_record_shares(vault, list(record_uids)) + if not shared_records: + return [] + current_username = vault.keeper_auth.auth_context.username + updates: List[Dict[str, Any]] = [] + for sr in shared_records: + shares = sr.get('shares', {}) + user_permissions = shares.get('user_permissions', []) + for up in user_permissions: + if up.get('owner'): + continue + username = up.get('username') + if username == current_username: + continue + needs = (change_edit and (should_have != up.get('editable'))) or ( + change_share and (should_have != up.get('shareable')) + ) + if needs: + updates.append({ + 'record_uid': sr.get('record_uid'), + 'to_username': username, + 'editable': should_have if change_edit else up.get('editable'), + 'shareable': should_have if change_share else up.get('shareable'), + }) + return updates + + +def _process_shared_folder_permission_updates( + vault: vault_online.VaultOnline, + folders: List[vault_types.Folder], + should_have: bool, + change_edit: bool, + change_share: bool +) -> Tuple[Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]: + """Collect shared-folder record permission updates and skipped (no permission).""" + share_admin = _get_share_admin_folders(vault, folders) + account_uid = utils.base64_url_encode(vault.keeper_auth.auth_context.account_uid) + updates: Dict[str, Dict[str, Any]] = {} + skipped: Dict[str, Dict[str, Any]] = {} + for folder in folders: + if folder.folder_type not in _SHARED_FOLDER_TYPES: + continue + shared_folder_uid = _get_shared_folder_uid(folder) + if not shared_folder_uid or shared_folder_uid not in vault.vault_data._shared_folders: + continue + is_share_admin = shared_folder_uid in share_admin + shared_folder = vault.vault_data.load_shared_folder(shared_folder_uid) + if not shared_folder: + continue + has_manage = _has_manage_records_permission( + vault, shared_folder, shared_folder_uid, is_share_admin + ) + container = updates if (is_share_admin or has_manage) else skipped + if not shared_folder.record_permissions: + continue + record_uids = folder.records if folder.records else set() + for rp in shared_folder.record_permissions: + record_uid = rp.record_uid + if record_uid not in record_uids: + continue + if record_uid in container.get(shared_folder_uid, {}): + continue + if _needs_shared_folder_record_update(rp, should_have, change_edit, change_share): + container.setdefault(shared_folder_uid, {}) + container[shared_folder_uid][record_uid] = _build_shared_folder_record_update( + record_uid, shared_folder_uid, should_have, change_edit, change_share + ) + # drop empty dicts + updates = {k: v for k, v in updates.items() if v} + skipped = {k: v for k, v in skipped.items() if v} + return updates, skipped + + +def _to_shared_record_proto(item: Dict[str, Any]) -> Any: + """Build SharedRecord protobuf for records_share_update.""" + sr = record_pb2.SharedRecord() + sr.toUsername = item['to_username'] + sr.recordUid = utils.base64_url_decode(item['record_uid']) + if 'editable' in item: + sr.editable = item['editable'] + if 'shareable' in item: + sr.shareable = item['shareable'] + return sr + + +def _execute_direct_share_updates( + vault: vault_online.VaultOnline, + updates: List[Dict[str, Any]] +) -> List[List[Any]]: + """Apply direct record share permission updates. Returns list of error rows [record_uid, username, status, message].""" + errors: List[List[Any]] = [] + while updates: + batch = updates[:_DIRECT_SHARE_BATCH_SIZE] + updates = updates[_DIRECT_SHARE_BATCH_SIZE:] + rq = record_pb2.RecordShareUpdateRequest() + rq.updateSharedRecord.extend(_to_shared_record_proto(x) for x in batch) + rs = vault.keeper_auth.execute_auth_rest( + rest_endpoint=_RECORDS_SHARE_UPDATE_ENDPOINT, + request=rq, + response_type=record_pb2.RecordShareUpdateResponse + ) + for status in rs.updateSharedRecordStatus: + if status.status.lower() != 'success': + errors.append([ + utils.base64_url_encode(status.recordUid), + status.username, + status.status.lower(), + status.message + ]) + return errors + + +def _execute_shared_folder_updates( + vault: vault_online.VaultOnline, + updates: Dict[str, Dict[str, Any]] +) -> List[List[Any]]: + """Apply shared folder record permission updates. Returns list of error rows [shared_folder_uid, record_uid, status].""" + errors: List[List[Any]] = [] + requests: List[Any] = [] + for shared_folder_uid in updates: + commands = list(updates[shared_folder_uid].values()) + while commands: + batch = commands[:_SHARED_FOLDER_RECORD_BATCH_SIZE] + commands = commands[_SHARED_FOLDER_RECORD_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) + # Chunk for API size limits + chunks: List[Dict[bytes, Any]] = [] + current: Dict[bytes, Any] = {} + total = 0 + for rq in requests: + if rq.sharedFolderUid in current: + chunks.append(current) + current = {} + total = 0 + n = len(rq.sharedFolderUpdateRecord) + if total + n > _SHARED_FOLDER_CHUNK_ELEMENT_LIMIT: + chunks.append(current) + current = {} + total = 0 + current[rq.sharedFolderUid] = rq + total += n + if current: + chunks.append(current) + for chunk in chunks: + rqs = folder_pb2.SharedFolderUpdateV3RequestV2() + rqs.sharedFoldersUpdateV3.extend(chunk.values()) + rss = vault.keeper_auth.execute_auth_rest( + rest_endpoint=_SHARED_FOLDER_UPDATE_V3_ENDPOINT, + request=rqs, + response_type=folder_pb2.SharedFolderUpdateV3ResponseV2, + payload_version=1 + ) + for rs in rss.sharedFoldersUpdateV3Response: + sf_uid = utils.base64_url_encode(rs.sharedFolderUid) + for status in rs.sharedFolderUpdateRecordStatus: + if status.status != 'success': + errors.append([sf_uid, utils.base64_url_encode(status.recordUid), status.status]) + return errors + + +def update_record_permissions( + vault: vault_online.VaultOnline, + action: str, + can_share: bool = False, + can_edit: bool = False, + *, + folder_uid_or_path: Optional[str] = None, + recursive: bool = False, + share_record: bool = True, + share_folder: bool = True, + dry_run: bool = False, + sync_after: bool = True +) -> Dict[str, Any]: + """Update record permissions (can_edit / can_share) in a folder and optionally its subfolders. + + Args: + vault: Connected vault. + action: ``'grant'`` or ``'revoke'``. + can_share: Whether to change the "can share" permission. + can_edit: Whether to change the "can edit" permission. + folder_uid_or_path: Folder UID or path; if None or empty, uses root. + recursive: If True, include all subfolders. + share_record: If True, update direct record shares (record shared to users). + share_folder: If True, update shared folder record permissions. + dry_run: If True, do not apply changes; only compute and return planned updates. + sync_after: If True and changes were applied, sync vault down after updates. + + Returns: + Dict with keys: + - ``direct_share_updates``: list of direct-share updates (each a dict with + record_uid, to_username, editable, shareable). + - ``shared_folder_updates``: dict shared_folder_uid -> { record_uid -> update_cmd }. + - ``direct_share_errors``: list of error rows for direct share API (if not dry_run). + - ``shared_folder_errors``: list of error rows for shared folder API (if not dry_run). + - ``skipped_shared_folders``: shared folder UIDs skipped due to insufficient permissions. + + Raises: + ShareValidationError: If neither can_share nor can_edit is True, or action is invalid. + ShareNotFoundError: If folder_uid_or_path is not found. + """ + if action not in ('grant', 'revoke'): + raise ShareValidationError(f'Invalid action: {action!r}; use "grant" or "revoke"') + if not can_share and not can_edit: + raise ShareValidationError('Specify at least one of can_share or can_edit') + should_have = action == 'grant' + folder = _resolve_folder_for_permission(vault, folder_uid_or_path) + folders = _get_folders_to_process(vault, folder, recursive) + direct_share_updates: List[Dict[str, Any]] = [] + shared_folder_updates: Dict[str, Dict[str, Any]] = {} + skipped_shared_folders: Dict[str, Dict[str, Any]] = {} + if share_record: + direct_share_updates = _process_direct_share_updates( + vault, folders, should_have, can_edit, can_share + ) + if share_folder: + shared_folder_updates, skipped_shared_folders = _process_shared_folder_permission_updates( + vault, folders, should_have, can_edit, can_share + ) + result: Dict[str, Any] = { + 'direct_share_updates': direct_share_updates, + 'shared_folder_updates': shared_folder_updates, + 'direct_share_errors': [], + 'shared_folder_errors': [], + 'skipped_shared_folders': skipped_shared_folders, + } + if dry_run: + return result + if direct_share_updates: + result['direct_share_errors'] = _execute_direct_share_updates(vault, direct_share_updates) + if shared_folder_updates: + result['shared_folder_errors'] = _execute_shared_folder_updates(vault, shared_folder_updates) + if sync_after and (direct_share_updates or shared_folder_updates): + vault.sync_down(True) + return result + + From 2f1fc33a5fd20c0101a4ddddbff3b23fc4e17939 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Mon, 23 Mar 2026 14:27:27 +0530 Subject: [PATCH 08/23] share-report and security-audit-report bugfix --- .../commands/security_audit_report.py | 11 ++ .../src/keepercli/commands/share_report.py | 14 +- .../src/keepersdk/vault/share_report.py | 158 ++++++++++++++---- 3 files changed, 151 insertions(+), 32 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/security_audit_report.py b/keepercli-package/src/keepercli/commands/security_audit_report.py index 4e5efeff..dbba2cfe 100644 --- a/keepercli-package/src/keepercli/commands/security_audit_report.py +++ b/keepercli-package/src/keepercli/commands/security_audit_report.py @@ -164,11 +164,22 @@ def _resolve_node_ids(self, enterprise_data, nodes: Optional[List[str]]) -> List return [] node_ids = [] + unresolved = [] for name_or_id in nodes: + matched = False for n in enterprise_data.nodes.get_all_entities(): if name_or_id == str(n.node_id) or name_or_id == n.name: node_ids.append(n.node_id) + matched = True break + if not matched: + unresolved.append(name_or_id) + + if unresolved: + raise base.CommandError( + f'Invalid node(s): {", ".join(repr(x) for x in unresolved)}. ' + 'Provide a valid node name or node UID.' + ) return node_ids def _format_report( diff --git a/keepercli-package/src/keepercli/commands/share_report.py b/keepercli-package/src/keepercli/commands/share_report.py index 9aafc6e1..e38db783 100644 --- a/keepercli-package/src/keepercli/commands/share_report.py +++ b/keepercli-package/src/keepercli/commands/share_report.py @@ -126,7 +126,10 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: if config.folders_only: return self._generate_folders_report(generator, output_format, output_file) if config.show_ownership: - return self._generate_ownership_report(generator, output_format, output_file, verbose) + return self._generate_ownership_report( + generator, output_format, output_file, verbose, + show_share_date=config.show_share_date + ) if config.record_filter: return self._generate_record_detail_report(generator, config) if config.user_filter: @@ -158,15 +161,18 @@ def _generate_ownership_report( generator: share_report.ShareReportGenerator, output_format: str, output_file: Optional[str], - verbose: bool + verbose: bool, + show_share_date: bool = False ) -> Optional[str]: """Generate record ownership report.""" entries = generator.generate_records_report() - headers = share_report.ShareReportGenerator.get_headers(ownership=True) + headers = share_report.ShareReportGenerator.get_headers( + ownership=True, show_share_date=show_share_date + ) table = [ [e.record_owner, e.record_uid, e.record_title, e.shared_with if verbose else e.shared_with_count, - '\n'.join(e.folder_paths)] + '\n'.join(e.folder_paths)] + ([e.share_date or ''] if show_share_date else []) for e in entries ] diff --git a/keepersdk-package/src/keepersdk/vault/share_report.py b/keepersdk-package/src/keepersdk/vault/share_report.py index 66067013..782566f9 100644 --- a/keepersdk-package/src/keepersdk/vault/share_report.py +++ b/keepersdk-package/src/keepersdk/vault/share_report.py @@ -16,6 +16,7 @@ import dataclasses import datetime +import logging from typing import Optional, List, Dict, Any, Iterable, Set, NamedTuple from . import vault_online, vault_types, vault_utils @@ -23,6 +24,11 @@ from ..authentication import keeper_auth from ..enterprise import enterprise_data as enterprise_data_types +_SHARE_DATE_EVENT_TYPES = ['folder_add_record', 'record_add'] +_AUDIT_EVENT_LIMIT = 1000 +_SHARE_DATE_PAGINATION_MAX = 100 +_logger = logging.getLogger(__name__) + @dataclasses.dataclass class ShareReportEntry: @@ -217,21 +223,35 @@ def generate_records_report(self) -> List[ShareReportEntry]: share_info_map = self._fetch_share_info(list(record_uids)) or {} entries: List[ShareReportEntry] = [] processed_uids: Set[str] = set() - - user_filter_lower = {u.lower() for u in self._config.user_filter} if self._config.user_filter else None - + + user_filter_lower = ( + {u.lower() for u in self._config.user_filter} if self._config.user_filter else None + ) + share_date_map: Dict[str, str] = {} + if self._config.show_share_date and self._auth and self._enterprise: + sf_records = ( + self._get_shared_folder_records_for_user(user_filter_lower) + if user_filter_lower is not None + else self._get_all_shared_folder_records() + ) + all_uids = set(share_info_map.keys()) | sf_records + if all_uids: + share_date_map = self._fetch_share_dates(list(all_uids)) + for uid, share_info in share_info_map.items(): if not self._should_include_record(share_info): continue - + if user_filter_lower and not self._record_matches_user_filter(share_info, user_filter_lower): continue - - entries.append(self._build_share_entry(share_info)) + + entries.append(self._build_share_entry(share_info, share_date_map.get(uid))) processed_uids.add(uid) - - self._add_shared_folder_records(entries, processed_uids, share_info_map, user_filter_lower) - + + self._add_shared_folder_records( + entries, processed_uids, share_info_map, user_filter_lower, share_date_map + ) + return entries def _should_include_record(self, share_info: RecordShareInfo) -> bool: @@ -252,42 +272,45 @@ def _add_shared_folder_records( entries: List[ShareReportEntry], processed_uids: Set[str], share_info_map: Dict[str, RecordShareInfo], - user_filter_lower: Optional[Set[str]] + user_filter_lower: Optional[Set[str]], + share_date_map: Optional[Dict[str, str]] = None ) -> None: """Add records from shared folders that weren't returned by the share API.""" should_include = ( - self._config.user_filter or - self._config.show_ownership or + self._config.user_filter or + self._config.show_ownership or not self._config.record_filter ) - + if not should_include: return - + sf_records = ( self._get_shared_folder_records_for_user(user_filter_lower) if user_filter_lower else self._get_all_shared_folder_records() ) - + share_dates = share_date_map or {} + for record_uid in sf_records: if record_uid in processed_uids: continue - + record_info = self._vault.vault_data.get_record(record_uid) if not record_info: continue - + folder_paths = self._get_folder_paths(record_uid) owner = self._get_owner_from_share_info(share_info_map, record_uid) - + entries.append(ShareReportEntry( record_uid=record_uid, record_title=record_info.title, record_owner=owner, shared_with='', shared_with_count=0, - folder_paths=folder_paths + folder_paths=folder_paths, + share_date=share_dates.get(record_uid) )) processed_uids.add(record_uid) @@ -454,19 +477,27 @@ def generate_report_rows(self) -> Iterable[List[Any]]: elif self._config.show_ownership: for entry in self.generate_records_report(): shared_info = entry.shared_with if self._config.verbose else entry.shared_with_count - yield [entry.record_owner, entry.record_uid, entry.record_title, + row = [entry.record_owner, entry.record_uid, entry.record_title, shared_info, '\n'.join(entry.folder_paths)] + if self._config.show_share_date: + row.append(entry.share_date or '') + yield row else: for entry in self.generate_summary_report(): yield [entry.shared_to, entry.record_count, entry.shared_folder_count] @staticmethod - def get_headers(folders_only: bool = False, ownership: bool = False) -> List[str]: + def get_headers( + folders_only: bool = False, + ownership: bool = False, + show_share_date: bool = False + ) -> List[str]: """Get report headers based on configuration. Args: folders_only: True if generating shared folders report ownership: True if generating ownership report + show_share_date: True to include share date column (ownership report only) Returns: List of header column names @@ -474,7 +505,10 @@ def get_headers(folders_only: bool = False, ownership: bool = False) -> List[str if folders_only: return ['folder_uid', 'folder_name', 'shared_to', 'permissions', 'folder_path'] if ownership: - return ['record_owner', 'record_uid', 'record_title', 'shared_with', 'folder_path'] + headers = ['record_owner', 'record_uid', 'record_title', 'shared_with', 'folder_path'] + if show_share_date: + headers.append('share_date') + return headers return ['shared_to', 'records', 'shared_folders'] def _resolve_record_uids(self, record_refs: List[str]) -> Set[str]: @@ -504,7 +538,9 @@ def _fetch_share_info(self, record_uids: List[str]) -> Dict[str, RecordShareInfo try: shares_data = share_management_utils.get_record_shares( - self._vault, record_uids, is_share_admin=False + self._vault, + record_uids, + is_share_admin=self._config.show_share_date, ) if not shares_data: @@ -533,7 +569,70 @@ def _fetch_share_info(self, record_uids: List[str]) -> Dict[str, RecordShareInfo pass return result - + + def _fetch_share_dates(self, record_uids: List[str]) -> Dict[str, str]: + """Fetch earliest share-related audit event date per record (enterprise only). + Returns dict of record_uid -> formatted date string, or empty if not available. + """ + if not record_uids or not self._auth or not self._enterprise: + return {} + record_uid_set = set(record_uids) + min_ts: Dict[str, int] = {} + search_min_ts = int( + (datetime.datetime.now() - datetime.timedelta(days=365 * 5)).timestamp() + ) + audit_filter: Dict[str, Any] = { + 'audit_event_type': _SHARE_DATE_EVENT_TYPES, + 'created': {'min': search_min_ts}, + 'record_uid': record_uids, + } + rq: Dict[str, Any] = { + 'command': 'get_audit_event_reports', + 'scope': 'enterprise', + 'report_type': 'raw', + 'filter': audit_filter, + 'limit': _AUDIT_EVENT_LIMIT, + 'order': 'ascending', + } + iterations = 0 + try: + while iterations < _SHARE_DATE_PAGINATION_MAX: + iterations += 1 + rs = self._auth.execute_auth_command(rq) + events = rs.get('audit_event_overview_report_rows') or [] + if not events: + break + for event in events: + uid = event.get('record_uid') or '' + if uid not in record_uid_set: + continue + ts = event.get('created') + if ts is None: + continue + try: + ts_int = int(ts) + except (TypeError, ValueError): + continue + if uid not in min_ts or ts_int < min_ts[uid]: + min_ts[uid] = ts_int + if len(events) < _AUDIT_EVENT_LIMIT: + break + last_ts = max(int(e.get('created', 0)) for e in events) + audit_filter['created'] = {'min': last_ts + 1} + except Exception as e: + _logger.debug('Failed to fetch share dates from audit: %s', e) + # Format as date string (created may be Unix seconds or milliseconds) + result: Dict[str, str] = {} + for uid, ts in min_ts.items(): + try: + if ts > 1e12: + ts = ts // 1000 + dt = datetime.datetime.fromtimestamp(ts, tz=datetime.timezone.utc) + result[uid] = dt.strftime('%Y-%m-%d %H:%M UTC') + except (OSError, ValueError): + result[uid] = str(ts) + return result + def _parse_user_permissions(self, shares: Dict) -> List[UserPermissionInfo]: """Parse user permissions from share data.""" permissions = [] @@ -554,22 +653,25 @@ def _parse_user_permissions(self, shares: Dict) -> List[UserPermissionInfo]: )) return permissions - def _build_share_entry(self, share_info: RecordShareInfo) -> ShareReportEntry: + def _build_share_entry( + self, share_info: RecordShareInfo, share_date: Optional[str] = None + ) -> ShareReportEntry: """Build a ShareReportEntry from RecordShareInfo.""" owner = self._get_owner_from_share_info({share_info.record_uid: share_info}, share_info.record_uid) non_owner_shares = [p for p in share_info.user_permissions if not p.is_owner] - + shared_with = '' if self._config.verbose: shared_with = self._format_verbose_permissions(share_info) - + return ShareReportEntry( record_uid=share_info.record_uid, record_title=share_info.record_title, record_owner=owner, shared_with=shared_with, shared_with_count=len(non_owner_shares), - folder_paths=share_info.folder_paths + folder_paths=share_info.folder_paths, + share_date=share_date ) def _format_verbose_permissions(self, share_info: RecordShareInfo) -> str: From 6def342c6e70b946ef858db86aef6502c465be81 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 2 Apr 2026 15:11:36 +0530 Subject: [PATCH 09/23] Skip sync functions implemented for shared folder sharing with teams and users --- .../revoke_shared_folder_team_skip_sync.py | 525 +++++++++++++ .../revoke_shared_folder_user_skip_sync.py | 525 +++++++++++++ .../share_shared_folder_team_skip_sync.py | 538 ++++++++++++++ .../share_shared_folder_user_skip_sync.py | 536 +++++++++++++ .../src/keepersdk/vault/skip_sync.py | 703 ++++++++++++++++++ 5 files changed, 2827 insertions(+) create mode 100644 examples/sdk_examples/sharing_commands/revoke_shared_folder_team_skip_sync.py create mode 100644 examples/sdk_examples/sharing_commands/revoke_shared_folder_user_skip_sync.py create mode 100644 examples/sdk_examples/sharing_commands/share_shared_folder_team_skip_sync.py create mode 100644 examples/sdk_examples/sharing_commands/share_shared_folder_user_skip_sync.py create mode 100644 keepersdk-package/src/keepersdk/vault/skip_sync.py diff --git a/examples/sdk_examples/sharing_commands/revoke_shared_folder_team_skip_sync.py b/examples/sdk_examples/sharing_commands/revoke_shared_folder_team_skip_sync.py new file mode 100644 index 00000000..371d7836 --- /dev/null +++ b/examples/sdk_examples/sharing_commands/revoke_shared_folder_team_skip_sync.py @@ -0,0 +1,525 @@ +""" +Example: Revoke a shared folder from a team WITHOUT syncing the full vault down. + +This uses the skip-sync helpers in keepersdk.vault.skip_sync. +""" + +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.vault import skip_sync +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +def main() -> None: + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to revoke share.") + return + + # Fill these values before running: + shared_folder_uid = "" + # team can be either team UID (base64url) or team name; skip_sync will resolve it + team_name_or_uid = "" + + skip_sync.revoke_shared_folder_from_team( + keeper_auth_context, + shared_folder_uid=shared_folder_uid, + team_name_or_uid=team_name_or_uid, + ) + print(f'Revoked shared folder "{shared_folder_uid}" from team "{team_name_or_uid}" via skip-sync.') + + keeper_auth_context.close() + + +if __name__ == "__main__": + main() + diff --git a/examples/sdk_examples/sharing_commands/revoke_shared_folder_user_skip_sync.py b/examples/sdk_examples/sharing_commands/revoke_shared_folder_user_skip_sync.py new file mode 100644 index 00000000..a1c834c1 --- /dev/null +++ b/examples/sdk_examples/sharing_commands/revoke_shared_folder_user_skip_sync.py @@ -0,0 +1,525 @@ +""" +Example: Revoke a shared folder from a user WITHOUT syncing the full vault down. + +This uses the skip-sync helpers in keepersdk.vault.skip_sync. +""" + +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.vault import skip_sync +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def main() -> None: + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to revoke share.") + return + + # Fill these values before running: + shared_folder_uid = "" + username = "" + + skip_sync.revoke_shared_folder_from_user( + keeper_auth_context, + shared_folder_uid=shared_folder_uid, + username=username, + ) + print(f'Revoked shared folder "{shared_folder_uid}" from user "{username}" via skip-sync.') + + keeper_auth_context.close() + + +if __name__ == "__main__": + main() + diff --git a/examples/sdk_examples/sharing_commands/share_shared_folder_team_skip_sync.py b/examples/sdk_examples/sharing_commands/share_shared_folder_team_skip_sync.py new file mode 100644 index 00000000..3c96ed47 --- /dev/null +++ b/examples/sdk_examples/sharing_commands/share_shared_folder_team_skip_sync.py @@ -0,0 +1,538 @@ +""" +Example: Share a shared folder to a team WITHOUT syncing the full vault down. + +This uses the skip-sync helpers in keepersdk.vault.skip_sync. +""" + +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.vault import skip_sync +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def main() -> None: + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to share folder.") + return + + # Fill these values before running: + shared_folder_uid = "" + # team can be either team UID (base64url) or team name; skip_sync will resolve it + team_name_or_uid = "" + + # Optional share options + manage_users = False # allow this team to manage other users in the shared folder + manage_records = False # allow this team to manage records in the shared folder + expiration = None # Unix timestamp seconds, or None for no expiration + #Note: Default shared folder permissions will take effect if not specified + + records = skip_sync.share_shared_folder_to_team( + keeper_auth_context, + shared_folder_uid=shared_folder_uid, + team_name_or_uid=team_name_or_uid, + manage_users=manage_users, + manage_records=manage_records, + expiration=expiration, + ) + print(f'Shared folder "{shared_folder_uid}" with team "{team_name_or_uid}" via skip-sync.') + print("Records in folder (decrypted title):") + for row in records: + print(f" {row.record_uid}\t{row.name!r}") + + keeper_auth_context.close() + + +if __name__ == "__main__": + main() + diff --git a/examples/sdk_examples/sharing_commands/share_shared_folder_user_skip_sync.py b/examples/sdk_examples/sharing_commands/share_shared_folder_user_skip_sync.py new file mode 100644 index 00000000..a611bc52 --- /dev/null +++ b/examples/sdk_examples/sharing_commands/share_shared_folder_user_skip_sync.py @@ -0,0 +1,536 @@ +""" +Example: Share a shared folder to a user WITHOUT syncing the full vault down. + +This uses the skip-sync helpers in keepersdk.vault.skip_sync. +""" + +import getpass +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.vault import skip_sync +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def main() -> None: + keeper_auth_context, _ = login() + if not keeper_auth_context: + print("Login failed. Unable to share folder.") + return + + # Fill these values before running: + shared_folder_uid = "" + username = "" + + # Optional share options + manage_users = True # allow this user to manage other users in the shared folder + manage_records = True # allow this user to manage records in the shared folder + expiration = None # Unix timestamp seconds, or None for no expiration + + records = skip_sync.share_shared_folder_to_user( + keeper_auth_context, + shared_folder_uid=shared_folder_uid, + username=username, + manage_users=manage_users, + manage_records=manage_records, + expiration=expiration, + ) + print(f'Shared folder "{shared_folder_uid}" with user "{username}" via skip-sync.') + print("Records in folder (decrypted title):") + for row in records: + print(f" {row.record_uid}\t{row.name!r}") + + keeper_auth_context.close() + + +if __name__ == "__main__": + main() + diff --git a/keepersdk-package/src/keepersdk/vault/skip_sync.py b/keepersdk-package/src/keepersdk/vault/skip_sync.py new file mode 100644 index 00000000..c5869961 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/skip_sync.py @@ -0,0 +1,703 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +from .. import crypto, utils +from ..authentication import keeper_auth +from ..proto import folder_pb2, record_pb2 +from . import sync_down, vault_types, vault_utils + + +_GET_SHARED_FOLDERS_COMMAND = "get_shared_folders" +_SHARED_FOLDER_UPDATE_V3_ENDPOINT = "vault/shared_folder_update_v3" +_RECORD_DETAILS_URL = "vault/get_records_details" +_RECORD_DETAILS_CHUNK = 999 +_MAX_V2_RECORD_VERSION = 2 + + +def _coerce_shared_folder_header_key_type(raw) -> Optional[int]: + """ + Parse get_shared_folders ``key_type`` into an int compatible with + ``record_pb2.RecordKeyType`` / ``sync_down.decrypt_keeper_key``. + + The protobuf Python ``RecordKeyType`` wrapper is not callable (do not use + ``RecordKeyType(n)``); values are plain ints. The API may return an int or + a string (digits or protobuf-style enum name in snake_case). + """ + if raw is None: + return None + if isinstance(raw, bool): + return None + if isinstance(raw, int): + return raw + if isinstance(raw, str): + s = raw.strip() + if not s: + return None + if s.isdigit() or (s.startswith("-") and s[1:].isdigit()): + return int(s) + try: + return record_pb2.RecordKeyType.Value(s.upper()) + except ValueError: + return None + return None + + +@dataclass(frozen=True) +class SharedFolderRecordDisplay: + """Decrypted display row for a record in a shared folder (skip-sync path).""" + + record_uid: str + name: str + + +def _ensure_auth(auth: Optional[keeper_auth.KeeperAuth]) -> keeper_auth.KeeperAuth: + if auth is None: + raise ValueError("Authenticated KeeperAuth instance is required.") + return auth + + +def load_shared_folder_raw( + auth: keeper_auth.KeeperAuth, shared_folder_uid: str +) -> Optional[Dict]: + auth = _ensure_auth(auth) + if not shared_folder_uid or not shared_folder_uid.strip(): + raise ValueError("shared_folder_uid is required.") + + rq = { + "command": _GET_SHARED_FOLDERS_COMMAND, + "shared_folders": [{"shared_folder_uid": shared_folder_uid}], + "include": ["sfheaders", "sfusers", "sfrecords", "sfteams"], + } + rs = auth.execute_auth_command(rq, throw_on_error=False) + if not rs or rs.get("result") != "success": + return None + + folders = rs.get("shared_folders") or [] + if not folders: + return None + + uid = shared_folder_uid.strip() + for sf in folders: + if isinstance(sf, dict) and sf.get("shared_folder_uid", "").strip() == uid: + return sf + return folders[0] + + +def _decrypt_shared_folder_key( + auth: keeper_auth.KeeperAuth, sf: Dict +) -> Optional[bytes]: + """ + Decrypt shared folder AES key from a get_shared_folders row. + """ + if not sf: + return None + + key_type = _coerce_shared_folder_header_key_type(sf.get("key_type")) + shared_folder_key_b64 = sf.get("shared_folder_key") + + if key_type is None: + return None + + auth_context = auth.auth_context + + if shared_folder_key_b64: + try: + encrypted = utils.base64_url_decode(shared_folder_key_b64) + except Exception: + return None + try: + return sync_down.decrypt_keeper_key(auth_context, encrypted, key_type) + except Exception: + return None + + if key_type == record_pb2.NO_KEY: + try: + return sync_down.decrypt_keeper_key(auth_context, b"", key_type) + except Exception: + return None + + return None + + +def _decrypt_shared_folder_name(sf: Dict, sf_key: Optional[bytes]) -> str: + if not sf_key: + return "Shared Folder" + name_b64 = sf.get("name") + if not name_b64: + return "Shared Folder" + try: + encrypted = utils.base64_url_decode(name_b64) + if not encrypted: + return "Shared Folder" + decrypted = crypto.decrypt_aes_v1(encrypted, sf_key) + return decrypted.decode("utf-8") or "Shared Folder" + except Exception: + raise ValueError("Failed to decrypt shared folder name.") + + +def get_record_uids_from_shared_folder( + auth: keeper_auth.KeeperAuth, shared_folder_uid: str +) -> List[str]: + """ + Return distinct record UIDs from get_shared_folders -> records for a single + shared folder without syncing the vault. + """ + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + return [] + records = sf.get("records") or [] + uids: List[str] = [] + seen = set() + for r in records: + if not isinstance(r, dict): + continue + uid = r.get("record_uid") + if uid and uid not in seen: + seen.add(uid) + uids.append(uid) + return uids + + +def _unwrap_record_key_from_shared_folder_record( + encrypted_record_key_b64: str, shared_folder_key: bytes +) -> Optional[bytes]: + if not encrypted_record_key_b64 or not shared_folder_key: + return None + try: + encrypted = utils.base64_url_decode(encrypted_record_key_b64) + except Exception: + return None + if not encrypted: + return None + use_v2_first = len(encrypted) == 60 + for first_v2 in (use_v2_first, not use_v2_first): + try: + if first_v2: + return crypto.decrypt_aes_v2(encrypted, shared_folder_key) + return crypto.decrypt_aes_v1(encrypted, shared_folder_key) + except Exception: + continue + return None + + +def get_record_keys_from_shared_folder( + auth: keeper_auth.KeeperAuth, shared_folder_uid: str +) -> Dict[str, bytes]: + """ + Return a mapping record_uid -> decrypted AES record key for all records in + a shared folder, using get_shared_folders and without syncing the vault. + """ + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + return {} + + sf_key = _decrypt_shared_folder_key(auth, sf) + if not sf_key: + return {} + + records = sf.get("records") or [] + result: Dict[str, bytes] = {} + for r in records: + if not isinstance(r, dict): + continue + record_uid = r.get("record_uid") + record_key_b64 = r.get("record_key") + if not record_uid or not record_key_b64: + continue + try: + plain = _unwrap_record_key_from_shared_folder_record(record_key_b64, sf_key) + except Exception: + plain = None + if plain: + result[record_uid] = plain + return result + + +def _build_record_details_request(uids: List[str]) -> record_pb2.GetRecordDataWithAccessInfoRequest: + rq = record_pb2.GetRecordDataWithAccessInfoRequest() + rq.clientTime = utils.current_milli_time() + rq.recordDetailsInclude = record_pb2.DATA_PLUS_SHARE + for uid in uids: + try: + rq.recordUid.append(utils.base64_url_decode(uid)) + except Exception: + pass + return rq + + +def _process_record_owner_key_details( + record_data: record_pb2.RecordData, record_uid: str, record_keys: Dict[str, bytes] +) -> None: + if record_data.recordUid and record_data.recordKey: + owner_id = utils.base64_url_encode(record_data.recordUid) + if owner_id in record_keys: + try: + record_keys[record_uid] = crypto.decrypt_aes_v2( + record_data.recordKey, record_keys[owner_id] + ) + except Exception: + pass + + +def _resolve_record_key_for_details( + auth: keeper_auth.KeeperAuth, + record_data: record_pb2.RecordData, + record_uid: str, + record_keys: Dict[str, bytes], +) -> Optional[bytes]: + _process_record_owner_key_details(record_data, record_uid, record_keys) + if record_uid in record_keys: + k = record_keys[record_uid] + if k: + return k + try: + return sync_down.decrypt_keeper_key( + auth.auth_context, record_data.recordKey or b"", record_data.recordKeyType + ) + except Exception: + return None + + +def _decrypt_record_payload( + record_data: record_pb2.RecordData, record_key: bytes +) -> Optional[bytes]: + if not record_data.encryptedRecordData: + return None + try: + data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) + except Exception: + return None + version = record_data.version + try: + if version <= _MAX_V2_RECORD_VERSION: + return crypto.decrypt_aes_v1(data_decoded, record_key) + return crypto.decrypt_aes_v2(data_decoded, record_key) + except Exception: + return None + + +def _record_display_name_from_payload(data: bytes) -> str: + try: + d = json.loads(data.decode("utf-8")) + title = d.get("title") + return title if isinstance(title, str) else "" + except Exception: + return "" + + +def get_shared_folder_records_display( + auth: keeper_auth.KeeperAuth, shared_folder_uid: str +) -> List[SharedFolderRecordDisplay]: + """ + Analog of .NET RecordSkipSyncDown.GetSharedFolderRecordsAsync: load keys via + get_shared_folders, call vault/get_records_details, decrypt payloads, return + only record UID and decrypted title (name) for each row. + """ + auth = _ensure_auth(auth) + keys = get_record_keys_from_shared_folder(auth, shared_folder_uid) + if not keys: + return [] + + uid_list = list(keys.keys()) + working_keys = dict(keys) + out: List[SharedFolderRecordDisplay] = [] + + for i in range(0, len(uid_list), _RECORD_DETAILS_CHUNK): + chunk = uid_list[i : i + _RECORD_DETAILS_CHUNK] + rq = _build_record_details_request(chunk) + if len(rq.recordUid) == 0: + continue + rs = auth.execute_auth_rest( + _RECORD_DETAILS_URL, + rq, + response_type=record_pb2.GetRecordDataWithAccessInfoResponse, + ) + if not rs: + continue + for item in rs.recordDataWithAccessInfo: + if not item.recordUid: + continue + uid = utils.base64_url_encode(item.recordUid) + rd = item.recordData + if rd is None or not rd.encryptedRecordData: + continue + rk = _resolve_record_key_for_details(auth, rd, uid, working_keys) + if not rk: + continue + payload = _decrypt_record_payload(rd, rk) + if not payload: + continue + name = _record_display_name_from_payload(payload) + out.append(SharedFolderRecordDisplay(record_uid=uid, name=name)) + + return out + + +def get_available_teams_for_share( + auth: keeper_auth.KeeperAuth, +) -> List[vault_types.TeamInfo]: + """ + Return teams that the current user may share with, without loading the vault. + Thin wrapper over get_available_teams. + """ + return list(vault_utils.load_available_teams(auth)) + + +def _shared_folder_user_matches(user_row: Dict, user_id: str) -> bool: + if not user_row or not user_id: + return False + email = user_row.get("email") or user_row.get("username") or "" + return email.strip().casefold() == user_id.strip().casefold() + + +def _is_shared_folder_user_member(sf: Dict, user_id: str) -> bool: + for u in sf.get("users") or []: + if _shared_folder_user_matches(u, user_id): + return True + return False + + +def _find_shared_folder_team(sf: Dict, team_uid: str) -> Optional[Dict]: + if not team_uid: + return None + for t in sf.get("teams") or []: + if t.get("team_uid", "").strip() == team_uid.strip(): + return t + return None + + +def _has_no_share_option_changes( + manage_users: Optional[bool], manage_records: Optional[bool], expiration: Optional[int] +) -> bool: + return manage_users is None and manage_records is None and expiration is None + + +def _is_put_status_ok(status: str) -> bool: + if not status: + return False + s = status.lower() + return s in ("success", "duplicate") + + +def _is_remove_status_ok(status: str) -> bool: + if not status: + return False + s = status.lower() + return s in ("success", "not_member", "not_in_shared_folder") + + +def _encrypt_shared_folder_key_for_team( + auth: keeper_auth.KeeperAuth, team_uid: str, shared_folder_key: bytes +) -> Tuple[bytes, int]: + """Match ``shares_management.FolderShares._encrypt_shared_folder_key_for_team``.""" + ac = auth.auth_context + auth.load_team_keys([team_uid]) + keys = auth.get_team_keys(team_uid) + if keys is None: + raise ValueError(f'Cannot retrieve team "{team_uid}" keys for sharing.') + + if keys.aes: + if ac.forbid_rsa: + encrypted = crypto.encrypt_aes_v2(shared_folder_key, keys.aes) + return encrypted, folder_pb2.encrypted_by_data_key_gcm + encrypted = crypto.encrypt_aes_v1(shared_folder_key, keys.aes) + return encrypted, folder_pb2.encrypted_by_data_key + elif ac.forbid_rsa and keys.ec: + pub = crypto.load_ec_public_key(keys.ec) + encrypted = crypto.encrypt_ec(shared_folder_key, pub) + return encrypted, folder_pb2.encrypted_by_public_key_ecc + elif not ac.forbid_rsa and keys.rsa: + pub = crypto.load_rsa_public_key(keys.rsa) + encrypted = crypto.encrypt_rsa(shared_folder_key, pub) + return encrypted, folder_pb2.encrypted_by_public_key + + raise ValueError(f'Cannot retrieve team "{team_uid}" keys for sharing.') + + +def _encrypt_shared_folder_key_for_user( + auth: keeper_auth.KeeperAuth, username: str, shared_folder_key: bytes +) -> Tuple[bytes, int]: + ac = auth.auth_context + if username.strip().casefold() == ac.username.strip().casefold(): + encrypted = crypto.encrypt_aes_v1(shared_folder_key, ac.data_key) + return encrypted, folder_pb2.encrypted_by_data_key + + auth.load_user_public_keys([username], send_invites=False) + keys = auth.get_user_keys(username) + if keys is None: + raise ValueError(f'Cannot retrieve user "{username}" public key for sharing.') + + elif ac.forbid_rsa and keys.ec: + pub = crypto.load_ec_public_key(keys.ec) + encrypted = crypto.encrypt_ec(shared_folder_key, pub) + return encrypted, folder_pb2.encrypted_by_public_key_ecc + elif not ac.forbid_rsa and keys.rsa: + pub = crypto.load_rsa_public_key(keys.rsa) + encrypted = crypto.encrypt_rsa(shared_folder_key, pub) + return encrypted, folder_pb2.encrypted_by_public_key + + raise ValueError(f'Cannot retrieve user "{username}" public key for sharing.') + + +def _build_shared_folder_update_request( + auth: keeper_auth.KeeperAuth, shared_folder_uid: str, sf: Dict, sf_key: bytes +) -> Tuple[folder_pb2.SharedFolderUpdateV3Request, str]: + display_name = _decrypt_shared_folder_name(sf, sf_key) + rq = folder_pb2.SharedFolderUpdateV3Request() + rq.sharedFolderUid = utils.base64_url_decode(shared_folder_uid) + rq.encryptedSharedFolderName = crypto.encrypt_aes_v1(display_name.encode("utf-8"), sf_key) + rq.forceUpdate = True + return rq, display_name + + +def share_shared_folder_to_team( + auth: keeper_auth.KeeperAuth, + shared_folder_uid: str, + team_name_or_uid: str, + *, + manage_users: Optional[bool] = None, + manage_records: Optional[bool] = None, + expiration: Optional[int] = None, +) -> List[SharedFolderRecordDisplay]: + """ + Share a shared folder to a team without requiring a synced vault. + + Returns decrypted record UID and title (name) for each record in the folder. + """ + auth = _ensure_auth(auth) + if not shared_folder_uid: + raise ValueError("shared_folder_uid is required.") + if not team_name_or_uid: + raise ValueError("team_name_or_uid is required.") + + # Resolve team UID from name or UID using existing helper + team_uid = None + for t in vault_utils.load_available_teams(auth): + if team_name_or_uid.strip().casefold() in ( + t.team_uid.strip().casefold(), + t.name.strip().casefold(), + ): + team_uid = t.team_uid + break + if not team_uid: + raise ValueError(f'Team "{team_name_or_uid}" not found.') + + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + raise ValueError(f'Shared folder "{shared_folder_uid}" not found.') + + sf_key = _decrypt_shared_folder_key(auth, sf) + if not sf_key: + raise ValueError(f'Shared folder "{shared_folder_uid}" key could not be decrypted.') + + rq, display_name = _build_shared_folder_update_request(auth, shared_folder_uid, sf, sf_key) + + existing_team = _find_shared_folder_team(sf, team_uid) + team_is_member = existing_team is not None + + if _has_no_share_option_changes(manage_users, manage_records, expiration) and team_is_member: + return get_shared_folder_records_display(auth, shared_folder_uid) + + sfut = folder_pb2.SharedFolderUpdateTeam() + sfut.teamUid = utils.base64_url_decode(team_uid) + sfut.expiration = expiration or 0 + + if team_is_member: + sfut.manageUsers = manage_users if manage_users is not None else existing_team.get("manage_users", False) + sfut.manageRecords = manage_records if manage_records is not None else existing_team.get( + "manage_records", False + ) + rq.sharedFolderUpdateTeam.append(sfut) + else: + sfut.manageUsers = manage_users if manage_users is not None else sf.get("default_manage_users", False) + sfut.manageRecords = manage_records if manage_records is not None else sf.get("default_manage_records", False) + + encrypted_key, key_type = _encrypt_shared_folder_key_for_team(auth, team_uid, sf_key) + sfut.typedSharedFolderKey.encryptedKey = encrypted_key + sfut.typedSharedFolderKey.encryptedKeyType = key_type + rq.sharedFolderAddTeam.append(sfut) + + rs = auth.execute_auth_rest( + _SHARED_FOLDER_UPDATE_V3_ENDPOINT, rq, response_type=folder_pb2.SharedFolderUpdateV3Response + ) + assert rs is not None + + for arr in (rs.sharedFolderAddTeamStatus, rs.sharedFolderUpdateTeamStatus): + for st in arr: + if not _is_put_status_ok(st.status): + raise ValueError( + f'Put Team "{utils.base64_url_encode(st.teamUid)}" to Shared Folder "{display_name}" error: {st.status}' + ) + + return get_shared_folder_records_display(auth, shared_folder_uid) + + +def revoke_shared_folder_from_team( + auth: keeper_auth.KeeperAuth, + shared_folder_uid: str, + team_name_or_uid: str, +) -> None: + """ + Remove a team from a shared folder without requiring a synced vault. + """ + auth = _ensure_auth(auth) + if not shared_folder_uid: + raise ValueError("shared_folder_uid is required.") + if not team_name_or_uid: + raise ValueError("team_name_or_uid is required.") + + team_uid = None + for t in vault_utils.load_available_teams(auth): + if team_name_or_uid.strip().casefold() in ( + t.team_uid.strip().casefold(), + t.name.strip().casefold(), + ): + team_uid = t.team_uid + break + if not team_uid: + return + + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + return + + if sf.get("teams") and _find_shared_folder_team(sf, team_uid) is None: + return + + sf_key = _decrypt_shared_folder_key(auth, sf) + if not sf_key: + raise ValueError(f'Shared folder "{shared_folder_uid}" key could not be decrypted.') + + rq, display_name = _build_shared_folder_update_request(auth, shared_folder_uid, sf, sf_key) + rq.sharedFolderRemoveTeam.append(utils.base64_url_decode(team_uid)) + + rs = auth.execute_auth_rest( + _SHARED_FOLDER_UPDATE_V3_ENDPOINT, rq, response_type=folder_pb2.SharedFolderUpdateV3Response + ) + assert rs is not None + for st in rs.sharedFolderRemoveTeamStatus: + if not _is_remove_status_ok(st.status): + raise ValueError( + f'Remove Team "{utils.base64_url_encode(st.teamUid)}" from Shared Folder "{display_name}" error: {st.status}' + ) + + +def share_shared_folder_to_user( + auth: keeper_auth.KeeperAuth, + shared_folder_uid: str, + username: str, + *, + manage_users: Optional[bool] = None, + manage_records: Optional[bool] = None, + expiration: Optional[int] = None, +) -> List[SharedFolderRecordDisplay]: + """ + Share a shared folder to a user (email) without requiring a synced vault. + + Returns decrypted record UID and title (name) for each record in the folder. + """ + auth = _ensure_auth(auth) + if not shared_folder_uid: + raise ValueError("shared_folder_uid is required.") + if not username: + raise ValueError("username is required.") + + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + raise ValueError(f'Shared folder "{shared_folder_uid}" not found.') + + sf_key = _decrypt_shared_folder_key(auth, sf) + if not sf_key: + raise ValueError(f'Shared folder "{shared_folder_uid}" key could not be decrypted.') + + rq, display_name = _build_shared_folder_update_request(auth, shared_folder_uid, sf, sf_key) + + user_is_member = _is_shared_folder_user_member(sf, username) + if _has_no_share_option_changes(manage_users, manage_records, expiration) and user_is_member: + return get_shared_folder_records_display(auth, shared_folder_uid) + + sfu = folder_pb2.SharedFolderUpdateUser() + sfu.username = username + sfu.expiration = expiration or 0 + + if user_is_member: + sfu.manageUsers = ( + folder_pb2.BOOLEAN_NO_CHANGE + if manage_users is None + else (folder_pb2.BOOLEAN_TRUE if manage_users else folder_pb2.BOOLEAN_FALSE) + ) + sfu.manageRecords = ( + folder_pb2.BOOLEAN_NO_CHANGE + if manage_records is None + else (folder_pb2.BOOLEAN_TRUE if manage_records else folder_pb2.BOOLEAN_FALSE) + ) + rq.sharedFolderUpdateUser.append(sfu) + else: + default_mu = sf.get("default_manage_users", False) + default_mr = sf.get("default_manage_records", False) + sfu.manageUsers = ( + folder_pb2.BOOLEAN_TRUE if (manage_users if manage_users is not None else default_mu) else folder_pb2.BOOLEAN_FALSE + ) + sfu.manageRecords = ( + folder_pb2.BOOLEAN_TRUE if (manage_records if manage_records is not None else default_mr) else folder_pb2.BOOLEAN_FALSE + ) + + encrypted_key, key_type = _encrypt_shared_folder_key_for_user(auth, username, sf_key) + sfu.typedSharedFolderKey.encryptedKey = encrypted_key + sfu.typedSharedFolderKey.encryptedKeyType = key_type + rq.sharedFolderAddUser.append(sfu) + + rs = auth.execute_auth_rest( + _SHARED_FOLDER_UPDATE_V3_ENDPOINT, rq, response_type=folder_pb2.SharedFolderUpdateV3Response + ) + assert rs is not None + for arr in (rs.sharedFolderAddUserStatus, rs.sharedFolderUpdateUserStatus): + for st in arr: + if not _is_put_status_ok(st.status): + raise ValueError( + f'Put "{st.username}" to Shared Folder "{display_name}" error: {st.status}' + ) + + return get_shared_folder_records_display(auth, shared_folder_uid) + + +def revoke_shared_folder_from_user( + auth: keeper_auth.KeeperAuth, + shared_folder_uid: str, + username: str, +) -> None: + """ + Remove a user from a shared folder without requiring a synced vault. + """ + auth = _ensure_auth(auth) + if not shared_folder_uid: + raise ValueError("shared_folder_uid is required.") + if not username: + raise ValueError("username is required.") + + sf = load_shared_folder_raw(auth, shared_folder_uid) + if not sf: + return + if not _is_shared_folder_user_member(sf, username): + return + + sf_key = _decrypt_shared_folder_key(auth, sf) + if not sf_key: + raise ValueError(f'Shared folder "{shared_folder_uid}" key could not be decrypted.') + + rq, display_name = _build_shared_folder_update_request(auth, shared_folder_uid, sf, sf_key) + rq.sharedFolderRemoveUser.append(username) + + rs = auth.execute_auth_rest( + _SHARED_FOLDER_UPDATE_V3_ENDPOINT, rq, response_type=folder_pb2.SharedFolderUpdateV3Response + ) + assert rs is not None + for st in rs.sharedFolderRemoveUserStatus: + if not _is_remove_status_ok(st.status): + raise ValueError( + f'Remove user "{st.username}" from Shared Folder "{display_name}" error: {st.status}' + ) + From 6035f59e720a1f633449773c023fc5c8cf9ba006 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 30 Apr 2026 19:50:56 +0530 Subject: [PATCH 10/23] AWS lambda list record sample script and readme (#162) --- .../list_records_lambda_function/.gitignore | 7 + .../list_records_lambda_function/README.md | 189 ++++++++++ .../lambda_function.py | 329 ++++++++++++++++++ .../requirements.txt | 2 + 4 files changed, 527 insertions(+) create mode 100644 examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/.gitignore create mode 100644 examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/README.md create mode 100644 examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/lambda_function.py create mode 100644 examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/requirements.txt diff --git a/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/.gitignore b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/.gitignore new file mode 100644 index 00000000..3ad496e4 --- /dev/null +++ b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/.gitignore @@ -0,0 +1,7 @@ +# Dependencies are produced with: pip install -r requirements.txt -t . +# Track only hand-edited example files; do not commit the install tree. +/* +!/.gitignore +!/lambda_function.py +!/requirements.txt +!/README.md diff --git a/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/README.md b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/README.md new file mode 100644 index 00000000..c7f9626a --- /dev/null +++ b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/README.md @@ -0,0 +1,189 @@ +# Keeper Records Lambda (Config + Password Fallback) + +This Lambda function logs in to Keeper and lists user's vault records. + +It is designed to: +- use `config.json` from Keeper first (stored in AWS Secrets Manager as `keeper-secret-1`) +- automatically fall back to a password secret (`keeper-secret-2`) when config-based login is not valid for that run + +This makes the Lambda non-interactive and suitable for AWS execution. + +## What is in GitHub, and what you must build + +This repository (and the `keeper-sdk-python` project on GitHub) includes **source only**: `lambda_function.py`, `requirements.txt`, and this guide. **Third-party dependencies are not checked in**—on purpose, so the repo stays small and maintainable (a `.gitignore` in this folder is meant to keep the `pip install -t` output from being re-committed). **The Lambda will not work** until you install those dependencies into the same directory as the handler, then **zip** that full tree, then **upload** the zip to AWS Lambda. See [Build the deployment package](#build-the-deployment-package) below for the exact commands. + +## What This Function Does + +1. Reads Keeper config from AWS Secrets Manager (`keeper-secret-1`) +2. Attempts non-interactive login using persisted config/session data +3. If Keeper requests password (`LoginStepPassword`), reads password from `keeper-secret-2` +4. Completes login, syncs the vault, and returns records + +## Prerequisites + +- AWS account with permissions to create and run Lambda + Secrets Manager secrets +- Keeper account credentials +- A machine with Python **3.11+** and `pip` (or a CI image) to run `pip install` for the deployment package +- Local machine with Keeper SDK workflow that generates `~/.keeper/config.json` +- For the Lambda **runtime** in AWS: set **Python 3.11** to match a typical build (see the packaging section below for matching Linux vs macOS/Windows when building the zip) + +## Build the deployment package + +Do this **before** you upload code to Lambda. The handler file alone is not enough: Lambda needs `boto3`, `keepersdk`, and all transitive dependencies installed next to `lambda_function.py` inside the zip. + +1. Open a shell and go to the directory that contains `lambda_function.py` and `requirements.txt` (this folder). + +2. Install dependencies into the **same directory** (typical for Lambda function packages): + + ```text + pip install -r requirements.txt -t . + ``` + +3. If `requirements.txt` points at `../../../../keepersdk-package`, that path only works from a full clone of `keeper-sdk-python` at the expected path. If you are using a standalone copy of the example, edit `requirements.txt` and use the `keepersdk` line from [PyPI](https://pypi.org/project/keepersdk/) instead, then run the `pip` command again. + +4. **Build the zip for Lambda (Linux).** The runtime is Amazon Linux; if you run `pip` on **macOS or Windows**, the wheels you get may not run on Lambda. For production packages, use a **Linux** environment with **Python 3.11** (for example a container image or Amazon Linux 2) and run the same `pip install` there before zipping. + +5. From *inside* this directory (so paths at the root of the zip are correct for Lambda), create a zip of **everything** needed, including the installed site-packages. For example: + + ```text + zip -r ../list_records_lambda_function.zip . + ``` + + (Adjust the name or parent path as you like; the important part is that the archive contains `lambda_function.py` and the top-level import packages, e.g. `boto3/`, `keepersdk/`, and their dependencies, at the **root** of the zip.) + +6. You will **upload** this zip in the Lambda console (or via CLI/API) as the function code. **Without this step, the function will fail** at import time because dependencies are not present in GitHub and are not on Lambda by default. + +## Step 1: Prepare Keeper Config Locally + +Login to Keeper locally with your email and password, then: + +- register device +- enable persistent login +- set a long timeout (for example, 30 days) + +This creates/updates your local Keeper config at: + +- `~/.keeper/config.json` + +## Step 2: Create Secret `keeper-secret-1` (Config Secret) + +In AWS Secrets Manager, create a secret named: + +- `keeper-secret-1` + +Paste the contents of `~/.keeper/config.json` as plaintext JSON. + +### Optional safety note (base64) + +If you are not comfortable pasting raw JSON, you can base64-encode it first and store that value. +In that case, `lambda_function.py` can be updated to decode the base64 payload before parsing JSON. + +> Current implementation expects JSON config content as the secret payload. + +## Step 3: Create Secret `keeper-secret-2` (Password Fallback) + +Create another secret named: + +- `keeper-secret-2` + +Store password as JSON, for example: + +```json +{"password":"Pass@123"} +``` + +Why this exists: + +- Normally, Lambda will authenticate from `keeper-secret-1` config. +- If config/session data is stale or invalid for a run, Keeper may require password verification. +- Lambda is non-interactive, so it uses `keeper-secret-2` automatically as fallback. + +## Step 4: Configure Lambda Function + +Create/update your Lambda with `lambda_function.py`. + +Use these settings and paths in AWS Lambda console: + +- **Memory**: Go to `Configuration` -> `General configuration` -> `Edit`, set to `512 MB`. +- **Timeout**: In `Configuration` -> `General configuration` -> `Edit`, set at least `15-30 seconds` (30 seconds recommended). +- **Runtime**: Under code section, open `Runtime settings` (Code properties) and set to `Python 3.11`. + +### Lambda Execution Role Permissions + +Make sure the Lambda execution role has at least: + +- `secretsmanager:GetSecretValue` +- `secretsmanager:DescribeSecret` (recommended) +- `logs:CreateLogGroup` +- `logs:CreateLogStream` +- `logs:PutLogEvents` + +If secrets use a customer-managed KMS key, also allow: + +- `kms:Decrypt` for that key + +Scope secret access to: + +- `keeper-secret-1` +- `keeper-secret-2` + +## Step 5: Upload Deployment Package + +In Lambda code source: + +1. Click **Upload from** +2. Select **.zip file** +3. Upload the **zip you built** in [Build the deployment package](#build-the-deployment-package) (for example `list_records_lambda_function.zip`). Do not expect a pre-built zip to be present in the GitHub tree; you must build it locally or in CI. + +That package must include dependencies installed with `pip install -r requirements.txt -t .`, so that components such as `boto3` and `keepersdk` are importable in Lambda. + +## Step 6: Set Environment Variables (Optional/Recommended) + +The function supports these environment variables: + +- `KEEPER_SECRET_ID` (default: `keeper-secret-1`) +- `KEEPER_PASSWORD_SECRET_ID` (default: `keeper-secret-2`) +- `AWS_REGION` or `KEEPER_AWS_REGION` +- `KEEPER_USERNAME` (optional fallback if config lacks username) +- `KEEPER_SERVER` (optional fallback if config lacks server) + +If you keep the default secret names, you can skip the first two. + +## Step 7: Test the Function + +1. Create a default test event (any name is fine) +2. Click the blue **Test** button (under Deploy) + +Expected result: + +- `statusCode: 200` +- response body with `ok: true` +- `record_count` and `records` list returned + +If fallback is used, logs include a message similar to: + +- `Config-based resume login is not valid for this run; falling back to master password from secret 'keeper-secret-2'.` + +## Troubleshooting + +- **`Missing username in config secret`** + - Ensure `last_login` exists in `keeper-secret-1`, or set `KEEPER_USERNAME`. + +- **Password fallback verification fails** + - Validate `keeper-secret-2` format and correct password value. + +- **Secrets Manager access errors** + - Check Lambda role IAM permissions and KMS decrypt permissions. + +- **Timeout or slow execution** + - Increase lambda timeout to 30 seconds or more and retry. + +- **Non-interactive step errors (2FA/SSO/device approval loops)** + - Re-run local Keeper login, confirm persistent login/device registration, and refresh `keeper-secret-1` from latest `~/.keeper/config.json`. + +## Security Recommendations + +- Do not print secrets or passwords in logs. +- Restrict IAM permissions to only required secret ARNs. +- Rotate password in `keeper-secret-2` as needed. +- Prefer AWS-managed encryption/KMS and audit secret access with CloudTrail. diff --git a/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/lambda_function.py b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/lambda_function.py new file mode 100644 index 00000000..5dfb5e16 --- /dev/null +++ b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/lambda_function.py @@ -0,0 +1,329 @@ +import base64 +import json +import logging +import os +import sqlite3 +from typing import Any, Dict, List, Optional + +import boto3 + +from keepersdk import utils +from keepersdk.authentication import configuration, endpoint, keeper_auth, login_auth +from keepersdk.vault import sqlite_storage, vault_online, vault_record + + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class SecretsManagerJsonLoader(configuration.IJsonLoader): + """IJsonLoader implementation backed by AWS Secrets Manager.""" + + def __init__(self, secret_id: str, region_name: Optional[str] = None) -> None: + self.secret_id = secret_id + self.client = boto3.client("secretsmanager", region_name=region_name) + self._cached_bytes: Optional[bytes] = None + self._dirty = False + + def load_json(self) -> bytes: + if self._cached_bytes is not None: + return self._cached_bytes + + response = self.client.get_secret_value(SecretId=self.secret_id) + if "SecretString" in response: + secret_bytes = response["SecretString"].encode("utf-8") + else: + # Secrets Manager returns base64-encoded bytes for SecretBinary. + secret_bytes = base64.b64decode(response["SecretBinary"]) + + # Normalize empty payloads to JSON object. + payload = secret_bytes.strip() or b"{}" + self._cached_bytes = payload + return payload + + def store_json(self, data: bytes) -> None: + # JsonConfigurationStorage.put() calls this; persist later with flush(). + self._cached_bytes = data + self._dirty = True + + def flush(self) -> None: + if not self._dirty or self._cached_bytes is None: + return + self.client.put_secret_value( + SecretId=self.secret_id, + SecretString=self._cached_bytes.decode("utf-8"), + ) + self._dirty = False + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """Enable persistent login and register device key for future resume login.""" + keeper_auth.set_user_setting(keeper_auth_context, "persistent_login", "1") + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 + keeper_auth.set_user_setting( + keeper_auth_context, "logout_timer", str(timeout_in_minutes) + ) + + +def _fail_non_interactive(step: Any) -> None: + raise RuntimeError( + "Non-interactive Lambda login cannot continue. " + f"Received step: {type(step).__name__}. " + "Run one interactive login locally to fully approve/register this device, " + "then update the same config JSON in Secrets Manager." + ) + + +def _configuration_health(conf: configuration.IKeeperConfiguration) -> Dict[str, Any]: + """Return non-sensitive diagnostics for secret/config completeness.""" + user = conf.last_login or "" + has_user = bool(user) + has_server = bool(conf.last_server) + user_cfg = conf.users().get(user) if user else None + last_device_token = "" + if user_cfg and user_cfg.last_device: + last_device_token = user_cfg.last_device.device_token or "" + has_last_device = bool(last_device_token) + + device_cfg = conf.devices().get(last_device_token) if last_device_token else None + has_device = bool(device_cfg) + has_private_key = bool(device_cfg and device_cfg.private_key) + has_clone_code = bool( + device_cfg + and device_cfg.get_server_info() + and device_cfg.get_server_info().get(conf.last_server) + and device_cfg.get_server_info().get(conf.last_server).clone_code + ) + return { + "has_last_login": has_user, + "has_last_server": has_server, + "users_count": len(list(conf.users().list())), + "devices_count": len(list(conf.devices().list())), + "has_user_last_device": has_last_device, + "has_device_entry_for_last_device": has_device, + "has_device_private_key": has_private_key, + "has_clone_code_for_last_server": has_clone_code, + } + + +def _password_not_allowed_error() -> RuntimeError: + return RuntimeError( + "Keeper requested Password step in Lambda, but this function is configured " + "for password-free persistent login only. Update Secrets Manager with your " + "latest local Keeper config after enabling persistent login on the same device." + ) + + +def _fetch_password_from_secret( + secret_id: str, region_name: Optional[str] = None +) -> str: + """Read Keeper password from Secrets Manager secret string/binary.""" + client = boto3.client("secretsmanager", region_name=region_name) + response = client.get_secret_value(SecretId=secret_id) + if "SecretString" in response: + secret_text = response["SecretString"] + else: + secret_text = base64.b64decode(response["SecretBinary"]).decode("utf-8") + + value = secret_text.strip() + if not value: + raise ValueError(f"Password secret '{secret_id}' is empty.") + + # Accept either a raw secret string or a JSON object with a password field. + try: + parsed = json.loads(value) + except json.JSONDecodeError: + parsed = None + + if isinstance(parsed, dict): + for key in ("password", "keeper_password", "value"): + candidate = parsed.get(key) + if isinstance(candidate, str) and candidate.strip(): + return candidate.strip() + raise ValueError( + f"Password secret '{secret_id}' JSON must include one of: " + "'password', 'keeper_password', or 'value'." + ) + + return value + + +def login_from_secret( + secret_id: str, region_name: Optional[str] = None +) -> keeper_auth.KeeperAuth: + """ + Login with Keeper config stored in Secrets Manager. + Works only for resume/session-based non-interactive login in Lambda. + """ + loader = SecretsManagerJsonLoader(secret_id=secret_id, region_name=region_name) + config_storage = configuration.JsonConfigurationStorage(loader=loader) + conf = config_storage.get() + + if not conf.last_server: + conf.last_server = os.getenv("KEEPER_SERVER", "keepersecurity.com") + if not conf.last_login: + env_user = os.getenv("KEEPER_USERNAME", "").strip() + if env_user: + conf.last_login = env_user + + if not conf.last_login: + raise ValueError( + "Missing username in config secret. " + "Set last_login/user in secret JSON or KEEPER_USERNAME env var." + ) + + config_storage.put(conf) + + keeper_endpoint = endpoint.KeeperEndpoint(config_storage, conf.last_server) + password_secret_id = os.getenv("KEEPER_PASSWORD_SECRET_ID", "keeper-secret-2") + password_from_secret: Optional[str] = None + + login_ctx = login_auth.LoginAuth(keeper_endpoint) + login_ctx.resume_session = True + login_ctx.login(conf.last_login) + + approval_attempted = False + used_interactive_step = False + + while not login_ctx.login_step.is_final(): + step = login_ctx.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + if approval_attempted: + _fail_non_interactive(step) + approval_attempted = True + step.resume() + used_interactive_step = True + continue + + if isinstance(step, login_auth.LoginStepPassword): + logger.info( + "Config-based resume login is not valid for this run; " + "falling back to master password from secret '%s'.", + password_secret_id, + ) + if password_from_secret is None: + password_from_secret = _fetch_password_from_secret( + secret_id=password_secret_id, region_name=region_name + ) + try: + step.verify_password(password_from_secret) + used_interactive_step = True + continue + except Exception as ex: + raise RuntimeError( + "Keeper requested password fallback, but verification failed " + f"using secret '{password_secret_id}'." + ) from ex + + if isinstance( + step, + ( + login_auth.LoginStepTwoFactor, + login_auth.LoginStepSsoToken, + login_auth.LoginStepSsoDataKey, + ), + ): + _fail_non_interactive(step) + + if isinstance(step, login_auth.LoginStepError): + raise RuntimeError(f"Keeper login error: ({step.code}) {step.message}") + + _fail_non_interactive(step) + + if not isinstance(login_ctx.login_step, login_auth.LoginStepConnected): + raise RuntimeError("Login did not reach connected state.") + + keeper_ctx = login_ctx.login_step.take_keeper_auth() + + # If login needed any step loop, ensure persistent login/device key are set. + if used_interactive_step: + enable_persistent_login(keeper_ctx) + + # Persist refreshed config (clone codes/device/server mappings) back to secret. + config_storage.put(config_storage.get()) + loader.flush() + return keeper_ctx + + +def list_records(keeper_auth_context: keeper_auth.KeeperAuth) -> List[Dict[str, Any]]: + """Sync vault and return records shaped like the example output.""" + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + try: + vault.sync_down() + output: List[Dict[str, Any]] = [] + for record in vault.vault_data.records(): + item: Dict[str, Any] = { + "Title": record.title, + "Record UID": record.record_uid, + "Record Type": record.record_type, + } + if record.version == 2: + legacy_record = vault.vault_data.load_record(record.record_uid) + if isinstance(legacy_record, vault_record.PasswordRecord): + item["Username"] = legacy_record.login + item["URL"] = legacy_record.link + output.append(item) + return output + finally: + vault.close() + keeper_auth_context.close() + + +def lambda_handler(event, context): + """ + Lambda handler. + Env vars: + - KEEPER_SECRET_ID (default: keeper-secret-1) + - KEEPER_PASSWORD_SECRET_ID (default: keeper-secret-2) + - AWS_REGION / KEEPER_AWS_REGION (optional override) + - KEEPER_USERNAME (optional fallback if secret lacks last_login) + - KEEPER_SERVER (optional fallback if secret lacks last_server) + """ + secret_id = os.getenv("KEEPER_SECRET_ID", "keeper-secret-1") + region = os.getenv("KEEPER_AWS_REGION") or os.getenv("AWS_REGION") + run_diagnostics = bool(event and event.get("diagnose_config")) + + try: + if run_diagnostics: + loader = SecretsManagerJsonLoader(secret_id=secret_id, region_name=region) + config_storage = configuration.JsonConfigurationStorage(loader=loader) + conf = config_storage.get() + diagnostics = _configuration_health(conf) + return { + "statusCode": 200, + "body": json.dumps({"ok": True, "diagnostics": diagnostics}), + } + + keeper_ctx = login_from_secret(secret_id=secret_id, region_name=region) + records = list_records(keeper_ctx) + return { + "statusCode": 200, + "body": json.dumps( + { + "ok": True, + "record_count": len(records), + "records": records, + } + ), + } + except Exception as e: + logger.exception("Keeper Lambda execution failed") + return { + "statusCode": 500, + "body": json.dumps({"ok": False, "error": str(e)}), + } diff --git a/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/requirements.txt b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/requirements.txt new file mode 100644 index 00000000..cfe35b4e --- /dev/null +++ b/examples/sdk_examples/aws_cloud_sample/list_records_lambda_function/requirements.txt @@ -0,0 +1,2 @@ +boto3 +keepersdk From ec1ea2264a111d69c0ceb6f612d21e628b5c44b7 Mon Sep 17 00:00:00 2001 From: idimov-keeper <78815270+idimov-keeper@users.noreply.github.com> Date: Sun, 3 May 2026 23:12:34 -0500 Subject: [PATCH 11/23] Fix sync_down: copy manageUsers into team manage_users --- keepersdk-package/src/keepersdk/vault/sync_down.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepersdk-package/src/keepersdk/vault/sync_down.py b/keepersdk-package/src/keepersdk/vault/sync_down.py index 9b7bec93..9cf6c679 100644 --- a/keepersdk-package/src/keepersdk/vault/sync_down.py +++ b/keepersdk-package/src/keepersdk/vault/sync_down.py @@ -401,7 +401,7 @@ def to_sf_team(sft: SyncDown_pb2.SharedFolderTeam) -> StorageSharedFolderPermiss s_sfp.user_type = SharedFolderUserType.Team s_sfp.user_uid = utils.base64_url_encode(sft.teamUid) s_sfp.manage_records = sft.manageRecords - s_sfp.manage_users = sft.manageRecords + s_sfp.manage_users = sft.manageUsers s_sfp.expiration_time = sft.expiration return s_sfp From 6a74fdb520079c9b6c5ba4bcbc6e0b2333341c69 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 7 May 2026 09:43:29 +0530 Subject: [PATCH 12/23] msp down, info, add, update, remove, convert-node commands added --- .../src/keepercli/commands/enterprise_info.py | 4 +- .../src/keepercli/commands/msp.py | 317 ++++- .../src/keepercli/register_commands.py | 6 + .../enterprise/enterprise_constants.py | 4 +- .../src/keepersdk/enterprise/msp_auth.py | 1158 ++++++++++++++++- 5 files changed, 1480 insertions(+), 9 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/enterprise_info.py b/keepercli-package/src/keepercli/commands/enterprise_info.py index 7bce8179..a8bac213 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_info.py +++ b/keepercli-package/src/keepercli/commands/enterprise_info.py @@ -17,6 +17,8 @@ SUPPORTED_TEAM_COLUMNS = ['restricts', 'node', 'user_count', 'users', 'queued_user_count', 'queued_users', 'role_count', 'roles'] SUPPORTED_ROLE_COLUMNS = ['visible_below', 'default_role', 'admin', 'node', 'user_count', 'users', 'team_count', 'teams'] +# API uses signed 32-bit INT_MAX for unlimited seat counts. +_INT32_MAX = (1 << 31) - 1 class EnterpriseDownCommand(base.ArgparseCommand): def __init__(self): @@ -759,7 +761,7 @@ def execute(self, context: KeeperParams, **kwargs): node_name = enterprise_utils.NodeUtils.get_node_path(enterprise_data, mc.msp_node_id, omit_root=True) allocated: Optional[int] = mc.number_of_seats - if allocated == 2147483647: + if allocated == _INT32_MAX: allocated = None # Get active users diff --git a/keepercli-package/src/keepercli/commands/msp.py b/keepercli-package/src/keepercli/commands/msp.py index 2c7138a3..e4271908 100644 --- a/keepercli-package/src/keepercli/commands/msp.py +++ b/keepercli-package/src/keepercli/commands/msp.py @@ -1,10 +1,323 @@ import argparse +from typing import List, Optional -from keepersdk.enterprise import msp_auth -from . import base +from keepersdk import errors as sdk_errors +from keepersdk.enterprise import enterprise_constants, msp_auth +from . import base, enterprise_utils from .. import prompt_utils, api +from ..helpers import report_utils from ..params import KeeperParams +_MSP_PLAN_CHOICES = [x[1] for x in enterprise_constants.MSP_PLANS] + + +class MspDownCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-down', + description='Download current MSP data from the Keeper cloud (refresh managed companies and enterprise graph).', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '--reset', + dest='reset', + action='store_true', + help='Clear sync continuation token and reload enterprise data from scratch', + ) + + def execute(self, context: KeeperParams, **kwargs) -> None: + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + + reset: Optional[bool] = None + if kwargs.get('reset') is True: + reset = True + msp_auth.msp_down(enterprise_loader, reset=reset or False) + + +class MspInfoCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-info', + parents=[base.report_output_parser], + description='Display MSP details, including managed companies, restrictions, and pricing.', + formatter_class=argparse.RawTextHelpFormatter, + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('-p', '--pricing', dest='pricing', action='store_true', help='Display pricing information') + parser.add_argument('-r', '--restriction', dest='restriction', action='store_true', + help='Display MSP restriction information') + parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Print details') + parser.add_argument('-mc', '--managed-company', dest='managed_company', action='store', + help='Filter by managed company name or id') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + + try: + report = msp_auth.msp_info( + enterprise_loader, + restriction=bool(kwargs.get('restriction')), + pricing=bool(kwargs.get('pricing')), + managed_company=kwargs.get('managed_company'), + verbose=bool(kwargs.get('verbose')), + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + if report.message: + api.get_logger().info(report.message) + return None + + headers = list(report.headers) + fmt = kwargs.get('format') + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + + return report_utils.dump_report_data( + [list(r) for r in report.rows], + headers, + fmt=fmt, + filename=kwargs.get('output'), + row_number=report.row_numbers, + ) + + +class MspAddCommand(base.ArgparseCommand): + fp_help = ', '.join((x[2].lower() for x in enterprise_constants.MSP_FILE_PLANS)) + addon_help = ', '.join((x[0] + (':N' if x[2] else '') for x in enterprise_constants.MSP_ADDONS)) + + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-add', + description='Add a managed company to the MSP tenant.', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--node', dest='node', action='store', help='Node name or node ID (default: enterprise root)') + parser.add_argument('-s', '--seats', dest='seats', action='store', type=int, + help='Maximum licences allowed (-1 for unlimited when permitted)') + parser.add_argument('-p', '--plan', dest='plan', action='store', required=True, choices=_MSP_PLAN_CHOICES, + help=f'License plan: {", ".join(_MSP_PLAN_CHOICES)}') + parser.add_argument('-f', '--file-plan', dest='file_plan', action='store', help=f'File storage plan: {MspAddCommand.fp_help}') + parser.add_argument('-a', '--addon', dest='addon', action='append', metavar='ADDON[:SEATS]', + help=f'Add-ons: {MspAddCommand.addon_help}') + parser.add_argument('name', action='store', help='Managed company name') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + enterprise_data = context.enterprise_data + + node_arg = kwargs.get('node') + if node_arg: + node = enterprise_utils.NodeUtils.resolve_single_node(enterprise_data, node_arg) + node_id = node.node_id + else: + node_id = enterprise_data.root_node.node_id + + addon_list: Optional[List[str]] = kwargs.get('addon') + if isinstance(addon_list, list) and len(addon_list) == 0: + addon_list = None + + try: + mc_id = msp_auth.msp_add_managed_company( + enterprise_loader, + enterprise_name=str(kwargs['name']), + plan=str(kwargs['plan']), + node_id=node_id, + seats=kwargs.get('seats'), + file_plan=kwargs.get('file_plan'), + addons=addon_list, + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + api.get_logger().info('Managed company "%s" added. ID=%s', kwargs['name'], mc_id) + return mc_id + + +class MspUpdateCommand(base.ArgparseCommand): + fp_help = ', '.join((x[2].lower() for x in enterprise_constants.MSP_FILE_PLANS)) + addon_help = ', '.join(((x[0] + (':N' if x[2] else '')) for x in enterprise_constants.MSP_ADDONS)) + + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-update', + description='Modify a managed company license (plan, seats, node, add-ons).', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--node', dest='node', action='store', help='Node name or node ID') + parser.add_argument('-n', '--name', dest='name', action='store', help='Update managed company name') + parser.add_argument('-p', '--plan', dest='plan', action='store', choices=_MSP_PLAN_CHOICES, + help=f'License plan: {", ".join(_MSP_PLAN_CHOICES)}') + parser.add_argument('-s', '--seats', dest='seats', action='store', type=int, + help='Maximum licences allowed (-1 for unlimited when permitted)') + parser.add_argument('-f', '--file-plan', dest='file_plan', action='store', help=f'File storage plan: {MspUpdateCommand.fp_help}') + parser.add_argument('-aa', '--add-addon', dest='add_addon', action='append', metavar='ADDON[:SEATS]', + help=f'Add add-ons: {MspUpdateCommand.addon_help}') + parser.add_argument('-ra', '--remove-addon', dest='remove_addon', action='append', metavar='ADDON', + help=f'Remove add-ons: {MspUpdateCommand.addon_help}') + parser.add_argument('mc', action='store', help='Managed company name or id') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + enterprise_data = context.enterprise_data + + node_id: Optional[int] = None + node_arg = kwargs.get('node') + if node_arg: + node = enterprise_utils.NodeUtils.resolve_single_node(enterprise_data, node_arg) + node_id = node.node_id + + add_list: Optional[List[str]] = kwargs.get('add_addon') + if isinstance(add_list, list) and len(add_list) == 0: + add_list = None + rem_list: Optional[List[str]] = kwargs.get('remove_addon') + if isinstance(rem_list, list) and len(rem_list) == 0: + rem_list = None + + try: + eid = msp_auth.msp_update_managed_company( + enterprise_loader, + managed_company=str(kwargs['mc']), + node_id=node_id, + new_name=kwargs.get('name'), + plan=kwargs.get('plan'), + seats=kwargs.get('seats'), + file_plan=kwargs.get('file_plan'), + add_addons=add_list, + remove_addons=rem_list, + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + api.get_logger().info('Successfully updated managed company id=%s', eid) + return eid + + +class MspRemoveCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-remove', + description='Remove a managed company (MC) tenant.', + ) + self.add_arguments_to_parser(parser) + super().__init__(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('mc', action='store', help='Managed company name or id') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + enterprise_data = context.enterprise_data + + mc_input = str(kwargs.get('mc') or '').strip() + if not mc_input: + raise base.CommandError('Managed Company name or id is required') + + current = None + if mc_input.isdigit(): + current = enterprise_data.managed_companies.get_entity(int(mc_input)) + else: + key = mc_input.lower() + for mc in enterprise_data.managed_companies.get_all_entities(): + if mc.mc_enterprise_name.lower() == key: + current = mc + break + if current is None: + raise base.CommandError(f'Managed Company "{mc_input}" not found') + + if not kwargs.get('force'): + seats = current.number_of_seats + seat_txt = 'unlimited' if seats > 2147483646 else str(seats) + msg = ( + 'ALERT: Remove Managed Company.\n\n' + 'Removing will expire the licences for the managed company and your admin access for the account.\n' + f'Managed company: "{current.mc_enterprise_name}", licences: {seat_txt}\n\n' + 'I want to remove these licences, the managed vault folder, and my access to the admin console from my MSP account.' + ) + answer = prompt_utils.user_choice(msg, 'yn', default='n') + if str(answer).lower() != 'y': + api.get_logger().info('Removal cancelled') + return None + + try: + eid = msp_auth.msp_remove_managed_company(enterprise_loader, managed_company=mc_input) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + api.get_logger().info('Managed company "%s" removed. ID=%s', current.mc_enterprise_name, eid) + return eid + + +class MspConvertNodeCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-convert-node', + description='Convert an MSP enterprise subtree (node and descendants) into a managed company.', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('-s', '--seats', dest='seats', action='store', type=int, + help='Number of seats when registering a new managed company (defaults to subtree user count)') + parser.add_argument('-p', '--plan', dest='plan', action='store', choices=_MSP_PLAN_CHOICES, + help=f'License plan when registering a new MC (default: business). Options: {", ".join(_MSP_PLAN_CHOICES)}') + parser.add_argument('node', action='store', help='Node name or node ID (subtree root)') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + enterprise_data = context.enterprise_data + + node_arg = str(kwargs.get('node') or '').strip() + if not node_arg: + raise base.CommandError('Node name or node ID is required') + node = enterprise_utils.NodeUtils.resolve_single_node(enterprise_data, node_arg) + + try: + mc_id = msp_auth.msp_convert_node( + enterprise_loader, + node_id=node.node_id, + seats=kwargs.get('seats'), + plan=kwargs.get('plan'), + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + api.get_logger().info('Node "%s" was converted to managed company id=%s', node.name or node.node_id, mc_id) + return mc_id + class SwitchToManagedCompanyCommand(base.ArgparseCommand): parser = argparse.ArgumentParser(prog='switch-to-mc', description='Switch to a managed company context') diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index ef77a7d5..dbe4494b 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -108,6 +108,12 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('apply-membership', importer_commands.ApplyMembershipCommand(), base.CommandScope.Enterprise) commands.register_command('device-approve', enterprise_user.EnterpriseDeviceApprovalCommand(), base.CommandScope.Enterprise) commands.register_command('pedm', pedm_admin.PedmCommand(), base.CommandScope.Enterprise) + commands.register_command('msp-down', msp.MspDownCommand(), base.CommandScope.Enterprise, 'md') + commands.register_command('msp-info', msp.MspInfoCommand(), base.CommandScope.Enterprise, 'mi') + commands.register_command('msp-add', msp.MspAddCommand(), base.CommandScope.Enterprise, 'ma') + commands.register_command('msp-update', msp.MspUpdateCommand(), base.CommandScope.Enterprise, 'mu') + commands.register_command('msp-remove', msp.MspRemoveCommand(), base.CommandScope.Enterprise, 'mrm') + commands.register_command('msp-convert-node', msp.MspConvertNodeCommand(), base.CommandScope.Enterprise) commands.register_command('switch-to-mc', msp.SwitchToManagedCompanyCommand(), base.CommandScope.Enterprise) commands.register_command('team-approve', enterprise_team.TeamApproveCommand(), base.CommandScope.Enterprise) commands.register_command('user-report', user_report.UserReportCommand(), base.CommandScope.Enterprise, 'ur') diff --git a/keepersdk-package/src/keepersdk/enterprise/enterprise_constants.py b/keepersdk-package/src/keepersdk/enterprise/enterprise_constants.py index cc51addd..05de45e2 100644 --- a/keepersdk-package/src/keepersdk/enterprise/enterprise_constants.py +++ b/keepersdk-package/src/keepersdk/enterprise/enterprise_constants.py @@ -4,8 +4,8 @@ # MSP Constants MSP_FILE_PLANS = [ (4, 'STORAGE_100GB', '100GB'), - (7, 'STORAGE_1000GB', '1TB'), - (8, 'STORAGE_10000GB', '10TB'), + (7, 'STORAGE_1TB', '1TB'), + (8, 'STORAGE_10TB', '10TB'), ] MSP_PLANS = [ (1, 'business', 'Business', 4), diff --git a/keepersdk-package/src/keepersdk/enterprise/msp_auth.py b/keepersdk-package/src/keepersdk/enterprise/msp_auth.py index 7c150b18..6126f9a5 100644 --- a/keepersdk-package/src/keepersdk/enterprise/msp_auth.py +++ b/keepersdk-package/src/keepersdk/enterprise/msp_auth.py @@ -1,9 +1,56 @@ -from typing import Tuple +import json +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Optional, Set, Tuple, Union +from urllib.parse import urlunparse from . import enterprise_types -from .. import crypto, utils +from . import enterprise_constants +from .. import crypto, errors, utils from ..authentication import keeper_auth -from ..proto import enterprise_pb2 +from ..proto import APIRequest_pb2, BI_pb2, breachwatch_pb2, enterprise_pb2 +from .private_data import _decrypt_encrypted_data + +_KEPM_ADDON = 'keeper_endpoint_privilege_manager' +_REMOTE_BROWSER_ISOLATION_ADDON = 'remote_browser_isolation' +_CONNECTION_MANAGER_ADDON = 'connection_manager' +_KEPM_VALID_SEATS = frozenset({1, 25, 50, 100, 500, 1000, 5000, 10000}) + +# API uses signed 32-bit INT_MAX for unlimited seat counts. +_INT32_MAX = (1 << 31) - 1 +_SEATS_UNLIMITED_THRESHOLD = _INT32_MAX - 1 + +_CMD_ENTERPRISE_REGISTRATION_BY_MSP = 'enterprise_registration_by_msp' +_CMD_ENTERPRISE_UPDATE_BY_MSP = 'enterprise_update_by_msp' +_CMD_ENTERPRISE_REMOVE_BY_MSP = 'enterprise_remove_by_msp' +_REST_LOGIN_TO_MC = 'authentication/login_to_mc' +_REST_NODE_TO_MANAGED_COMPANY = 'enterprise/node_to_managed_company' +_REST_USER_DATA_KEY_BY_NODE = 'enterprise/get_enterprise_user_data_key_by_node' +_REST_MC_PUBLIC_KEY = 'enterprise/get_enterprise_public_key' +_BI_CONSOLE_ADDONS_MAPPING = 'mapping/addons' +_BI_CONSOLE_MC_PRICING = 'subscription/mc_pricing' +_BI_API_PREFIX = '/bi_api/v2/enterprise_console/' + +_JSON_KEY_DISPLAYNAME = 'displayname' +_DISPLAYNAME_KEEPER_ADMIN = 'Keeper Administrator' +_DISPLAYNAME_ROOT = 'root' + +_MSG_NO_MC_RESTRICTIONS = 'MSP has no restrictions' +_MSG_NO_MANAGED_COMPANIES = 'No Managed Companies' +_DEFAULT_CONVERT_PLAN = 'business' +_KEY_TYPE_NO_KEY = 'no_key' +_ENCRYPTED_BY_DATA_KEY = 'encrypted_by_data_key' +_USER_DATA_KEY_TYPE_ID_ECC = 4 +_UNKNOWN_USER_LABEL = '?' + + +@dataclass(frozen=True) +class MspInfoReport: + """Tabular MSP info.""" + headers: Tuple[str, ...] + rows: Tuple[Tuple[Any, ...], ...] + row_numbers: bool = False + message: Optional[str] = None def login_to_managed_company(loader: enterprise_types.IEnterpriseLoader, mc_enterprise_id: int) -> Tuple[keeper_auth.KeeperAuth, bytes]: @@ -11,7 +58,7 @@ def login_to_managed_company(loader: enterprise_types.IEnterpriseLoader, mc_ente tree_key = loader.enterprise_data.enterprise_info.tree_key rq = enterprise_pb2.LoginToMcRequest() rq.mcEnterpriseId = mc_enterprise_id - rs = auth.execute_auth_rest('authentication/login_to_mc', rq, response_type=enterprise_pb2.LoginToMcResponse) + rs = auth.execute_auth_rest(_REST_LOGIN_TO_MC, rq, response_type=enterprise_pb2.LoginToMcResponse) assert rs is not None auth_context = keeper_auth.AuthContext() auth_context.username = auth.auth_context.username @@ -27,3 +74,1106 @@ def login_to_managed_company(loader: enterprise_types.IEnterpriseLoader, mc_ente return mc_auth, mc_tree_key + +def msp_down(loader: enterprise_types.IEnterpriseLoader, *, reset: bool = False) -> Set[int]: + """Download current MSP enterprise data from the Keeper cloud. + + :param loader: Active enterprise loader (MSP context). + :param reset: When True, clears continuation state and forces a full resync. + :return: Entity type ids touched during this load. + """ + return loader.load(reset=reset) + + +def decrypt_managed_company_tree_key(encrypted_tree_key_b64: str, msp_tree_key: bytes) -> Optional[bytes]: + """Decrypt a managed company tree key blob using the MSP enterprise tree key (AES-GCM v2).""" + if not encrypted_tree_key_b64: + return None + try: + return crypto.decrypt_aes_v2(utils.base64_url_decode(encrypted_tree_key_b64), msp_tree_key) + except Exception: + return None + + +def _bi_enterprise_console_url(auth: keeper_auth.KeeperAuth, endpoint: str) -> str: + host = auth.keeper_endpoint.server + path = _BI_API_PREFIX + endpoint.lstrip('/') + return urlunparse(('https', host, path, '', '', '')) + + +def _node_path(enterprise_data: enterprise_types.IEnterpriseData, node_id: int, *, omit_root: bool) -> str: + nodes: List[str] = [] + n_id = node_id + while isinstance(n_id, int) and n_id > 0: + node = enterprise_data.nodes.get_entity(n_id) + if node: + n_id = node.parent_id or 0 + if not omit_root or n_id > 0: + node_name = node.name + if not node_name and node.node_id == enterprise_data.root_node.node_id: + node_name = enterprise_data.enterprise_info.enterprise_name + nodes.append(node_name) + else: + break + nodes.reverse() + return '\\'.join(nodes) + + +class _MspBillingCurrency(str, Enum): + """API ``currency`` codes from billing/price payloads.""" + + USD = '$' + EUR = '\u20ac' + GBP = '\u00a3' + JPY = '\u00a5' + + +class _MspPriceUnit(str, Enum): + """API ``unit`` codes from billing/price payloads.""" + + USER_MONTH = 'user/month' + MONTH = 'month' + USER_CONSUMED_MONTH = '50k API calls/month' + + +def _billing_currency_display(currency: Any) -> Optional[str]: + if currency is None or currency == '': + return None + key = str(currency) + member = _MspBillingCurrency.__members__.get(key) + return member.value if member is not None else key + + +def _price_unit_display(unit: Any) -> Optional[str]: + if unit is None or unit == '': + return None + key = str(unit) + member = _MspPriceUnit.__members__.get(key) + return member.value if member is not None else key + + +def _price_text_short(price_info: Dict[str, Any]) -> str: + price = '' + amount = price_info.get('amount') + if amount is not None: + display_currency = _billing_currency_display(price_info.get('currency')) + if display_currency: + price += display_currency + price += str(amount) + return price + + +def _price_text(price_info: Dict[str, Any]) -> str: + price = _price_text_short(price_info) + if price: + display_unit = _price_unit_display(price_info.get('unit')) + if display_unit: + price += '/' + display_unit + return price + + +def _fetch_msp_addon_id_to_name(auth: keeper_auth.KeeperAuth) -> Dict[int, str]: + url = _bi_enterprise_console_url(auth, _BI_CONSOLE_ADDONS_MAPPING) + rq = BI_pb2.MappingAddonsRequest() + rs = auth.execute_auth_rest(url, rq, response_type=BI_pb2.MappingAddonsResponse) + if not rs: + raise errors.KeeperError("No response received from mapping addons API") + return {x.id: x.name for x in rs.addons} + + +def _fetch_mc_pricing(auth: keeper_auth.KeeperAuth) -> Dict[str, Dict[str, Dict[str, Any]]]: + plan_map = {x[0]: x[1] for x in enterprise_constants.MSP_PLANS} + file_map = {x[0]: x[1] for x in enterprise_constants.MSP_FILE_PLANS} + addon_map = _fetch_msp_addon_id_to_name(auth) + + pricing: Dict[str, Dict[str, Dict[str, Any]]] = { + 'mc_base_plans': {}, + 'mc_addons': {}, + 'mc_file_plans': {}, + } + + url = _bi_enterprise_console_url(auth, _BI_CONSOLE_MC_PRICING) + rq = BI_pb2.SubscriptionMcPricingRequest() + rs = auth.execute_auth_rest(url, rq, response_type=BI_pb2.SubscriptionMcPricingResponse) + if not rs: + raise errors.KeeperError("No response received from subscription mc pricing API") + + for bp in rs.basePlans: + if bp.id in plan_map: + code = plan_map[bp.id] + pricing['mc_base_plans'][code] = { + 'amount': bp.cost.amount, + 'unit': BI_pb2.Cost.AmountPer.Name(bp.cost.amountPer), + 'currency': BI_pb2.Currency.Name(bp.cost.currency), + } + for ap in rs.addons: + if ap.id in addon_map: + name = addon_map[ap.id] + pricing['mc_addons'][name] = { + 'amount': ap.cost.amount, + 'unit': BI_pb2.Cost.AmountPer.Name(ap.cost.amountPer), + 'currency': BI_pb2.Currency.Name(ap.cost.currency), + 'amount_consumed': ap.amountConsumed, + } + for fp in rs.filePlans: + if fp.id in file_map: + code = file_map[fp.id] + pricing['mc_file_plans'][code] = { + 'amount': fp.cost.amount, + 'unit': BI_pb2.Cost.AmountPer.Name(fp.cost.amountPer), + 'currency': BI_pb2.Currency.Name(fp.cost.currency), + } + return pricing + + +def _find_managed_company( + enterprise_data: enterprise_types.IEnterpriseData, + name_or_id: Union[int, str], +) -> Optional[enterprise_types.ManagedCompany]: + if isinstance(name_or_id, int): + return enterprise_data.managed_companies.get_entity(name_or_id) + key = name_or_id.lower() + for mc in enterprise_data.managed_companies.get_all_entities(): + if mc.mc_enterprise_name.lower() == key: + return mc + return None + + +def _parse_managed_company_filter(mc: Optional[str]) -> Optional[Union[int, str]]: + if mc is None or (isinstance(mc, str) and not mc.strip()): + return None + s = mc.strip() + if s.isdigit(): + return int(s) + return s + + +def _first_msp_permits(enterprise_data: enterprise_types.IEnterpriseData) -> Optional[enterprise_types.MspPermits]: + for lic in enterprise_data.licenses.get_all_entities(): + if lic.msp_permits is not None: + return lic.msp_permits + return None + + +def _lookup_msp_product_plan(plan: str) -> Optional[Tuple[Any, ...]]: + plan_name = plan.strip().lower() + return next((x for x in enterprise_constants.MSP_PLANS if x[1].lower() == plan_name), None) + + +def _lookup_msp_file_plan_row(file_plan: str) -> Optional[Tuple[Any, ...]]: + fp_name = file_plan.strip().lower() + return next( + ( + x + for x in enterprise_constants.MSP_FILE_PLANS + if fp_name in (str(y).lower() for y in x if isinstance(y, str)) + ), + None, + ) + + +def _msp_info_restriction_report(enterprise_data: enterprise_types.IEnterpriseData) -> MspInfoReport: + permits = _first_msp_permits(enterprise_data) + if not permits: + return MspInfoReport(headers=(), rows=(), message=_MSG_NO_MC_RESTRICTIONS) + all_products = {x[1].lower(): x[2] for x in enterprise_constants.MSP_PLANS} + all_addons = {x[0].lower(): x[3] for x in enterprise_constants.MSP_ADDONS} + all_file_plans = {x[1].lower(): x[2] for x in enterprise_constants.MSP_FILE_PLANS} + max_file_plan = permits.max_file_plan_type + allowed_products = permits.allowed_mc_products or [] + allowed_addons = permits.allowed_add_ons or [] + table = [ + ('Allow Unlimited Licenses', permits.allow_unlimited_licenses), + ('Allowed Products', [x + f' ({all_products.get(x.lower(), "")})' for x in allowed_products]), + ('Allowed Add-Ons', [x + f' ({all_addons.get(x.lower(), "")})' for x in allowed_addons]), + ('Max File Storage plan', all_file_plans.get(max_file_plan.lower(), max_file_plan)), + ] + return MspInfoReport(headers=('permit_name', 'value'), rows=tuple((a, b) for a, b in table)) + + +def _msp_info_pricing_report(auth: keeper_auth.KeeperAuth) -> MspInfoReport: + pricing_data = _fetch_mc_pricing(auth) + header = ('category', 'name', 'code', 'price') + rows: List[Tuple[Any, ...]] = [] + base_plans = pricing_data.get('mc_base_plans') or {} + for plan in enterprise_constants.MSP_PLANS: + code = plan[1] + if code in base_plans: + rows.append(('Product', plan[2], code, _price_text(base_plans[code]))) + addons = pricing_data.get('mc_addons') or {} + for addon in enterprise_constants.MSP_ADDONS: + code = addon[0] + if code in addons: + rows.append(('Addon', addon[1], code, _price_text(addons[code]))) + fplans = pricing_data.get('mc_file_plans') or {} + for fp in enterprise_constants.MSP_FILE_PLANS: + plan_code = fp[1] + if plan_code in fplans: + rows.append(('File Plan', fp[2], plan_code, _price_text(fplans[plan_code]))) + return MspInfoReport(headers=header, rows=tuple(rows)) + + +def _resolve_managed_companies_for_info( + enterprise_data: enterprise_types.IEnterpriseData, + managed_company: Optional[str], +) -> List[enterprise_types.ManagedCompany]: + mcs = list(enterprise_data.managed_companies.get_all_entities()) + flt = _parse_managed_company_filter(managed_company) + if flt is None: + return mcs + mc_one = _find_managed_company(enterprise_data, flt) + if mc_one is None: + raise errors.KeeperError(f'Managed Company "{managed_company}" not found') + return [mc_one] + + +def _addon_display_strings_for_mc_row( + mc: enterprise_types.ManagedCompany, + *, + verbose: bool, +) -> List[str]: + addon_list: List[str] = [] + if not mc.add_ons: + return addon_list + for addon_obj in mc.add_ons: + addon_name = addon_obj.name + if not verbose: + addon_list.append(addon_name) + continue + seats = addon_obj.seats + if seats and seats > 0: + addon_def = next((x for x in enterprise_constants.MSP_ADDONS if x[0] == addon_name), None) + if addon_def and addon_def[2]: + display_seats = -1 if seats == _INT32_MAX else seats + addon_list.append(f'{addon_name}:{display_seats}') + else: + addon_list.append(addon_name) + else: + addon_list.append(addon_name) + return addon_list + + +def _msp_info_managed_companies_report( + enterprise_data: enterprise_types.IEnterpriseData, + mcs: List[enterprise_types.ManagedCompany], + *, + verbose: bool, +) -> MspInfoReport: + sort_dict = {x[0]: i for i, x in enumerate(enterprise_constants.MSP_ADDONS)} + plan_map = {x[1]: x[2] for x in enterprise_constants.MSP_PLANS} + file_plan_map = {x[1]: x[2] for x in enterprise_constants.MSP_FILE_PLANS} + header = ['company_id', 'company_name', 'node', 'plan', 'storage', 'addons', 'allocated', 'active'] + if verbose: + header.insert(3, 'node_name') + + table_rows: List[Tuple[Any, ...]] = [] + for mc in mcs: + node_id = mc.msp_node_id + if verbose: + node_path = str(node_id) + node_name = _node_path(enterprise_data, node_id, omit_root=False) + else: + node_path = _node_path(enterprise_data, node_id, omit_root=False) + node_name = None + + file_plan_label = file_plan_map.get(mc.file_plan_type, mc.file_plan_type) + addon_list = _addon_display_strings_for_mc_row(mc, verbose=verbose) + addon_list.sort(key=lambda x: sort_dict.get(x.split(':')[0], -1)) + addons: Any = addon_list if verbose else len(addon_list) + + plan = mc.product_id + if not verbose: + plan = plan_map.get(plan, plan) + + seats = mc.number_of_seats + if seats > _SEATS_UNLIMITED_THRESHOLD: + seats = -1 + + users = mc.number_of_users or 0 + if verbose: + table_rows.append(( + mc.mc_enterprise_id, mc.mc_enterprise_name, node_path, node_name, plan, file_plan_label, addons, seats, users, + )) + else: + table_rows.append(( + mc.mc_enterprise_id, mc.mc_enterprise_name, node_path, plan, file_plan_label, addons, seats, users, + )) + + table_rows.sort(key=lambda x: str(x[1]).lower()) + return MspInfoReport(headers=tuple(header), rows=tuple(table_rows), row_numbers=True) + + +def msp_info( + loader: enterprise_types.IEnterpriseLoader, + *, + restriction: bool = False, + pricing: bool = False, + managed_company: Optional[str] = None, + verbose: bool = False, +) -> MspInfoReport: + """Build MSP information reports.""" + enterprise_data = loader.enterprise_data + auth = loader.keeper_auth + + if restriction: + return _msp_info_restriction_report(enterprise_data) + if pricing: + return _msp_info_pricing_report(auth) + + mcs = _resolve_managed_companies_for_info(enterprise_data, managed_company) + if len(mcs) == 0: + return MspInfoReport(headers=(), rows=(), message=_MSG_NO_MANAGED_COMPANIES) + return _msp_info_managed_companies_report(enterprise_data, mcs, verbose=verbose) + + +def _new_mc_encrypted_registration_fields(mc_tree_key: bytes, msp_tree_key: bytes) -> Dict[str, Any]: + role_json = json.dumps({_JSON_KEY_DISPLAYNAME: _DISPLAYNAME_KEEPER_ADMIN}).encode() + root_json = json.dumps({_JSON_KEY_DISPLAYNAME: _DISPLAYNAME_ROOT}).encode() + return { + 'encrypted_tree_key': utils.base64_url_encode(crypto.encrypt_aes_v2(mc_tree_key, msp_tree_key)), + 'role_data': utils.base64_url_encode(crypto.encrypt_aes_v1(role_json, mc_tree_key)), + 'root_node': utils.base64_url_encode(crypto.encrypt_aes_v1(root_json, mc_tree_key)), + } + + +def _registration_seat_cap(seats: Optional[int], permits: Optional[enterprise_types.MspPermits]) -> int: + seat_val = 0 if seats is None else int(seats) + if seat_val < 0: + if permits and not permits.allow_unlimited_licenses: + raise errors.KeeperError('Managed Company unlimited licences are not allowed') + return _INT32_MAX + return seat_val + + +def _assert_mc_product_plan_allowed( + plan_label: str, + plan_name_lower: str, + permits: Optional[enterprise_types.MspPermits], +) -> None: + if not permits or permits.allowed_mc_products is None: + return + allowed_products = permits.allowed_mc_products + if len(allowed_products) == 0 or not any(x.lower() == plan_name_lower for x in allowed_products): + raise errors.KeeperError(f'Managed Company plan "{plan_label}" is not allowed') + + +def _assert_file_plan_allowed_by_permits( + fp_row: Tuple[Any, ...], + permits: Optional[enterprise_types.MspPermits], +) -> None: + if not permits or not permits.max_file_plan_type: + return + allowed_fp = next( + (x for x in enterprise_constants.MSP_FILE_PLANS if permits.max_file_plan_type.lower() == x[1].lower()), + None, + ) + if allowed_fp and allowed_fp[0] < fp_row[0]: + raise errors.KeeperError(f'Managed Company file storage "{fp_row[2]}" is not allowed') + + +def _merge_optional_file_plan_into_registration_rq( + rq: Dict[str, Any], + file_plan: Optional[str], + product_plan: Tuple[Any, ...], + permits: Optional[enterprise_types.MspPermits], +) -> None: + if not file_plan: + return + fp_row = _lookup_msp_file_plan_row(file_plan) + if not fp_row: + raise errors.KeeperError(f'File plan "{file_plan}" is not found') + if product_plan[3] < fp_row[0]: + rq['file_plan_type'] = fp_row[1] + _assert_file_plan_allowed_by_permits(fp_row, permits) + + +def _addon_seats_from_spec_line( + addon_name: str, + *, + has_seat_token: bool, + seat_part: str, + spec: Tuple[Any, ...], + seat_label_for_errors: str, +) -> int: + if not (has_seat_token and spec[2]): + return 0 + sp = seat_part.strip() + if addon_name == _KEPM_ADDON and sp == '-1': + return _INT32_MAX + try: + addon_seats = int(sp) + except ValueError: + raise errors.KeeperError( + f'Addon "{addon_name}". Number of seats "{seat_label_for_errors}" is not integer') from None + if addon_name == _KEPM_ADDON: + if addon_seats not in _KEPM_VALID_SEATS and addon_seats != _INT32_MAX: + valid_values = ', '.join(str(x) for x in sorted(_KEPM_VALID_SEATS)) + ', -1 (for unlimited)' + raise errors.KeeperError( + f'Addon "{addon_name}". Invalid seat value "{seat_label_for_errors}". Valid values are: {valid_values}') + return addon_seats + + +def _registration_addon_lines_to_rq( + rq: Dict[str, Any], + addon_lines: List[str], + permits: Optional[enterprise_types.MspPermits], +) -> Dict[str, int]: + addon_data: Dict[str, int] = {} + rq['add_ons'] = [] + for line in addon_lines: + addon_name, sep, seat_part = line.partition(':') + addon_name = addon_name.lower().strip() + spec = next((x for x in enterprise_constants.MSP_ADDONS if x[0] == addon_name), None) + if spec is None: + raise errors.KeeperError(f'Addon "{addon_name}" is not found') + if permits and permits.allowed_add_ons is not None and len(permits.allowed_add_ons) > 0: + if addon_name not in (x.lower() for x in permits.allowed_add_ons): + raise errors.KeeperError(f'Managed Company add-on "{addon_name}" is not allowed') + addon_seats = _addon_seats_from_spec_line( + addon_name, + has_seat_token=(sep == ':' and bool(spec[2])), + seat_part=seat_part, + spec=spec, + seat_label_for_errors=seat_part, + ) + rqa: Dict[str, Any] = {'add_on': spec[0]} + if addon_seats > 0: + rqa['seats'] = addon_seats + addon_data[addon_name] = addon_seats + else: + addon_data[addon_name] = 0 + rq['add_ons'].append(rqa) + return addon_data + + +def _assert_remote_browser_requires_connection_manager_seats(addon_name_to_seats: Dict[str, int]) -> None: + if _REMOTE_BROWSER_ISOLATION_ADDON not in addon_name_to_seats: + return + if _CONNECTION_MANAGER_ADDON not in addon_name_to_seats: + raise errors.KeeperError( + f'Addon "{_REMOTE_BROWSER_ISOLATION_ADDON}" requires "{_CONNECTION_MANAGER_ADDON}" to be selected') + if addon_name_to_seats.get(_CONNECTION_MANAGER_ADDON, 0) == 0: + raise errors.KeeperError( + f'Addon "{_REMOTE_BROWSER_ISOLATION_ADDON}" requires "{_CONNECTION_MANAGER_ADDON}" ' + f'to have seats specified (e.g. {_CONNECTION_MANAGER_ADDON}:N)') + + +def _assert_remote_browser_requires_connection_manager_update(addons: Dict[str, Dict[str, Any]]) -> None: + addon_keys = {k.lower() for k in addons} + if _REMOTE_BROWSER_ISOLATION_ADDON not in addon_keys: + return + if _CONNECTION_MANAGER_ADDON not in addon_keys: + raise errors.KeeperError( + f'Addon "{_REMOTE_BROWSER_ISOLATION_ADDON}" requires "{_CONNECTION_MANAGER_ADDON}" to be selected') + cm_addon = addons.get(_CONNECTION_MANAGER_ADDON) + if cm_addon: + cm_seats = int(cm_addon.get('seats', 0) or 0) + if cm_seats == 0: + raise errors.KeeperError( + f'Addon "{_REMOTE_BROWSER_ISOLATION_ADDON}" requires "{_CONNECTION_MANAGER_ADDON}" ' + f'to have seats specified (e.g. {_CONNECTION_MANAGER_ADDON}:N)') + + +def msp_add_managed_company( + loader: enterprise_types.IEnterpriseLoader, + *, + enterprise_name: str, + plan: str, + node_id: int, + seats: Optional[int] = None, + file_plan: Optional[str] = None, + addons: Optional[List[str]] = None, +) -> int: + """Register a new managed company. + + :param loader: MSP enterprise loader. + :param enterprise_name: Display name for the new managed company. + :param plan: Product plan code (e.g. ``business``, ``enterprise``); must match :data:`enterprise_constants.MSP_PLANS`. + :param node_id: Enterprise node id under which the MC is created. + :param seats: Seat cap; use ``-1`` or ``None`` with unlimited permits for unlimited (signed 32-bit max, server-side). + :param file_plan: Optional storage plan code (e.g. ``STORAGE_100GB``). + :param addons: Optional ``ADDON`` or ``ADDON:SEATS`` strings. + :return: New managed company enterprise id. + """ + enterprise_data = loader.enterprise_data + auth = loader.keeper_auth + msp_tree_key = enterprise_data.enterprise_info.tree_key + + for mc in enterprise_data.managed_companies.get_all_entities(): + if mc.mc_enterprise_name.lower() == enterprise_name.strip().lower(): + raise errors.KeeperError(f"Managed company '{enterprise_name}' already exists") + + permits = _first_msp_permits(enterprise_data) + plan_name = plan.strip().lower() + product_plan = _lookup_msp_product_plan(plan) + if not product_plan: + raise errors.KeeperError(f'Managed Company plan "{plan}" is not found') + _assert_mc_product_plan_allowed(plan, plan_name, permits) + + seat_val = _registration_seat_cap(seats, permits) + mc_tree_key = utils.generate_aes_key() + rq: Dict[str, Any] = { + 'command': _CMD_ENTERPRISE_REGISTRATION_BY_MSP, + 'node_id': node_id, + 'product_id': product_plan[1], + 'seats': seat_val, + 'enterprise_name': enterprise_name.strip(), + **_new_mc_encrypted_registration_fields(mc_tree_key, msp_tree_key), + } + + _merge_optional_file_plan_into_registration_rq(rq, file_plan, product_plan, permits) + + if addons: + addon_data = _registration_addon_lines_to_rq(rq, addons, permits) + _assert_remote_browser_requires_connection_manager_seats(addon_data) + + rs = auth.execute_auth_command(rq) + company_id = int(rs.get('enterprise_id', -1)) + if company_id < 0: + raise errors.KeeperError('Managed company registration did not return an enterprise id') + msp_down(loader, reset=False) + return company_id + + +def _selectable_addons_from_managed_company( + current: enterprise_types.ManagedCompany, +) -> Dict[str, Dict[str, Any]]: + addons: Dict[str, Dict[str, Any]] = {} + if not current.add_ons: + return addons + for ao in current.add_ons: + if not ao.enabled or ao.included_in_product: + continue + entry: Dict[str, Any] = {'add_on': ao.name} + if ao.seats and ao.seats > 0: + entry['seats'] = ao.seats + addons[ao.name.lower()] = entry + return addons + + +def _apply_update_rq_plan( + rq: Dict[str, Any], + plan: str, + permits: Optional[enterprise_types.MspPermits], +) -> None: + plan_name = plan.strip().lower() + product_plan = _lookup_msp_product_plan(plan) + if not product_plan: + raise errors.KeeperError(f'Managed Company plan "{plan}" is not found') + _assert_mc_product_plan_allowed(plan, plan_name, permits) + rq['product_id'] = product_plan[1] + + +def _apply_update_rq_seats( + rq: Dict[str, Any], + seats: int, + permits: Optional[enterprise_types.MspPermits], +) -> None: + if seats < 0: + if permits and not permits.allow_unlimited_licenses: + raise errors.KeeperError('Managed Company unlimited licences are not allowed') + rq['seats'] = _INT32_MAX + else: + rq['seats'] = seats + + +def _apply_file_plan_fields_to_mc_update_rq( + rq: Dict[str, Any], + file_plan: Optional[str], + current: enterprise_types.ManagedCompany, + permits: Optional[enterprise_types.MspPermits], +) -> None: + if file_plan is not None: + fp_row = _lookup_msp_file_plan_row(file_plan) + if not fp_row: + raise errors.KeeperError(f'File plan "{file_plan}" is not found') + _assert_file_plan_allowed_by_permits(fp_row, permits) + product_id = str(rq['product_id']).lower() + product_plan = next((x for x in enterprise_constants.MSP_PLANS if product_id == x[1].lower()), None) + if product_plan and product_plan[3] < fp_row[0]: + rq['file_plan_type'] = fp_row[1] + return + existing_file_plan = current.file_plan_type + if not existing_file_plan: + return + product_id = str(rq['product_id']).lower() + product_plan = next((x for x in enterprise_constants.MSP_PLANS if product_id == x[1].lower()), None) + if not product_plan: + return + fp_existing = next((x for x in enterprise_constants.MSP_FILE_PLANS if x[1] == existing_file_plan), None) + if fp_existing and fp_existing[0] != product_plan[3]: + rq['file_plan_type'] = existing_file_plan + + +def _apply_update_addon_mutations( + addons: Dict[str, Dict[str, Any]], + *, + add_lines: List[str], + remove_lines: List[str], + permits: Optional[enterprise_types.MspPermits], +) -> None: + for aon in add_lines: + addon_name, sep, seat_str = aon.partition(':') + addon_name = addon_name.lower().strip() + spec = next((x for x in enterprise_constants.MSP_ADDONS if x[0] == addon_name), None) + if spec is None: + raise errors.KeeperError(f'Addon "{addon_name}" is not found') + addon_seats = _addon_seats_from_spec_line( + addon_name, + has_seat_token=(sep == ':' and bool(spec[2])), + seat_part=seat_str, + spec=spec, + seat_label_for_errors=seat_str, + ) + if permits and permits.allowed_add_ons is not None and len(permits.allowed_add_ons) > 0: + if addon_name not in (x.lower() for x in permits.allowed_add_ons): + raise errors.KeeperError(f'Managed Company add-on "{addon_name}" is not allowed') + add_entry: Dict[str, Any] = {'add_on': spec[0]} + if addon_seats > 0: + add_entry['seats'] = addon_seats + addons[addon_name] = add_entry + + for aon in remove_lines: + addon_name = aon.strip().lower() + spec = next((x for x in enterprise_constants.MSP_ADDONS if x[0] == addon_name), None) + if spec is None: + raise errors.KeeperError(f'Addon "{addon_name}" is not found') + if addon_name in addons: + del addons[addon_name] + + +def msp_update_managed_company( + loader: enterprise_types.IEnterpriseLoader, + *, + managed_company: str, + node_id: Optional[int] = None, + new_name: Optional[str] = None, + plan: Optional[str] = None, + seats: Optional[int] = None, + file_plan: Optional[str] = None, + add_addons: Optional[List[str]] = None, + remove_addons: Optional[List[str]] = None, +) -> int: + """Update an existing managed company. + + :param managed_company: Managed company name or numeric enterprise id. + :param node_id: When set, moves the MC under this enterprise node id. + :param new_name: New display name for the managed company. + :param plan: New product plan code (``MSP_PLANS`` code, e.g. ``business``). + :param seats: New seat cap; ``-1`` for unlimited when permitted. + :param file_plan: Storage plan code or label (same matching rules as ``msp_add_managed_company``). + :param add_addons: ``ADDON`` or ``ADDON:SEATS`` entries to add or replace. + :param remove_addons: Addon codes to remove. + :return: Managed company enterprise id (from the API response). + """ + enterprise_data = loader.enterprise_data + auth = loader.keeper_auth + + flt = _parse_managed_company_filter(managed_company) + if flt is None: + raise errors.KeeperError('Managed company name or id is required') + current = _find_managed_company(enterprise_data, flt) + if current is None: + raise errors.KeeperError(f'Managed Company "{managed_company}" not found') + + rq: Dict[str, Any] = { + 'command': _CMD_ENTERPRISE_UPDATE_BY_MSP, + 'enterprise_id': current.mc_enterprise_id, + 'enterprise_name': current.mc_enterprise_name, + 'product_id': current.product_id, + 'seats': current.number_of_seats, + } + + if node_id is not None: + rq['node_id'] = node_id + + if new_name: + rq['enterprise_name'] = new_name.strip() + + permits = _first_msp_permits(enterprise_data) + + if plan is not None: + _apply_update_rq_plan(rq, plan, permits) + + if isinstance(seats, int): + _apply_update_rq_seats(rq, seats, permits) + + _apply_file_plan_fields_to_mc_update_rq(rq, file_plan, current, permits) + + addons = _selectable_addons_from_managed_company(current) + add_list = add_addons if isinstance(add_addons, list) else [] + remove_list = remove_addons if isinstance(remove_addons, list) else [] + _apply_update_addon_mutations(addons, add_lines=add_list, remove_lines=remove_list, permits=permits) + _assert_remote_browser_requires_connection_manager_update(addons) + + rq['add_ons'] = list(addons.values()) + rs = auth.execute_auth_command(rq) + eid = int(rs.get('enterprise_id', current.mc_enterprise_id)) + msp_down(loader, reset=False) + return eid + + +def msp_remove_managed_company( + loader: enterprise_types.IEnterpriseLoader, + *, + managed_company: str, +) -> int: + """Remove a managed company tenant. + + :param managed_company: Managed company name or numeric enterprise id. + :return: The removed managed company enterprise id. + """ + enterprise_data = loader.enterprise_data + auth = loader.keeper_auth + + flt = _parse_managed_company_filter(managed_company) + if flt is None: + raise errors.KeeperError('Managed Company name or id is required') + current = _find_managed_company(enterprise_data, flt) + if current is None: + raise errors.KeeperError(f'Managed Company "{managed_company}" not found') + + rq: Dict[str, Any] = { + 'command': _CMD_ENTERPRISE_REMOVE_BY_MSP, + 'enterprise_id': current.mc_enterprise_id, + } + auth.execute_auth_command(rq) + eid = current.mc_enterprise_id + msp_down(loader, reset=False) + return eid + + +def _node_json_bytes_for_reencrypt(node: enterprise_types.Node, msp_tree_key: bytes) -> bytes: + if node.encrypted_data: + try: + payload = _decrypt_encrypted_data(node.encrypted_data, _ENCRYPTED_BY_DATA_KEY, msp_tree_key) + except Exception: + payload = {_JSON_KEY_DISPLAYNAME: node.name or ''} + else: + payload = {_JSON_KEY_DISPLAYNAME: node.name or ''} + return json.dumps(payload).encode('utf-8') + + +def _role_json_bytes_for_reencrypt(role: enterprise_types.Role, msp_tree_key: bytes) -> bytes: + if role.key_type == _KEY_TYPE_NO_KEY: + return json.dumps({_JSON_KEY_DISPLAYNAME: role.name or ''}).encode('utf-8') + if role.encrypted_data: + try: + payload = _decrypt_encrypted_data(role.encrypted_data, role.key_type, msp_tree_key) + except Exception: + payload = {_JSON_KEY_DISPLAYNAME: role.name or ''} + else: + payload = {_JSON_KEY_DISPLAYNAME: role.name or ''} + return json.dumps(payload).encode('utf-8') + + +def _user_reencrypt_payload(user: enterprise_types.User, msp_tree_key: bytes) -> Union[str, bytes]: + """Returns either plaintext display name (no_key) or UTF-8 JSON bytes to encrypt with MC tree key.""" + if user.key_type == _KEY_TYPE_NO_KEY: + if user.encrypted_data: + return str(user.encrypted_data) + return str(user.full_name or _UNKNOWN_USER_LABEL) + if user.encrypted_data: + try: + payload = _decrypt_encrypted_data(user.encrypted_data, user.key_type, msp_tree_key) + except Exception: + payload = {_JSON_KEY_DISPLAYNAME: user.full_name or ''} + else: + payload = {_JSON_KEY_DISPLAYNAME: user.full_name or ''} + return json.dumps(payload).encode('utf-8') + + +def _collect_convert_node_subtree(enterprise_data: enterprise_types.IEnterpriseData, msp_node_id: int) -> Tuple[List[int], Set[int], Set[int], Set[str], Set[int]]: + node_tree: Dict[int, Set[int]] = {} + for node in enterprise_data.nodes.get_all_entities(): + nid = node.node_id + pid = node.parent_id + if isinstance(pid, int) and isinstance(nid, int): + if pid not in node_tree: + node_tree[pid] = set() + node_tree[pid].add(nid) + all_subnodes: List[int] = [msp_node_id] + pos = 0 + while pos < len(all_subnodes): + nid = all_subnodes[pos] + pos += 1 + if nid in node_tree: + all_subnodes.extend(node_tree[nid]) + nodes_to_move = set(all_subnodes) + roles_to_move = {r.role_id for r in enterprise_data.roles.get_all_entities() if r.node_id in nodes_to_move} + teams_to_move = {t.team_uid for t in enterprise_data.teams.get_all_entities() if t.node_id in nodes_to_move} + users_to_move = {u.enterprise_user_id for u in enterprise_data.users.get_all_entities() if u.node_id in nodes_to_move} + return all_subnodes, nodes_to_move, roles_to_move, teams_to_move, users_to_move + + +def _validate_msp_convert_node( + enterprise_data: enterprise_types.IEnterpriseData, + *, + nodes_to_move: Set[int], + roles_to_move: Set[int], + teams_to_move: Set[str], + users_to_move: Set[int], +) -> List[str]: + messages: List[str] = [] + role_lookup = {r.role_id: r for r in enterprise_data.roles.get_all_entities()} + team_lookup = {t.team_uid: t for t in enterprise_data.teams.get_all_entities()} + user_lookup = {u.enterprise_user_id: u for u in enterprise_data.users.get_all_entities()} + + def node_path(nid: int) -> str: + return _node_path(enterprise_data, nid, omit_root=False) + + for bridge in enterprise_data.bridges.get_all_entities(): + if bridge.node_id in nodes_to_move: + messages.append( + f'Remove bridge provisioning before conversion from node {node_path(bridge.node_id)}') + for scim in enterprise_data.scims.get_all_entities(): + if scim.node_id in nodes_to_move: + messages.append( + f'Remove SCIM provisioning before conversion from node {node_path(scim.node_id)}') + for sso in enterprise_data.sso_services.get_all_entities(): + if sso.node_id in nodes_to_move: + messages.append( + f'Remove SSO provisioning before conversion from node {node_path(sso.node_id)}') + for email in enterprise_data.email_provision.get_all_entities(): + if email.node_id in nodes_to_move: + messages.append( + f'Remove email provisioning before conversion from node {node_path(email.node_id)}') + for mc in enterprise_data.managed_companies.get_all_entities(): + if mc.msp_node_id in nodes_to_move: + messages.append( + f'Remove managed company before conversion from node {node_path(mc.msp_node_id)}') + for qt in enterprise_data.queued_teams.get_all_entities(): + if qt.node_id in nodes_to_move: + messages.append( + f'Remove queued team {qt.name} before conversion from node {node_path(qt.node_id)}') + + for uid in users_to_move: + user = user_lookup.get(uid) + if user and user.status == 'invited': + messages.append(f'Pending user {user.username} must be removed') + + for ru in enterprise_data.role_users.get_all_links(): + move_user = ru.enterprise_user_id in users_to_move + move_role = ru.role_id in roles_to_move + if move_role != move_user: + user = user_lookup.get(ru.enterprise_user_id) + username = (user.username if user else '') or str(ru.enterprise_user_id) + role = role_lookup.get(ru.role_id) + rolename = (role.name if role else '') or str(ru.role_id) + messages.append(f'Conflicting role membership: User: {username}, Role: {rolename}') + + for rt in enterprise_data.role_teams.get_all_links(): + move_team = rt.team_uid in teams_to_move + move_role = rt.role_id in roles_to_move + if move_role != move_team: + team = team_lookup.get(rt.team_uid) + teamname = (team.name if team else rt.team_uid) or rt.team_uid + role = role_lookup.get(rt.role_id) + rolename = (role.name if role else '') or str(rt.role_id) + messages.append(f'Conflicting role membership: Team: {teamname}, Role: {rolename}') + + for tu in enterprise_data.team_users.get_all_links(): + move_user = tu.enterprise_user_id in users_to_move + move_team = tu.team_uid in teams_to_move + if move_team != move_user: + user = user_lookup.get(tu.enterprise_user_id) + username = (user.username if user else '') or str(tu.enterprise_user_id) + team = team_lookup.get(tu.team_uid) + teamname = (team.name if team else '') or tu.team_uid + messages.append(f'Conflicting team membership: User: {username}, Team: {teamname}') + + for mn in enterprise_data.managed_nodes.get_all_links(): + move_role = mn.role_id in roles_to_move + move_node = mn.managed_node_id in nodes_to_move + if move_role != move_node: + role = role_lookup.get(mn.role_id) + rolename = (role.name if role else '') or str(mn.role_id) + nodename = node_path(mn.managed_node_id) + messages.append(f'Conflicting admin role management: Node: {nodename}, Role: {rolename}') + + return messages + + +def msp_convert_node( + loader: enterprise_types.IEnterpriseLoader, + *, + node_id: int, + seats: Optional[int] = None, + plan: Optional[str] = None, +) -> int: + """Convert an MSP enterprise subtree rooted at ``node_id`` into a managed company. + + Validates the subtree (no bridges, SCIM, SSO on affected nodes, no straddling role/team links, etc.), + optionally registers a new managed company, logs into the MC, re-encrypts node/role/user/team keys for + the MC tree key, calls ``enterprise/node_to_managed_company``, then refreshes MSP data. + + :param loader: MSP enterprise loader. + :param node_id: Root of the subtree to convert (must not be the enterprise root). + :param seats: Seat count for a newly registered MC; adjusted upward to user count and defaulted. + :param plan: Product plan code when registering a new MC (default ``business``). + :return: Managed company enterprise id. + """ + enterprise_data = loader.enterprise_data + auth = loader.keeper_auth + msp_tree_key = enterprise_data.enterprise_info.tree_key + root_id = enterprise_data.root_node.node_id + + if node_id == root_id: + raise errors.KeeperError('Root node cannot be converted') + + all_subnodes, nodes_to_move, roles_to_move, teams_to_move, users_to_move = _collect_convert_node_subtree( + enterprise_data, node_id) + + err_list = _validate_msp_convert_node( + enterprise_data, + nodes_to_move=nodes_to_move, + roles_to_move=roles_to_move, + teams_to_move=teams_to_move, + users_to_move=users_to_move, + ) + if err_list: + raise errors.KeeperError('\n'.join(err_list)) + + msp_node = enterprise_data.nodes.get_entity(node_id) + if not msp_node: + raise errors.KeeperError(f'Node id {node_id} not found') + msp_node_name = (msp_node.name or '').strip() + if not msp_node_name: + raise errors.KeeperError('Node has no display name; cannot convert to a managed company') + + seat_val = 0 if seats is None else int(seats) + if seat_val < len(users_to_move): + seat_val = len(users_to_move) + if seat_val == 0: + seat_val = 1 + + plan_label = plan or _DEFAULT_CONVERT_PLAN + plan_name = plan_label.strip().lower() + product_plan = _lookup_msp_product_plan(plan_label) + if not product_plan: + raise errors.KeeperError(f'Managed Company plan "{plan_name}" is not found') + + permits = _first_msp_permits(enterprise_data) + _assert_mc_product_plan_allowed(plan_label, plan_name, permits) + + if seat_val < 0: + if permits and not permits.allow_unlimited_licenses: + raise errors.KeeperError('Managed Company unlimited licences are not allowed') + seat_val = _INT32_MAX + + mc_existing = next( + (x for x in enterprise_data.managed_companies.get_all_entities() if x.mc_enterprise_name == msp_node_name), + None, + ) + mc_id: int + + if mc_existing is None: + new_mc_tree_key = utils.generate_aes_key() + rq: Dict[str, Any] = { + 'command': _CMD_ENTERPRISE_REGISTRATION_BY_MSP, + 'node_id': root_id, + 'seats': seat_val, + 'product_id': product_plan[1], + 'enterprise_name': msp_node_name, + **_new_mc_encrypted_registration_fields(new_mc_tree_key, msp_tree_key), + } + rs = auth.execute_auth_command(rq) + mc_id = int(rs.get('enterprise_id', -1)) + if mc_id < 0: + raise errors.KeeperError('Managed company registration did not return an enterprise id') + else: + mc_id = mc_existing.mc_enterprise_id + + mc_auth, mc_tree_key_login = login_to_managed_company(loader, mc_id) + mc_tree_key = mc_tree_key_login + + mc_rq = enterprise_pb2.NodeToManagedCompanyRequest() + mc_rq.companyId = mc_id + + for nid in all_subnodes: + node = enterprise_data.nodes.get_entity(nid) + if not node: + continue + red = enterprise_pb2.ReEncryptedData() + red.id = nid + raw = _node_json_bytes_for_reencrypt(node, msp_tree_key) + red.data = utils.base64_url_encode(crypto.encrypt_aes_v1(raw, mc_tree_key)) + mc_rq.nodes.append(red) + + for role_id in roles_to_move: + role = enterprise_data.roles.get_entity(role_id) + if not role: + continue + red = enterprise_pb2.ReEncryptedData() + red.id = role_id + raw = _role_json_bytes_for_reencrypt(role, msp_tree_key) + red.data = utils.base64_url_encode(crypto.encrypt_aes_v1(raw, mc_tree_key)) + mc_rq.roles.append(red) + + for user_id in users_to_move: + user = enterprise_data.users.get_entity(user_id) + if not user: + continue + red = enterprise_pb2.ReEncryptedData() + red.id = user_id + payload = _user_reencrypt_payload(user, msp_tree_key) + if isinstance(payload, str): + red.data = payload + else: + red.data = utils.base64_url_encode(crypto.encrypt_aes_v1(payload, mc_tree_key)) + mc_rq.users.append(red) + + admin_role_ids = {mn.role_id for mn in enterprise_data.managed_nodes.get_all_links() if mn.role_id in roles_to_move} + if admin_role_ids: + loader.load_role_keys(admin_role_ids) + for rid in sorted(admin_role_ids): + role_key = loader.get_role_keys(rid) + if role_key: + rerk = enterprise_pb2.ReEncryptedRoleKey() + rerk.role_id = rid + rerk.encryptedRoleKey = crypto.encrypt_aes_v2(role_key, mc_tree_key) + mc_rq.roleKeys.append(rerk) + + for team_uid in teams_to_move: + team = enterprise_data.teams.get_entity(team_uid) + if not team: + continue + etkr = enterprise_pb2.EncryptedTeamKeyRequest() + etkr.teamUid = utils.base64_url_decode(team.team_uid) + if team.encrypted_team_key: + team_key = crypto.decrypt_aes_v2(team.encrypted_team_key, msp_tree_key) + etkr.encryptedTeamKey = crypto.encrypt_aes_v2(team_key, mc_tree_key) + else: + etkr.force = True + mc_rq.teamKeys.append(etkr) + + dk_rq = APIRequest_pb2.UserDataKeyByNodeRequest() + dk_rq.nodeIds.extend(nodes_to_move) + dk_rs = auth.execute_auth_rest( + _REST_USER_DATA_KEY_BY_NODE, + dk_rq, + response_type=enterprise_pb2.EnterpriseUserDataKeysByNodeResponse, + ) + + msp_ec_private = enterprise_data.enterprise_info.ec_private_key + if dk_rs and len(dk_rs.keys) > 0 and msp_ec_private: + mc_pk_rs = mc_auth.execute_auth_rest( + _REST_MC_PUBLIC_KEY, + None, + response_type=breachwatch_pb2.EnterprisePublicKeyResponse, + ) + if mc_pk_rs and mc_pk_rs.enterpriseECCPublicKey: + mc_public_key = crypto.load_ec_public_key(mc_pk_rs.enterpriseECCPublicKey) + for dk_node in dk_rs.keys: + for dk in dk_node.keys: + if dk.keyTypeId == _USER_DATA_KEY_TYPE_ID_ECC and dk.enterpriseUserId in users_to_move: + encrypted_key = crypto.decrypt_ec(dk.userEncryptedDataKey, msp_ec_private) + encrypted_key = crypto.encrypt_ec(encrypted_key, mc_public_key) + re_dk = enterprise_pb2.ReEncryptedUserDataKey() + re_dk.enterpriseUserId = dk.enterpriseUserId + re_dk.userEncryptedDataKey = encrypted_key + mc_rq.usersDataKeys.append(re_dk) + + auth.execute_auth_rest(_REST_NODE_TO_MANAGED_COMPANY, mc_rq, response_type=None) + msp_down(loader, reset=False) + return mc_id From 81e43b28510a29ef0016d80d765a374c68a5ef97 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 29 Apr 2026 10:42:53 +0530 Subject: [PATCH 13/23] KeeperPAM Commands Added --- examples/sdk_examples/records/list_records.py | 1 - .../biometric/commands/update_name.py | 5 +- .../src/keepercli/commands/enterprise_user.py | 29 +- .../keepercli/commands/pam/debug/__init__.py | 17 + .../keepercli/commands/pam/debug/debug_acl.py | 131 ++ .../commands/pam/debug/debug_gateway.py | 100 + .../commands/pam/debug/debug_graph.py | 671 ++++++ .../commands/pam/debug/debug_info.py | 532 +++++ .../commands/pam/debug/debug_link.py | 67 + .../keepercli/commands/pam/debug/debug_rs.py | 216 ++ .../commands/pam/debug/debug_vertex.py | 199 ++ .../commands/pam/discovery/__init__.py | 375 +++ .../commands/pam/discovery/discover.py | 2052 +++++++++++++++++ .../commands/pam/discovery/rule_commands.py | 432 ++++ .../src/keepercli/commands/pam/keeper_pam.py | 207 +- .../src/keepercli/commands/pam/pam_config.py | 1155 ++++++++++ .../keepercli/commands/pam/pam_connection.py | 274 +++ .../src/keepercli/commands/pam/pam_dto.py | 158 ++ .../commands/pam/pam_gateway_action.py | 521 +++++ .../src/keepercli/commands/pam/pam_rbi.py | 416 ++++ .../keepercli/commands/pam/pam_rotation.py | 1388 +++++++++++ .../keepercli/commands/pam/saas/__init__.py | 345 +++ .../commands/pam/saas/saas_commands.py | 1089 +++++++++ .../commands/pam/service/__init__.py | 18 + .../commands/pam/service/service_commands.py | 354 +++ .../src/keepercli/commands/secrets_manager.py | 5 +- .../src/keepercli/commands/shares.py | 2 - .../src/keepercli/helpers/email_utils.py | 1364 +++++++++++ .../src/keepercli/helpers/gateway_utils.py | 26 +- .../src/keepercli/helpers/record_utils.py | 36 +- .../src/keepercli/helpers/router_utils.py | 469 +++- keepercli-package/src/keepercli/params.py | 34 + keepersdk-package/requirements.txt | 1 + .../src/keepersdk/helpers/config_utils.py | 39 + .../keepersdk/helpers/keeper_dag/__init__.py | 302 +++ .../helpers/keeper_dag/__version__.py | 1 + .../helpers/keeper_dag/connection.py | 210 ++ .../helpers/keeper_dag/connection/__init__.py | 319 +++ .../keeper_dag/connection/commander.py | 206 ++ .../helpers/keeper_dag/connection/local.py | 600 +++++ .../keepersdk/helpers/keeper_dag/constants.py | 53 + .../src/keepersdk/helpers/keeper_dag/dag.py | 1289 +++++++++++ .../helpers/keeper_dag/dag_crypto.py | 52 + .../keepersdk/helpers/keeper_dag/dag_edge.py | 266 +++ .../keepersdk/helpers/keeper_dag/dag_sort.py | 117 + .../keepersdk/helpers/keeper_dag/dag_types.py | 787 +++++++ .../keepersdk/helpers/keeper_dag/dag_utils.py | 111 + .../helpers/keeper_dag/dag_vertex.py | 809 +++++++ .../helpers/keeper_dag/exceptions.py | 80 + .../helpers/keeper_dag/infrastructure.py | 333 +++ .../src/keepersdk/helpers/keeper_dag/jobs.py | 480 ++++ .../keepersdk/helpers/keeper_dag/process.py | 1367 +++++++++++ .../helpers/keeper_dag/record_link.py | 463 ++++ .../src/keepersdk/helpers/keeper_dag/rule.py | 368 +++ .../helpers/keeper_dag/struct/__init__.py | 56 + .../helpers/keeper_dag/struct/default.py | 80 + .../helpers/keeper_dag/struct/protobuf.py | 184 ++ .../helpers/keeper_dag/user_service.py | 626 +++++ .../keepersdk/helpers/pam_config_facade.py | 97 + .../helpers/pam_user_record_facade.py | 123 + .../keepersdk/helpers/tunnel/tunnel_graph.py | 609 +++++ .../keepersdk/helpers/tunnel/tunnel_utils.py | 83 + .../src/keepersdk/plugins/pam/__init__.py | 0 .../src/keepersdk/plugins/pam/pam_plugin.py | 149 ++ .../src/keepersdk/plugins/pam/pam_storage.py | 127 + .../src/keepersdk/plugins/pam/pam_types.py | 42 + .../src/keepersdk/proto/APIRequest_pb2.py | 520 ++--- .../src/keepersdk/proto/APIRequest_pb2.pyi | 59 +- .../src/keepersdk/proto/AccountSummary_pb2.py | 80 +- .../keepersdk/proto/AccountSummary_pb2.pyi | 35 +- .../src/keepersdk/proto/BI_pb2.py | 114 +- .../src/keepersdk/proto/BI_pb2.pyi | 222 +- .../src/keepersdk/proto/GraphSync_pb2.py | 8 +- .../src/keepersdk/proto/GraphSync_pb2.pyi | 5 +- .../keepersdk/proto/NotificationCenter_pb2.py | 78 +- .../proto/NotificationCenter_pb2.pyi | 57 +- .../src/keepersdk/proto/SyncDown_pb2.py | 8 +- .../src/keepersdk/proto/SyncDown_pb2.pyi | 29 +- .../src/keepersdk/proto/automator_pb2.py | 144 +- .../src/keepersdk/proto/automator_pb2.pyi | 63 +- .../src/keepersdk/proto/breachwatch_pb2.py | 12 +- .../src/keepersdk/proto/breachwatch_pb2.pyi | 13 +- .../src/keepersdk/proto/client_pb2.py | 12 +- .../src/keepersdk/proto/client_pb2.pyi | 5 +- .../src/keepersdk/proto/enterprise_pb2.py | 608 ++--- .../src/keepersdk/proto/enterprise_pb2.pyi | 160 +- .../src/keepersdk/proto/folder_pb2.py | 8 +- .../src/keepersdk/proto/folder_pb2.pyi | 13 +- .../src/keepersdk/proto/pam_pb2.py | 82 +- .../src/keepersdk/proto/pam_pb2.pyi | 279 ++- .../src/keepersdk/proto/pedm_pb2.py | 236 +- .../src/keepersdk/proto/pedm_pb2.pyi | 73 +- .../src/keepersdk/proto/push_pb2.py | 12 +- .../src/keepersdk/proto/push_pb2.pyi | 3 +- .../src/keepersdk/proto/record_pb2.py | 8 +- .../src/keepersdk/proto/record_pb2.pyi | 29 +- .../src/keepersdk/proto/router_pb2.py | 152 +- .../src/keepersdk/proto/router_pb2.pyi | 70 +- .../src/keepersdk/proto/ssocloud_pb2.py | 28 +- .../src/keepersdk/proto/ssocloud_pb2.pyi | 25 +- .../src/keepersdk/proto/version_pb2.py | 12 +- keepersdk-package/src/keepersdk/utils.py | 85 + .../src/keepersdk/vault/record_management.py | 4 +- .../src/keepersdk/vault/vault_data.py | 2 +- .../src/keepersdk/vault/vault_utils.py | 17 + 105 files changed, 25208 insertions(+), 1279 deletions(-) create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/__init__.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_acl.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_gateway.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_graph.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_info.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_link.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_rs.py create mode 100644 keepercli-package/src/keepercli/commands/pam/debug/debug_vertex.py create mode 100644 keepercli-package/src/keepercli/commands/pam/discovery/__init__.py create mode 100644 keepercli-package/src/keepercli/commands/pam/discovery/discover.py create mode 100644 keepercli-package/src/keepercli/commands/pam/discovery/rule_commands.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_config.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_connection.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_dto.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_gateway_action.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_rbi.py create mode 100644 keepercli-package/src/keepercli/commands/pam/pam_rotation.py create mode 100644 keepercli-package/src/keepercli/commands/pam/saas/__init__.py create mode 100644 keepercli-package/src/keepercli/commands/pam/saas/saas_commands.py create mode 100644 keepercli-package/src/keepercli/commands/pam/service/__init__.py create mode 100644 keepercli-package/src/keepercli/commands/pam/service/service_commands.py create mode 100644 keepercli-package/src/keepercli/helpers/email_utils.py create mode 100644 keepersdk-package/src/keepersdk/helpers/config_utils.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/__init__.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/connection.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/__init__.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/commander.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/local.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_crypto.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_edge.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_sort.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_types.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_utils.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_vertex.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/exceptions.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/infrastructure.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/jobs.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/process.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/record_link.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/rule.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/__init__.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/default.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/protobuf.py create mode 100644 keepersdk-package/src/keepersdk/helpers/keeper_dag/user_service.py create mode 100644 keepersdk-package/src/keepersdk/helpers/pam_config_facade.py create mode 100644 keepersdk-package/src/keepersdk/helpers/pam_user_record_facade.py create mode 100644 keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py create mode 100644 keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_utils.py create mode 100644 keepersdk-package/src/keepersdk/plugins/pam/__init__.py create mode 100644 keepersdk-package/src/keepersdk/plugins/pam/pam_plugin.py create mode 100644 keepersdk-package/src/keepersdk/plugins/pam/pam_storage.py create mode 100644 keepersdk-package/src/keepersdk/plugins/pam/pam_types.py diff --git a/examples/sdk_examples/records/list_records.py b/examples/sdk_examples/records/list_records.py index 1679d3bf..e65183f4 100644 --- a/examples/sdk_examples/records/list_records.py +++ b/examples/sdk_examples/records/list_records.py @@ -542,4 +542,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/keepercli-package/src/keepercli/biometric/commands/update_name.py b/keepercli-package/src/keepercli/biometric/commands/update_name.py index c353dd46..db674387 100644 --- a/keepercli-package/src/keepercli/biometric/commands/update_name.py +++ b/keepercli-package/src/keepercli/biometric/commands/update_name.py @@ -23,9 +23,6 @@ def __init__(self): parser = argparse.ArgumentParser(prog='biometric update-name', description='Update friendly name of a biometric passkey') super().__init__(parser) - # def get_parser(self): - # return self.parser - def execute(self, context: KeeperParams, **kwargs): """Execute biometric update-name command""" def _update_name(): @@ -147,4 +144,4 @@ def _report_update_results(self, result, credential, new_name): 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 + print("=" * 30) diff --git a/keepercli-package/src/keepercli/commands/enterprise_user.py b/keepercli-package/src/keepercli/commands/enterprise_user.py index 67f08cef..b6a2044b 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_user.py +++ b/keepercli-package/src/keepercli/commands/enterprise_user.py @@ -1054,22 +1054,21 @@ def _get_ecc_data_keys(self, context: KeeperParams, user_ids: Set[int]) -> Dict[ 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=enterprise_pb2.EnterpriseUserDataKeys) + response_type=APIRequest_pb2.EnterpriseUserIdDataKeyPair) - 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) + enc_data_key = data_key_rs.encryptedDataKey + 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[data_key_rs.enterpriseUserId] = data_key + except Exception as e: + logger.debug(e) return data_keys diff --git a/keepercli-package/src/keepercli/commands/pam/debug/__init__.py b/keepercli-package/src/keepercli/commands/pam/debug/__init__.py new file mode 100644 index 00000000..d6f6ac91 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations +from keepersdk.helpers.keeper_dag.dag_utils import value_to_boolean +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ....params import KeeperParams + from keepersdk.helpers.keeper_dag.connection import ConnectionBase + + +def get_connection(context: KeeperParams) -> ConnectionBase: + if value_to_boolean(os.environ.get("USE_LOCAL_DAG", False)) is False: + from keepersdk.helpers.keeper_dag.connection.commander import Connection as CommanderConnection + return CommanderConnection(context=context) + else: + from keepersdk.helpers.keeper_dag.connection.local import Connection as LocalConnection + return LocalConnection() \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_acl.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_acl.py new file mode 100644 index 00000000..5439b675 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_acl.py @@ -0,0 +1,131 @@ +import argparse + +from ....params import KeeperParams +from .... import prompt_utils +from keepersdk.helpers.keeper_dag.constants import PAM_USER +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.dag_types import UserAcl +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api + +logger = api.get_logger() + +class PAMDebugACLCommand(PAMGatewayActionDiscoverCommandBase): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug acl') + PAMDebugACLCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID.') + parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='User UID.') + parser.add_argument('--parent-uid', '-r', required=True, dest='parent_uid', action='store', + help='Resource or Configuration UID.') + parser.add_argument('--debug-gs-level', required=False, dest='debug_level', action='store', + help='GraphSync debug level. Default is 0', type=int, default=0) + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + user_uid = kwargs.get("user_uid") + parent_uid = kwargs.get("parent_uid") + debug_level = int(kwargs.get("debug_level", 0)) + + logger.info("") + + configuration_uid = kwargs.get('configuration_uid') + + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + record_link = RecordLink(record=gateway_context.configuration, vault=context.vault, fail_on_corrupt=False) + user_record = context.vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + logger.info(f"The user record is {user_record.title}") + + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User record.") + return + + parent_record = context.vault.vault_data.get_record(parent_uid) + if parent_record is None: + logger.error(f"The parent record does not exists.") + return + + logger.info(f"The parent record is {parent_record.title}") + + if parent_record.record_type.startswith("pam") is False: + logger.error(f"The parent record is not a PAM record.") + return + + if parent_record.record_type == PAM_USER: + logger.error(f"The parent record cannot be a PAM User record.") + return + + parent_is_config = parent_record.record_type.endswith("Configuration") + + acl_exists = True + acl = record_link.get_acl(user_uid, parent_uid) + if acl is None: + logger.info("No existing ACL, creating an ACL.") + acl = UserAcl() + acl_exists = False + + if parent_is_config is True: + logger.info("Is an IAM user.") + acl.is_iam_user = True + + rl_parent_vertex = record_link.dag.get_vertex(parent_uid) + if rl_parent_vertex is None: + logger.info("Parent record linking vertex did not exists, creating one.") + rl_parent_vertex = record_link.dag.add_vertex(parent_uid) + + rl_user_vertex = record_link.dag.get_vertex(user_uid) + if rl_user_vertex is None: + logger.info("User record linking vertex did not exists, creating one.") + rl_user_vertex = record_link.dag.add_vertex(user_uid) + + has_admin_uid = record_link.get_admin_record_uid(parent_uid) + if has_admin_uid is not None: + logger.info("Parent record already has an admin.") + else: + logger.info("Parent record does not have an admin.") + + belongs_to_vertex = record_link.acl_has_belong_to_record_uid(user_uid) + if belongs_to_vertex is None: + logger.info("User record does not belong to any resource, or provider.") + else: + if not belongs_to_vertex.active: + logger.info("User record belongs to an inactive parent.") + else: + logger.info("User record belongs to another record.") + + logger.info("") + + acl.belongs_to = prompt_utils.user_choice( + f"Does this user belong to {parent_record.title}", 'yn', default='n' + ).lower() == 'y' + + if has_admin_uid is None: + acl.is_admin = prompt_utils.user_choice( + f"Is this user the admin of {parent_record.title}", 'yn', default='n' + ).lower() == 'y' + + try: + record_link.belongs_to(user_uid, parent_uid, acl=acl) + record_link.save() + logger.info(f"Updated/added ACL between {user_record.title} and " + f"{parent_record.title}") + except Exception as err: + logger.error(f"Could not update ACL: {err}") diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_gateway.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_gateway.py new file mode 100644 index 00000000..bf856c64 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_gateway.py @@ -0,0 +1,100 @@ +import argparse + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure +from keepersdk.helpers.keeper_dag.user_service import UserService +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api +from .debug_graph import PAMDebugGraphCommand + +logger = api.get_logger() + +class PAMDebugGatewayCommand(PAMGatewayActionDiscoverCommandBase): + + type_name_map = { + PAM_USER: "PAM User", + PAM_MACHINE: "PAM Machine", + PAM_DATABASE: "PAM Database", + PAM_DIRECTORY: "PAM Directory", + } + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug gateway') + PAMDebugGatewayCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + debug_level = kwargs.get("debug_level", False) + + configuration_uid = kwargs.get('configuration_uid') + vault = context.vault + + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + infra = Infrastructure(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False) + infra.load() + + record_link = RecordLink(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False) + user_service = UserService(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False) + + if gateway_context is None: + logger.error(f"Cannot get gateway information. Gateway may not be up.") + return + + logger.info("") + logger.info(f"Gateway Information") + logger.info(f" Gateway UID: {gateway_context.gateway_uid}") + logger.info(f" Gateway Name: {gateway_context.gateway_name}") + if gateway_context.configuration is not None: + record_key = vault.vault_data.get_record_key(gateway_context.configuration_uid) + logger.info(f" Configuration UID: {gateway_context.configuration_uid}") + logger.info(f" Configuration Title: {gateway_context.configuration.title}") + logger.info(f" Configuration Key Bytes Hex: {record_key.hex()}") + else: + logger.error(f"The gateway appears to not have a configuration.") + logger.info("") + + graph = PAMDebugGraphCommand() + + if infra.dag.has_graph is True: + logger.info(f"Infrastructure Graph") + graph.do_list(context=context, gateway_context=gateway_context, graph_type="infra", debug_level=debug_level, + indent=1) + else: + logger.error(f"The gateway configuration does not have a infrastructure graph.") + + logger.info("") + + if record_link.dag.has_graph is True: + logger.info(f"Record Linking Graph") + graph.do_list(context=context, gateway_context=gateway_context, graph_type="rl", debug_level=debug_level, + indent=1) + else: + logger.error(f"The gateway configuration does not have a record linking graph.") + + logger.info("") + + if user_service.dag.has_graph is True: + logger.info(f"User to Service/Task Graph") + graph.do_list(context=context, gateway_context=gateway_context, graph_type="service", debug_level=debug_level, + indent=1) + else: + logger.error(f"The gateway configuration does not have a user to service/task graph.") + + logger.info("") diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_graph.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_graph.py new file mode 100644 index 00000000..aa8efcd9 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_graph.py @@ -0,0 +1,671 @@ +import argparse +import logging +from typing import Optional + +from keepersdk.helpers.keeper_dag.jobs import Jobs +from keepersdk.helpers.keeper_dag.process import VERTICES_SORT_MAP, DiscoveryObject + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure +from keepersdk.helpers.keeper_dag.user_service import UserService +from keepersdk.helpers.keeper_dag.dag_types import DiscoveryUser, DiscoveryDirectory, DiscoveryMachine, DiscoveryDatabase, JobContent +from keepersdk.helpers.keeper_dag.dag import DAGVertex, DAG +from keepersdk.helpers.keeper_dag.constants import DIS_INFRA_GRAPH_ID, RECORD_LINK_GRAPH_ID, USER_SERVICE_GRAPH_ID, DIS_JOBS_GRAPH_ID +from keepersdk.helpers.keeper_dag.dag_sort import sort_infra_vertices +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api +from . import get_connection + +logger = api.get_logger() + +class PAMDebugGraphCommand(PAMGatewayActionDiscoverCommandBase): + + NO_RECORD = "NO RECORD" + OTHER = "OTHER" + + mapping = { + PAM_USER: {"order": 1, "sort": "_sort_name", "item": DiscoveryUser, "key": "user"}, + PAM_DIRECTORY: {"order": 1, "sort": "_sort_name", "item": DiscoveryDirectory, "key": "host_port"}, + PAM_MACHINE: {"order": 2, "sort": "_sort_host", "item": DiscoveryMachine, "key": "host"}, + PAM_DATABASE: {"order": 3, "sort": "_sort_host", "item": DiscoveryDatabase, "key": "host_port"}, + } + + graph_id_map = { + "infra": DIS_INFRA_GRAPH_ID, + "rl": RECORD_LINK_GRAPH_ID, + "service": USER_SERVICE_GRAPH_ID, + "jobs": DIS_JOBS_GRAPH_ID + } + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug graph') + PAMDebugGraphCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID.') + parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--type', '-t', required=True, choices=['infra', 'rl', 'service', 'jobs'], + dest='graph_type', action='store', help='Graph type', default='infra') + parser.add_argument('--raw', required=False, dest='raw', action='store_true', + help='Render raw graph. Will render corrupt graphs.') + parser.add_argument('--list', required=False, dest='do_text_list', action='store_true', + help='List items in a list.') + parser.add_argument('--render', required=False, dest='do_render', action='store_true', + help='Render a graph') + parser.add_argument('--file', '-f', required=False, dest='filepath', action='store', + default="keeper_graph", help='Base name for the graph file.') + parser.add_argument('--format', required=False, choices=['raw', 'dot', 'twopi', 'patchwork'], + dest='format', default="dot", action='store', help='The format of the graph.') + parser.add_argument('--debug-gs-level', required=False, dest='debug_level', action='store', + help='GraphSync debug level. Default is 0', type=int, default=0) + + def _do_text_list_infra(self, context: KeeperParams, gateway_context: GatewayContext, debug_level: int = 0, + indent: int = 0): + + infra = Infrastructure(record=gateway_context.configuration, context=context, logger=logging, + debug_level=debug_level) + infra.load(sync_point=0) + + try: + configuration = infra.get_root.has_vertices()[0] + except (Exception,): + logger.error(f"Could not find the configuration in the infrastructure graph. " + f"Has discovery been run for this gateway?") + + return + + line_start = { + 0: "", + 1: "* ", + 2: "- ", + } + + def _handle(current_vertex: DAGVertex, indent: int = 0, last_record_type: Optional[str] = None): + + if not current_vertex.active: + return + + pad = "" + if indent > 0: + pad = "".ljust(4 * indent, ' ') + + text = "" + ls = line_start.get(indent, " ") + + if not current_vertex.active: + text += f"{pad}{current_vertex.uid} (Inactive)" + elif not current_vertex.corrupt: + current_content = DiscoveryObject.get_discovery_object(current_vertex) + if current_content.record_uid is None: + text += f"{pad}{ls}{current_vertex.uid}; {current_content.title} does not have a record." + else: + record = context.vault.vault_data.load_record(current_content.record_uid) + if record is not None: + text += f"{pad}{ls}" + (f"{current_vertex.uid}; {record.title}; {record.record_uid}") + else: + text += f"{pad}{ls}" + (f"{current_vertex.uid}; {current_content.title}; have record uid, record does not exists, might have to sync.") + else: + text += f"{pad}{current_vertex.uid} (Corrupt)" + + logger.info(text) + + record_type_to_vertices_map = sort_infra_vertices(current_vertex) + + for record_type in sorted(record_type_to_vertices_map, key=lambda i: VERTICES_SORT_MAP[i]['order']): + for vertex in record_type_to_vertices_map[record_type]: + if last_record_type is None or last_record_type != record_type: + if indent == 0: + logger.info(f"{pad} {record_type}") + last_record_type = record_type + + _handle(vertex, indent=indent+1) + + logger.info("") + _handle(configuration, indent=indent) + logger.info("") + + def _do_text_list_rl(self, context: KeeperParams, gateway_context: GatewayContext, debug_level: int = 0, + indent: int = 0): + + logger.info("") + + pad = "" + if indent > 0: + pad = "".ljust(4 * indent, ' ') + + record_link = RecordLink(record=gateway_context.configuration, + context=context, + logger=logging, + debug_level=debug_level) + configuration = record_link.dag.get_root + + record = context.vault.vault_data.get_record(record_uid=configuration.uid) + if record is None: + logger.error(f"Configuration record does not exists.") + return + + logger.info(f"{pad}{record.record_type}, {record.title}, {record.record_uid}") + + if configuration.has_data: + try: + data = configuration.content_as_dict + logger.info(f"{pad} . data") + for k, v in data.items(): + logger.info(f"{pad} + {k} = {v}") + except Exception as err: + logger.error(f"{pad} ! data not JSON: {err}") + + def _group(configuration_vertex: DAGVertex) -> dict: + + group = { + PAM_USER: [], + PAM_DIRECTORY: [], + PAM_DATABASE: [], + PAM_MACHINE: [], + PAMDebugGraphCommand.NO_RECORD: [], + PAMDebugGraphCommand.OTHER: [] + } + + for vertex in configuration_vertex.has_vertices(): + record = context.vault.vault_data.get_record(record_uid=vertex.uid) + if record is None: + group[PAMDebugGraphCommand.NO_RECORD].append({ + "v": vertex + }) + continue + rt = record.record_type + if rt not in group: + rt = PAMDebugGraphCommand.OTHER + group[rt].append({ + "v": vertex, + "r": record + }) + + return group + + group = _group(configuration) + + for record_type in [PAM_USER, PAM_DIRECTORY, PAM_MACHINE, PAM_DATABASE]: + if len(group[record_type]) > 0: + logger.info(f"{pad} {record_type}") + for item in group[record_type]: + vertex = item.get("v") + record = item.get("r") + text = f"{record.title}; {record.record_uid}" + if not vertex.active: + text += " Inactive" + logger.info(f"{pad} * {text}") + + if record_type == PAM_USER: + acl = record_link.get_acl(vertex.uid, configuration.uid) + if acl is None: + logger.info(f"{pad} missing ACL") + else: + if acl.is_iam_user: + logger.info(f"{pad} . is IAM user") + if acl.is_admin: + logger.info(f"{pad} . is the Admin") + if acl.belongs_to: + logger.info(f"{pad} . belongs to this resource") + else: + logger.info(f"{pad} . looks like directory user") + + if acl.rotation_settings: + if acl.rotation_settings.noop: + logger.info(f"{pad} . is a NOOP") + if acl.rotation_settings.disabled: + logger.info(f"{pad} . rotation is disabled") + + if (acl.rotation_settings.saas_record_uid_list is not None + and len(acl.rotation_settings.saas_record_uid_list) > 0): + logger.info(f"{pad} . has SaaS rotation: " + f"{acl.rotation_settings.saas_record_uid_list[0]}") + + continue + + if vertex.has_data: + try: + data = vertex.content_as_dict + logger.info(f"{pad} . data") + for k, v in data.items(): + logger.info(f"{pad} + {k} = {v}") + except Exception as err: + logger.error(f"{pad} ! data not JSON: {err}") + + children = vertex.has_vertices() + if len(children) > 0: + bad = [] + for child in children: + child_record = context.vault.vault_data.load_record(record_uid=child.uid) + if child_record is None: + if child.active: + bad.append(f"- Record UID {child.uid} does not exists.") + continue + else: + logger.info(f"{pad} - {child_record.title}; {child_record.record_uid}") + acl = record_link.get_acl(child.uid, vertex.uid) + if acl is None: + logger.info(f"{pad} missing ACL") + else: + if acl.is_admin: + logger.info(f"{pad} . is the Admin") + if acl.belongs_to: + logger.info(f"{pad} . belongs to this resource") + else: + logger.info(f"{pad} . looks like directory user") + + if child.has_data: + try: + data = child.content_as_dict + logger.info(f"{pad} . data") + for k, v in data.items(): + logger.info(f"{pad} + {k} = {v}") + except Exception as err: + logger.info(f"{pad} ! data not JSON: {err}") + for i in bad: + logger.error(f"{pad} {i}") + + if len(group[PAMDebugGraphCommand.OTHER]) > 0: + logger.info(f"{pad} Other PAM Types") + for item in group[PAMDebugGraphCommand.OTHER]: + vertex = item.get("v") + record = item.get("r") + text = f"{record.record_type}; {record.title}; {record.record_uid}" + if not vertex.active: + text += " Inactive" + logger.info(f"{pad} * {text}") + + if len(group[PAMDebugGraphCommand.NO_RECORD]) > 0: + + logger.info(f"{pad} In Graph, No Vault Record") + for item in group[PAMDebugGraphCommand.NO_RECORD]: + vertex = item.get("v") + logger.info(f"{pad} * {vertex.uid}") + + def _do_text_list_service(self, context: KeeperParams, gateway_context: GatewayContext, debug_level: int = 0, + indent: int = 0): + + user_service = UserService(record=gateway_context.configuration, context=context, logger=logging, + debug_level=debug_level) + configuration = user_service.dag.get_root + + def _handle(current_vertex: DAGVertex, parent_vertex: Optional[DAGVertex] = None, indent: int = 0): + + pad = "" + if indent > 0: + pad = "".ljust(2 * indent, ' ') + "* " + + record = context.vault.vault_data.get_record(record_uid=current_vertex.uid) + if record is None: + if not current_vertex.active: + logger.info(f"{pad}Record {current_vertex.uid} does not exists, inactive in the graph.") + else: + logger.info(f"{pad}Record {current_vertex.uid} does not exists, active in the graph.") + return + elif not current_vertex.active: + logger.info(f"{pad}{record.record_type}, {record.title}, {record.record_uid} exists, " + "inactive in the graph.") + return + + acl_text = "" + if parent_vertex is not None: + acl = user_service.get_acl(resource_uid=parent_vertex.uid, user_uid=current_vertex.uid) + if acl is not None: + acl_text = "No Services" + acl_parts = [] + if acl.is_service: + acl_parts.append("Service") + if acl.is_task: + acl_parts.append("Task") + if acl.is_iis_pool: + acl_parts.append("Task") + if len(acl_parts) > 0: + acl_text = ", ".join(acl_parts) + acl_text = f" -> {acl_text}" + + logger.info(f"{pad}{record.record_type}, {record.title}, {record.record_uid}{acl_text}") + + for vertex in current_vertex.has_vertices(): + _handle(current_vertex=vertex, parent_vertex=current_vertex, indent=indent+1) + + _handle(current_vertex=configuration, parent_vertex=None, indent=indent) + + def _do_text_list_jobs(self, context: KeeperParams, gateway_context: GatewayContext, debug_level: int = 0, + indent: int = 0): + + infra = Infrastructure(record=gateway_context.configuration, context=context, logger=logging, + debug_level=debug_level, fail_on_corrupt=False) + infra.load(sync_point=0) + + pad = "" + if indent > 0: + pad = "".ljust(2 * indent, ' ') + "* " + + conn = get_connection(context=context) + graph_sync = DAG(conn=conn, record=gateway_context.configuration, logger=logging, debug_level=debug_level, + graph_id=DIS_JOBS_GRAPH_ID) + graph_sync.load(0) + configuration = graph_sync.get_root + vertices = configuration.has_vertices() + if len(vertices) == 0: + logger.error(f"The jobs graph has not been initialized. Only has root vertex.") + return + + vertex = vertices[0] + if not vertex.has_data: + logger.error(f"The job vertex does not contain any data") + return + + current_json = vertex.content_as_str + if current_json is None: + logger.error(f"The current job vertex content is None") + return + + content = JobContent.model_validate_json(current_json) + logger.info(f"{pad}Active Job ID: {content.active_job_id}") + logger.info("") + logger.info(f"{pad}History") + logger.info("") + for job in content.job_history: + logger.info(f"{pad} --------------------------------------") + logger.info(f"{pad} Job Id: {job.job_id}") + logger.info(f"{pad} Started: {job.start_ts_str}") + logger.info(f"{pad} Ended: {job.end_ts_str}") + logger.info(f"{pad} Duration: {job.duration_sec_str}") + logger.info(f"{pad} Infra Sync Point: {job.sync_point}") + if job.success: + logger.info(f"{pad} Status: Success") + else: + logger.info(f"{pad} Status: Fail") + if job.error is not None: + logger.info(f"{pad} Error: {job.error}") + + logger.info("") + + if job.delta is None: + logger.error(f"{pad}The job is missing a delta, never finished discovery.") + else: + if len(job.delta.added) > 0: + logger.info(f"{pad} Added") + for added in job.delta.added: + vertex = infra.dag.get_vertex(added.uid) + if vertex is None: + logger.info(f"{pad} * Vertex {added.uid} does not exists.") + else: + if not vertex.active: + logger.info(f"{pad} * Vertex {added.uid} is inactive.") + elif vertex.corrupt: + logger.info(f"{pad} * Vertex {added.uid} is corrupt.") + else: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f"{pad} * {content.description}; Record UID: {content.record_uid}") + logger.info("") + + if len(job.delta.changed) > 0: + logger.info(f"{pad} Changed") + for changed in job.delta.changed: + vertex = infra.dag.get_vertex(changed.uid) + if vertex is None: + logger.info(f"{pad} * Vertex {changed.uid} does not exists.") + else: + if not vertex.active: + logger.info(f"{pad} * Vertex {changed.uid} is inactive.") + elif vertex.corrupt: + logger.info(f"{pad} * Vertex {changed.uid} is corrupt.") + else: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f"{pad} * {content.description}; Record UID: {content.record_uid}") + if changed.changes is not None: + for k, v in changed.changes.items(): + logger.info(f"{pad} {k} = {v}") + logger.info("") + + if len(job.delta.deleted) > 0: + logger.info(f"{pad} Deleted") + for deleted in job.delta.deleted: + logger.info(f"{pad} * Removed vertex {deleted.uid}.") + logger.info("") + + def _do_render_infra(self, context: KeeperParams, gateway_context: GatewayContext, filepath: str, graph_format: str, + debug_level: int = 0): + + infra = Infrastructure(record=gateway_context.configuration, context=context, logger=logging, + debug_level=debug_level) + infra.load(sync_point=0) + + logger.info("") + dot_instance = infra.to_dot( + graph_type=graph_format if graph_format != "raw" else "dot", + show_only_active_vertices=False, + show_only_active_edges=False + ) + if graph_format == "raw": + logger.info(dot_instance) + else: + try: + dot_instance.render(filepath) + logger.info(f"Infrastructure graph rendered to {filepath}") + except Exception as err: + logger.error(f"Could not generate graph: {err}") + raise err + logger.info("") + + def _do_render_rl(self, context: KeeperParams, gateway_context: GatewayContext, filepath: str, graph_format: str, + debug_level: int = 0): + + rl = RecordLink(record=gateway_context.configuration, + context=context, + logger=logging, + debug_level=debug_level) + + logger.info("") + dot_instance = rl.to_dot( + graph_type=graph_format if graph_format != "raw" else "dot", + show_only_active_vertices=False, + show_only_active_edges=False + ) + if graph_format == "raw": + logger.info(dot_instance) + else: + try: + dot_instance.render(filepath) + logger.info(f"Record linking graph rendered to {filepath}") + except Exception as err: + logger.error(f"Could not generate graph: {err}") + raise err + logger.info("") + + def _do_render_service(self, context: KeeperParams, gateway_context: GatewayContext, filepath: str, + graph_format: str, debug_level: int = 0): + + service = UserService(record=gateway_context.configuration, context=context, logger=logging, + debug_level=debug_level) + + logger.info("") + dot_instance = service.to_dot( + graph_type=graph_format if graph_format != "raw" else "dot", + show_only_active_vertices=False, + show_only_active_edges=False + ) + if graph_format == "raw": + logger.info(dot_instance) + else: + try: + dot_instance.render(filepath) + logger.info(f"User service/tasks graph rendered to {filepath}") + except Exception as err: + logger.error(f"Could not generate graph: {err}") + raise err + logger.info("") + + def _do_render_jobs(self, context: KeeperParams, gateway_context: GatewayContext, filepath: str, + graph_format: str, debug_level: int = 0): + + jobs = Jobs(record=gateway_context.configuration, context=context, logger=logging, debug_level=debug_level) + + logger.info("") + dot_instance = jobs.dag.to_dot() + if graph_format == "raw": + logger.info(dot_instance) + else: + try: + dot_instance.render(filepath) + logger.info(f"Job graph rendered to {filepath}") + except Exception as err: + logger.error(f"Could not generate graph: {err}") + raise err + logger.info("") + + def _do_raw_text_list(self, context: KeeperParams, gateway_context: GatewayContext, graph_id: int = 0, + debug_level: int = 0): + + logging.debug(f"loading graph id {graph_id}, for record uid {gateway_context.configuration.record_uid}") + + conn = get_connection(context=context) + dag = DAG(conn=conn, record=gateway_context.configuration, graph_id=graph_id, fail_on_corrupt=False, + logger=logging, debug_level=debug_level) + dag.load(sync_point=0) + logger.info("") + if dag.is_corrupt is True: + logger.error(f"The graph is corrupt at Vertex UIDs: {', '.join(dag.corrupt_uids)}") + logger.error("") + + logger.debug("DAG DOT -------------------------------") + logger.debug(str(dag.to_dot())) + logger.debug("DAG DOT -------------------------------") + + line_start = { + 0: "", + 1: "* ", + 2: "- ", + 3: ". ", + } + + def _handle(current_vertex: DAGVertex, last_vertex: Optional[DAGVertex] = None, indent: int = 0): + + pad = "" + if indent > 0: + pad = "".ljust(4 * indent, ' ') + + ls = line_start.get(indent, " ") + text = f"{pad}{ls}{current_vertex.uid}" + + edge_types = [] + if last_vertex is not None: + for edge in current_vertex.edges: + if not edge.active: + continue + if edge.head_uid == last_vertex.uid: + edge_types.append(edge.edge_type.value) + if len(edge_types) > 0: + text += f"; edges: {', '.join(edge_types)}" + + if not current_vertex.active: + text += " Inactive" + if current_vertex.corrupt: + text += " Corrupt" + + logger.info(text) + + if not current_vertex.active: + logger.debug(f"vertex {current_vertex.uid} is not active, will not get children.") + return + + vertices = current_vertex.has_vertices() + if len(vertices) == 0: + logger.debug(f"vertex {current_vertex.uid} does not have any children.") + return + + for vertex in vertices: + _handle(vertex, current_vertex, indent=indent + 1) + + logger.info("") + _handle(dag.get_root) + logger.info("") + + def _do_raw_render_graph(self, context: KeeperParams, gateway_context: GatewayContext, filepath: str, + graph_format: str, graph_id: int = 0, debug_level: int = 0): + + conn = get_connection(context=context) + dag = DAG(conn=conn, record=gateway_context.configuration, graph_id=graph_id, fail_on_corrupt=False, + logger=logging, debug_level=debug_level) + dag.load(sync_point=0) + dot = dag.to_dot(graph_format=graph_format) + if graph_format == "raw": + logger.info(dot) + else: + try: + dot.render(filepath) + logger.info(f"Graph rendered to {filepath}") + except Exception as err: + logger.error(f"Could not generate graph: {err}") + raise err + + logger.info("") + + def do_list(self, context: KeeperParams, gateway_context: GatewayContext, graph_type: str, debug_level: int = 0, + indent: int = 0): + list_func = getattr(self, f"_do_text_list_{graph_type}") + list_func(context=context, + gateway_context=gateway_context, + debug_level=debug_level, + indent=indent) + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + raw = kwargs.get("raw", False) + graph_type = kwargs.get("graph_type") + do_text_list = kwargs.get("do_text_list") + do_render = kwargs.get("do_render") + debug_level = int(kwargs.get("debug_level", 0)) + + configuration_uid = kwargs.get('configuration_uid') + + vault = context.vault + + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + if raw: + if do_text_list: + self._do_raw_text_list(context=context, + gateway_context=gateway_context, + graph_id=PAMDebugGraphCommand.graph_id_map.get(graph_type), + debug_level=debug_level) + if do_render: + filepath = kwargs.get("filepath") + graph_format = kwargs.get("format") + self._do_raw_render_graph(context=context, + gateway_context=gateway_context, + filepath=filepath, + graph_format=graph_format, + graph_id=PAMDebugGraphCommand.graph_id_map.get(graph_type), + debug_level=debug_level) + else: + if do_text_list: + self.do_list( + context=context, + gateway_context=gateway_context, + graph_type=graph_type, + debug_level=debug_level + ) + if do_render: + filepath = kwargs.get("filepath") + graph_format = kwargs.get("format") + render_func = getattr(self, f"_do_render_{graph_type}") + render_func(context=context, + gateway_context=gateway_context, + filepath=filepath, + graph_format=graph_format, + debug_level=debug_level) diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_info.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_info.py new file mode 100644 index 00000000..3e67368b --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_info.py @@ -0,0 +1,532 @@ +import re +import argparse +import time + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.dag_types import UserAcl +from keepersdk.helpers.keeper_dag.dag import EdgeType +from keepersdk.helpers.keeper_dag.dag_types import DiscoveryObject +from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure +from keepersdk.helpers.keeper_dag.user_service import UserService +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api + +logger = api.get_logger() + + +class PAMDebugInfoCommand(PAMGatewayActionDiscoverCommandBase): + + type_name_map = { + PAM_USER: "PAM User", + PAM_MACHINE: "PAM Machine", + PAM_DATABASE: "PAM Database", + PAM_DIRECTORY: "PAM Directory", + } + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug info') + PAMDebugInfoCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--record-uid', '-i', required=True, dest='record_uid', action='store', + help='Keeper PAM record UID.') + + def execute(self, context: KeeperParams, **kwargs): + + record_uid = kwargs.get("record_uid") + vault = context.vault + record = vault.vault_data.get_record(record_uid) + if record is None: + logger.error(f"Record does not exists.") + return + + if record.record_type not in ["pamUser", "pamMachine", "pamDatabase", "pamDirectory"]: + if re.search(r'^pam.+Configuration$', record.record_type) is None: + logger.error(f"The record is a {record.record_type}. This is not a PAM record.") + return + + resource_uid = None + controller_uid = None + + record_rotation = context.get_record_rotation(record_uid) + + if record_rotation is None: + logger.warning(f"PAM record does not have protobuf rotation settings, " + f"checking all configurations.") + + configuration_records = GatewayContext.get_configuration_records(vault=vault) + if len(configuration_records) == 0: + logger.error(f"Cannot find any PAM configuration records in the Vault") + + for configuration_record in configuration_records: + + record_link = RecordLink(record=configuration_record, vault=vault) + record_vertex = record_link.dag.get_vertex(record.record_uid) + if record_vertex is not None and record_vertex.active is True: + controller_uid = configuration_record.record_uid + break + if controller_uid is None: + logger.error(f"Could not find the record in any record linking graph; " + f"checked all configuration records.") + return + + else: + + controller_uid = record_rotation.configuration_uid + if controller_uid is None: + logger.error(f"Record does not have the PAM Configuration set.") + return + + resource_uid = record_rotation.resource_uid + + configuration_record = vault.vault_data.load_record(controller_uid) + record_key = vault.vault_data.get_record_key(controller_uid) + if configuration_record is None: + logger.error(f"The configuration record {controller_uid} does not exist.") + return + + gateway_context = GatewayContext.from_configuration_uid(vault=vault, configuration_uid=controller_uid, gateways=None) + if gateway_context is None: + logger.error(f"Could not find the gateway for configuration record.{controller_uid}") + return + + infra = Infrastructure(record=configuration_record, vault=vault) + infra.load() + record_link = RecordLink(record=configuration_record, vault=vault) + user_service = UserService(record=configuration_record, vault=vault) + + logger.info("") + logger.info(f"Record Information") + logger.info(f" {('Record UID')}: {record_uid}") + logger.info(f" {('Record Title')}: {record.title}") + logger.info(f" {('Record Type')}: {record.record_type}") + logger.info(f" {('Configuration UID')}: {configuration_record.record_uid}") + logger.info(f" {('Configuration Key Bytes Hex')}: {record_key.hex()}") + if resource_uid is not None: + logger.info(f" {('Resource UID')}: {resource_uid}") + + if gateway_context is not None: + logger.info(f" {('Gateway Name')}: {gateway_context.gateway_name}") + logger.info(f" {('Gateway UID')}: {gateway_context.gateway_uid}") + else: + logger.error(f" {('Cannot get gateway information. Gateway may not be up.')}") + logger.info("") + + def _print_field(f): + if f.type == "password": + display_value = f"Password is set" + if f.value == 0 or len(f.value) == 0: + display_value = f"Password IS NOT set" + logger.info(f" * Type: {f.type}, Label: {f.label or 'NO LABEL'}, " + f"Value(s): {display_value}") + elif f.label == "privatePEMKey": + display_value = f"Private Key is set" + if field.value == 0 or len(f.value) == 0: + display_value = f"Private Key IS NOT set" + logger.info(f" * Type: {f.type}, Label: {f.label or 'NO LABEL'}, " + f"Value(s): {display_value}") + elif f.type == "secret": + display_value = f"Secret value is set" + if field.value == 0 or len(f.value) == 0: + display_value = f"Secret value IS NOT set" + logger.info(f" * Type: {f.type}, Label: {f.label or 'NO LABEL'}, " + f"Value(s): {display_value}") + else: + logger.info(f" * Type: {f.type}, Label: {f.label or 'NO LABEL'}, " + f"Value(s): {f.value}") + + logger.info(f"Fields") + logger.info(f" Record Type Fields") + record_data = vault.vault_data.load_record(record_uid) + if record_data.fields is not None and len(record_data.fields) > 0: + for field in record_data.fields: + _print_field(field) + else: + logger.error(f" Record does not have record type fields!") + logger.info("") + logger.info(f" Custom Fields") + if record_data.custom is not None and len(record_data.custom) > 0: + for field in record_data.custom: + _print_field(field) + else: + logger.error(f" Record does not have custom fields.") + logger.info("") + + discovery_vertices = infra.dag.search_content({"record_uid": record.record_uid}) + record_vertex = record_link.dag.get_vertex(record.record_uid) + + if record_vertex is not None: + logger.info(f"Record Linking") + record_parent_vertices = record_vertex.belongs_to_vertices() + logger.info(f" Parent Records") + if len(record_parent_vertices) > 0: + for record_parent_vertex in record_parent_vertices: + + parent_record = vault.vault_data.get_record( + record_parent_vertex.uid) + if parent_record is None: + logger.error(f" * Parent record {record_parent_vertex.uid} " + f"does not exists.") + continue + + acl_edge = record_vertex.get_edge(record_parent_vertex, EdgeType.ACL) + if acl_edge is not None: + acl_content = acl_edge.content_as_object(UserAcl) # type: UserAcl + logger.info(f" * ACL to {parent_record.record_type}; {parent_record.title}; " + f"{record_parent_vertex.uid}") + if acl_content.is_admin: + logger.info(f" . Is Admin") + if acl_content.belongs_to: + logger.info(f" . Belongs") + else: + logger.info(f" . Is Remote user") + + if acl_content.rotation_settings is None: + logger.error(f" . There are no rotation settings!") + else: + if (acl_content.rotation_settings.schedule is None + or acl_content.rotation_settings.schedule == ""): + logger.info(f" . No Schedule") + else: + logger.info(f" . Schedule = {acl_content.rotation_settings.get_schedule()}") + + if (acl_content.rotation_settings.pwd_complexity is None + or acl_content.rotation_settings.pwd_complexity == ""): + logger.info(f" . No Password Complexity") + else: + record_key = vault.vault_data.get_record_key(record_uid) + key_bytes = record_key + logger.info(f" . Password Complexity = " + f"{acl_content.rotation_settings.get_pwd_complexity(key_bytes)}") + logger.info(f" . Disabled = {acl_content.rotation_settings.disabled}") + logger.info(f" . NOOP = {acl_content.rotation_settings.noop}") + logger.info(f" . SaaS Config Records = {acl_content.rotation_settings.saas_record_uid_list}") + + elif record.record_type == PAM_USER: + logger.error(f" * PAM User has NO acl!!!!!!") + + link_edge = record_vertex.get_edge(record_parent_vertex, EdgeType.LINK) + if link_edge is not None: + logger.info(f" * LINK to {parent_record.record_type}; {parent_record.title}; " + f"{record_parent_vertex.uid}") + else: + logger.error(f" Record does not have a parent record.") + logger.info("") + + record_child_vertices = record_vertex.has_vertices() + logger.info(f" Child Records") + if len(record_child_vertices) > 0: + for record_child_vertex in record_child_vertices: + child_record = vault.vault_data.get_record( + record_child_vertex.uid) + + if child_record is None: + logger.error(f" * Child record {record_child_vertex.uid} " + f"does not exists.") + continue + + acl_edge = record_child_vertex.get_edge(record_vertex, EdgeType.ACL) + link_edge = record_child_vertex.get_edge(record_vertex, EdgeType.LINK) + if acl_edge is not None: + acl_content = acl_edge.content_as_object(UserAcl) + logger.info(f" * ACL from {child_record.record_type}; {child_record.title}; " + f"{record_child_vertex.uid}") + if acl_content.is_admin: + logger.info(f" . Is Admin") + if acl_content.belongs_to: + logger.info(f" . Belongs") + else: + logger.info(f" . Is Remote user") + elif link_edge is not None: + logger.info(f" * LINK from {child_record.record_type}; {child_record.title}; " + "{record_child_vertex.uid}") + else: + for edge in record_vertex.edges: + logger.info(f" * {edge.edge_type}?") + + else: + logger.error(f" Record does not have any children.") + logger.info("") + + else: + logger.error(f"Cannot find record in record linking.") + + # Only PAM User and PAM Machine can have services and tasks. + if record.record_type == PAM_USER or record.record_type == PAM_MACHINE: + + user_service_vertex = user_service.dag.get_vertex(record_uid) + + if user_service_vertex is not None: + + if record.record_type == PAM_USER: + + user_results = { + "is_task": [], + "is_service": [] + } + + for us_machine_vertex in user_service.get_resource_vertices(record_uid): + + us_machine_record = ( + vault.vault_data.load_record(us_machine_vertex.uid)) + + acl = user_service.get_acl(us_machine_vertex.uid, user_service_vertex.uid) + for attr in ["is_task", "is_service"]: + value = getattr(acl, attr) + if value is True: + + if us_machine_record is None: + + title = "Unknown" + infra_resource_vertices = infra.dag.search_content( + {"record_uid": us_machine_vertex.uid}) + if len(infra_resource_vertices) > 0: + infra_resource_vertex = infra_resource_vertices[0] + if infra_resource_vertex.has_data is True: + content = DiscoveryObject.get_discovery_object(infra_resource_vertex) + title = content.title + + user_results[attr].append(f" * Record {us_machine_vertex.uid}, " + f"{title} does not exists.") + + else: + user_results[attr].append(f" * {us_machine_record.title}, " + f"{us_machine_vertex.uid}") + + logger.info(f"Service on Machines") + if len(user_results["is_service"]) > 0: + for service in user_results["is_service"]: + logger.info(service) + else: + logger.info(" PAM User is not used for any services.") + logger.info("") + + logger.info(f"Scheduled Tasks on Machines") + if len(user_results["is_task"]) > 0: + for task in user_results["is_task"]: + logger.info(task) + else: + logger.info(" PAM User is not used for any scheduled tasks.") + logger.info("") + + else: + user_results = { + "is_task": [], + "is_service": [] + } + + for us_user_vertex in user_service.get_user_vertices(record_uid): + + us_user_record = vault.vault_data.load_record( + us_user_vertex.uid) + acl = user_service.get_acl(user_service_vertex.uid, us_user_vertex.uid) + for attr in ["is_task", "is_service"]: + value = getattr(acl, attr) + if value is True: + + if us_user_record is None: + + title = "Unknown" + infra_resource_vertices = infra.dag.search_content( + {"record_uid": us_user_vertex.uid}) + if len(infra_resource_vertices) > 0: + infra_resource_vertex = infra_resource_vertices[0] + if infra_resource_vertex.has_data is True: + content = DiscoveryObject.get_discovery_object(infra_resource_vertex) + title = content.title + + user_results[attr].append(f" * Record {us_user_vertex.uid}, " + f"{title} does not exists.") + + else: + user_results[attr].append(f" * {us_user_record.title}, " + f"{us_user_vertex.uid}") + + logger.info(f"Users that are used for Services") + if len(user_results["is_service"]) > 0: + for service in user_results["is_service"]: + logger.info(service) + else: + logger.info(" Machine does not use any non-builtin users for services.") + logger.info("") + + logger.info(f"Users that are used for Scheduled Tasks") + if len(user_results["is_task"]) > 0: + for task in user_results["is_task"]: + logger.info(task) + else: + logger.info(" Machine does not use any non-builtin users for scheduled tasks.") + logger.info("") + else: + logger.error(f"There are no services or schedule tasks associated with this record.") + logger.info("") + try: + if len(discovery_vertices) == 0: + logger.error(f"Could not find any discovery infrastructure vertices for " + f"{record.record_uid}") + elif len(discovery_vertices) > 0: + + if len(discovery_vertices) > 1: + logger.error(f"Found multiple vertices with the record UID of " + f"{record.record_uid}") + for vertex in discovery_vertices: + logger.info(f" * Infrastructure Vertex UID: {vertex.uid}") + logger.info("") + + discovery_vertex = discovery_vertices[0] + content = DiscoveryObject.get_discovery_object(discovery_vertex) + + missing_since = "NA" + if content.missing_since_ts is not None: + missing_since = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(content.missing_since_ts)) + + logger.info(f"Discovery Object Information") + logger.info(f" Vertex UID: {content.uid}") + logger.info(f" Object ID: {content.id}") + logger.info(f" Record UID: {content.record_uid}") + logger.info(f" Parent Record UID: {content.parent_record_uid}") + logger.info(f" Shared Folder UID: {content.shared_folder_uid}") + logger.info(f" Record Type: {content.record_type}") + logger.info(f" Object Type: {content.object_type_value}") + logger.info(f" Ignore Object: {content.ignore_object}") + logger.info(f" Rule Engine Result: {content.action_rules_result}") + logger.info(f" Name: {content.name}") + logger.info(f" Generated Title: {content.title}") + logger.info(f" Generated Description: {content.description}") + logger.info(f" Missing Since: {missing_since}") + logger.info(f" Discovery Notes:") + for note in content.notes: + logger.info(f" * {note}") + if content.error is not None: + logger.error(f" Error: {content.error}") + if content.stacktrace is not None: + logger.error(f" Stack Trace:") + logger.error(f"{content.stacktrace}") + logger.info("") + logger.info(f"Record Type Specifics") + + if record.record_type == PAM_USER: + logger.info(f" User: {content.item.user}") + logger.info(f" DN: {content.item.dn}") + logger.info(f" Database: {content.item.database}") + logger.info(f" Active: {content.item.active}") + logger.info(f" Expired: {content.item.expired}") + logger.info(f" Source: {content.item.source}") + elif record.record_type == PAM_MACHINE: + logger.info(f" Host: {content.item.host}") + logger.info(f" IP: {content.item.ip}") + logger.info(f" Port: {content.item.port}") + logger.info(f" Operating System: {content.item.os}") + logger.info(f" Provider Region: {content.item.provider_region}") + logger.info(f" Provider Group: {content.item.provider_group}") + logger.info(f" Is the Gateway: {content.item.is_gateway}") + logger.info(f" Allows Admin: {content.item.allows_admin}") + logger.info(f" Admin Reason: {content.item.admin_reason}") + logger.info("") + + if content.item.facts.id is not None and content.item.facts.name is not None: + logger.info(f" Machine Name: {content.item.facts.name}") + logger.info(f" Machine ID: {content.item.facts.id.machine_id}") + logger.info(f" Product ID: {content.item.facts.id.product_id}") + logger.info(f" Board Serial: {content.item.facts.id.board_serial}") + logger.info(f" Directories:") + if content.item.facts.directories is not None and len(content.item.facts.directories) > 0: + for directory in content.item.facts.directories: + logger.info(f" * Directory Domain: {directory.domain}") + logger.info(f" Software: {directory.software}") + logger.info(f" Login Format: {directory.login_format}") + else: + logger.info(" Machines is not using any directories.") + + logger.info("") + logger.info(f" Services (Non Builtin Users):") + if len(content.item.facts.services) > 0: + for service in content.item.facts.services: + logger.info(f" * {service.name} = {service.user}") + else: + logger.info(" Machines has no services that are using non-builtin users.") + + logger.info(f" Scheduled Tasks (Non Builtin Users)") + if len(content.item.facts.tasks) > 0: + for task in content.item.facts.tasks: + logger.info(f" * {task.name} = {task.user}") + else: + logger.info(" Machines has no schedules tasks that are using non-builtin users.") + + logger.info(f" IIS Pools (Non Builtin Users)") + if len(content.item.facts.iis_pools) > 0: + for iis_pool in content.item.facts.iis_pools: + logger.info(f" * {iis_pool.name} = {iis_pool.user}") + else: + logger.info(" Machines has no IIS Pools that are using non-builtin users.") + else: + logger.error(f" Machine facts are not set. Discover inside may not have been " + f"performed.") + elif record.record_type == PAM_DATABASE: + logger.info(f" Host: {content.item.host}") + logger.info(f" IP: {content.item.ip}") + logger.info(f" Port: {content.item.port}") + logger.info(f" Database Type: {content.item.type}") + logger.info(f" Database: {content.item.database}") + logger.info(f" Use SSL: {content.item.use_ssl}") + logger.info(f" Provider Region: {content.item.provider_region}") + logger.info(f" Provider Group: {content.item.provider_group}") + logger.info(f" Allows Admin: {content.item.allows_admin}") + logger.info(f" Admin Reason: {content.item.admin_reason}") + elif record.record_type == PAM_DIRECTORY: + logger.info(f" Host: {content.item.host}") + logger.info(f" IP: {content.item.ip}") + logger.info(f" Port: {content.item.port}") + logger.info(f" Directory Type: {content.item.type}") + logger.info(f" Use SSL: {content.item.use_ssl}") + logger.info(f" Provider Region: {content.item.provider_region}") + logger.info(f" Provider Group: {content.item.provider_group}") + logger.info(f" Allows Admin: {content.item.allows_admin}") + logger.info(f" Admin Reason: {content.item.admin_reason}") + else: + for k, v in content.item: + logger.info(f" {k}: {v}") + + if record.version != 6: + logger.info("") + logger.info(f"Belongs To Vertices (Parents)") + vertices = discovery_vertex.belongs_to_vertices() + for vertex in vertices: + try: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {content.description} ({vertex.uid})") + for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]: + edge = discovery_vertex.get_edge(vertex, edge_type=edge_type) + if edge is not None: + logger.info(f" . {edge_type}, active: {edge.active}") + except Exception as err: + logger.error(f"Could not get belongs to information: {err}") + + if len(vertices) == 0: + logger.error(f" Does not belong to anyone") + + print("") + logger.info(f"Vertices Belonging To (Children)") + vertices = discovery_vertex.has_vertices() + for vertex in vertices: + try: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {content.description} ({vertex.uid})") + for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]: + edge = vertex.get_edge(discovery_vertex, edge_type=edge_type) + if edge is not None: + logger.info(f" . {edge_type}, active: {edge.active}") + except Exception as err: + logger.error(f"Could not get belonging to information: {err}") + if len(vertices) == 0: + logger.error(f" Does not have any children.") + + logger.info("") + else: + logger.error(f"Could not find infrastructure vertex.") + except Exception as err: + logger.error(f"Could not get information on infrastructure: {err}") diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_link.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_link.py new file mode 100644 index 00000000..78622ee4 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_link.py @@ -0,0 +1,67 @@ +import argparse + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api + +logger = api.get_logger() + +class PAMDebugLinkCommand(PAMGatewayActionDiscoverCommandBase): + parser = argparse.ArgumentParser(prog='pam action debug link') + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug link') + PAMDebugLinkCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID.') + parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--resource-uid', '-r', required=True, dest='resource_uid', action='store', + help='Resource record UID.') + parser.add_argument('--debug-gs-level', required=False, dest='debug_level', action='store', + help='GraphSync debug level. Default is 0', type=int, default=0) + + def execute(self, context: KeeperParams, **kwargs): + gateway = kwargs.get("gateway") + resource_uid = kwargs.get("resource_uid") + debug_level = int(kwargs.get("debug_level", 0)) + + logger.info("") + + configuration_uid = kwargs.get('configuration_uid') + + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + record_link = RecordLink(record=gateway_context.configuration, + context=context, + logger=logger, + debug_level=debug_level) + + resource_record = context.vault.vault_data.get_record(resource_uid) + if resource_record is None: + logger.error(f"The parent record does not exists.") + return + + if resource_record.record_type not in [PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY]: + logger.error(f"The resource record type, {resource_record.record_type} " + f"is not allowed.") + return + + try: + record_link.belongs_to(resource_uid, gateway_context.configuration_uid, ) + record_link.save() + logger.info(f"Added link between '{resource_uid}' and " + f"{gateway_context.configuration_uid}") + except Exception as err: + logger.error(f"Could not add LINK: {err}") + raise err diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_rs.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_rs.py new file mode 100644 index 00000000..f5d31c43 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_rs.py @@ -0,0 +1,216 @@ +import argparse +import re +from types import SimpleNamespace + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.dag_types import UserAcl, UserAclRotationSettings +from keepersdk.proto import router_pb2 +from ..discovery.__init__ import PAMGatewayActionDiscoverCommandBase +from .... import api +from ....helpers import router_utils +from keepersdk import utils + +logger = api.get_logger() + +class PAMDebugRotationSettingsCommand(PAMGatewayActionDiscoverCommandBase): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug rotation') + PAMDebugRotationSettingsCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--user-record-uid', '-i', required=True, dest='user_record_uid', action='store', + help='PAM user record UID.') + parser.add_argument('--configuration-record-uid', '-c', required=False, + dest='configuration_record_uid', action='store', help='PAM configuration record UID.') + parser.add_argument('--resource-record-uid', '-r', required=False, + dest='resource_record_uid', action='store', help='PAM resource record UID.') + parser.add_argument('--noop', required=False, dest='noop', action='store_true', + help='User is part of a No Operation.') + parser.add_argument('--force', required=False, dest='force', action='store_true', + help='Force reset of the rotation settings.') + parser.add_argument('--dry-run', required=False, dest='dry_run', action='store_true', + help='Do not create or update anything.') + + def execute(self, context: KeeperParams, **kwargs): + user_record_uid = kwargs.get("user_record_uid") + resource_record_uid = kwargs.get("resource_record_uid") + configuration_record_uid = kwargs.get("configuration_record_uid") + noop = kwargs.get("noop", False) + force = kwargs.get("force", False) + dry_run = kwargs.get("dry_run", False) + vault = context.vault + + logger.info("") + + user_record = vault.vault_data.get_record(user_record_uid) + if user_record is None: + logger.error(f"The PAM user record does not exists.") + return + + if user_record.record_type != PAM_USER: + logger.error(f"The PAM user record is a {PAM_USER}. " + f"The record is {user_record.record_type}") + return + + record_rotation = context.get_record_rotation(user_record_uid) + if record_rotation is None: + logger.warning(f"The protobuf rotation settings are missing. Attempting to create.") + + if configuration_record_uid is None: + logger.error(f"Cannot determine PAM configuration, please set the " + f"-c, --configuration-record-uid parameter for this command.") + return + + configuration_record = vault.vault_data.get_record(configuration_record_uid) + if configuration_record is None: + logger.error(f"Configuration record does not exists.") + return + + if re.search(r'^pam.+Configuration$', configuration_record.record_type) is None: + logger.error( + f"The configuration record is not a configuration record. " + f"It's {configuration_record.record_type} record.") + return + + if resource_record_uid is None: + while True: + yn = input("The resource record UID was not set. " + "This user does not belongs to a machine, database, or directory; " + "It's an IAM, Azure, or Domain Controller user? [Y/N]").lower() + if yn == "n": + logger.error(f"Since a resource is needed, please set --resource-record-uid, -r " + f"parameter for the this command.") + return + elif yn == "y": + break + + if resource_record_uid is not None: + + resource_record = vault.vault_data.get_record(resource_record_uid) + if resource_record is None: + logger.error(f"The resource record does not exists.") + return + + if resource_record.record_type not in [PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY]: + logger.error(f"The resource is NOT a " + f"{PAM_MACHINE}, {PAM_DATABASE}, or {PAM_DIRECTORY} record. " + f"It's a {resource_record.record_type}.") + return + + parent_uid = resource_record_uid or configuration_record_uid + + rq = router_pb2.RouterRecordRotationRequest() + rq.recordUid = utils.base64_url_encode(user_record_uid) + rq.revision = 0 + rq.configurationUid = utils.base64_url_encode(configuration_record_uid) + rq.resourceUid = utils.base64_url_encode(parent_uid) + rq.schedule = '' + rq.pwdComplexity = b'' + rq.disabled = False + + if not dry_run: + router_utils.router_set_record_rotation_information(context.vault, rq) + + context.sync_data = True + vault.sync_down() + context.refresh_record_rotations() + + record_rotation = context.get_record_rotation(user_record_uid) + if record_rotation is None: + logger.error(f"Protobuf rotation settings did not create.") + return + else: + logger.info(f"DRY RUN: Would have created the protobuf rotation settings.") + record_rotation = SimpleNamespace( + configuration_uid=configuration_record_uid, + resource_uid=resource_record_uid, + ) + + configuration_record_uid = record_rotation.configuration_uid + if configuration_record_uid is None: + logger.error(f"Record does not have the PAM Configuration set.") + return + + logger.info(f"Configuration Record UID: {configuration_record_uid}") + + configuration_record = vault.vault_data.load_record(configuration_record_uid) + if configuration_record is None: + logger.error(f"Configuration record does not exists.") + return + + resource_record_uid = record_rotation.resource_uid + if resource_record_uid is not None: + + logger.info(f"Resource Record UID: {resource_record_uid}") + + resource_record = vault.vault_data.get_record(resource_record_uid) + if resource_record is None: + logger.error(f"The resource record does not exists.") + return + + if resource_record.record_type not in [PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY]: + logger.error(f"The resource is a {PAM_MACHINE}, {PAM_DATABASE}, or {PAM_DIRECTORY} record. " + f"It's a {resource_record.record_type}.") + return + + record_link = RecordLink(record=configuration_record, context=context) + + parent_uid = resource_record_uid or configuration_record_uid + parent_vertex = record_link.get_record_link(parent_uid) + if parent_vertex is None: + parent_type = "configuration" + if resource_record_uid is not None: + parent_type = "resource" + logger.error(f"Could not find the parent linking vertex for the {parent_type}.") + return + + logger.info(f"User Record UID: {user_record_uid}") + + user_vertex = record_link.get_record_link(user_record_uid) + if user_vertex is None: + logger.warning(f"The user vertex is missing; creating.") + record_link.dag.add_vertex(uid=user_record_uid) + + user_acl = record_link.get_acl(user_record_uid, parent_uid) + if user_acl is None: + logger.warning(f"No ACL exists between the user and the parent; creating.") + user_acl = UserAcl.default() + user_acl.belongs_to = True + + logger.info("") + if user_acl.rotation_settings is not None: + if (force is False and ( + user_acl.rotation_settings.schedule != "" + or user_acl.rotation_settings.pwd_complexity != "" + or (user_acl.rotation_settings.saas_record_uid_list is not None + and len(user_acl.rotation_settings.saas_record_uid_list) != 0))): + logger.error(f"{user_acl.model_dump_json(indent=4)}") + logger.error(f"Rotation settings exist in graph, use --force to reset.") + return + + user_acl.rotation_settings = UserAclRotationSettings() + user_acl.rotation_settings.noop = noop + if resource_record_uid is None: + user_acl.is_iam_user = True + + record_link.belongs_to(user_record_uid, parent_uid, acl=user_acl) + + if parent_uid != configuration_record_uid: + if record_link.get_parent_record_uid(parent_uid) is None: + logger.warning(f"Resource record has no LINK to configuration record; " + f"creating.") + record_link.belongs_to(configuration_record_uid, parent_uid) + + if not dry_run: + record_link.save() + + logger.info(f"{user_acl.model_dump_json(indent=4)}") + logger.info(f"Updated the ACL for the user.") + else: + logger.info(f"DRY RUN: Would have created this ACL.") + logger.info(f"{user_acl.model_dump_json(indent=4)}") diff --git a/keepercli-package/src/keepercli/commands/pam/debug/debug_vertex.py b/keepercli-package/src/keepercli/commands/pam/debug/debug_vertex.py new file mode 100644 index 00000000..3ff01044 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/debug/debug_vertex.py @@ -0,0 +1,199 @@ +import argparse +import re +import time + +from keepersdk.helpers.keeper_dag.user_service import Infrastructure + +from ....params import KeeperParams +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.dag_types import DiscoveryObject +from keepersdk.helpers.keeper_dag.dag import EdgeType +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from .... import api + +logger = api.get_logger() + +class PAMDebugVertexCommand(PAMGatewayActionDiscoverCommandBase): + type_name_map = { + PAM_USER: "PAM User", + PAM_MACHINE: "PAM Machine", + PAM_DATABASE: "PAM Database", + PAM_DIRECTORY: "PAM Directory", + } + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action debug vertex') + PAMDebugVertexCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--vertex', '-i', required=True, dest='vertex_uid', action='store', + help='Vertex in infrastructure graph') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + debug_level = kwargs.get("debug_level", False) + + configuration_uid = kwargs.get('configuration_uid') + + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + infra = Infrastructure(record=gateway_context.configuration, context=context, fail_on_corrupt=False, + debug_level=debug_level) + infra.load() + + vertex_uid = kwargs.get("vertex_uid") + vertex = infra.dag.get_vertex(vertex_uid) + if vertex is None: + logger.error(f"Could not find the vertex in the graph for {gateway}.") + return + + content = DiscoveryObject.get_discovery_object(vertex) + missing_since = "NA" + if content.missing_since_ts is not None: + missing_since = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(content.missing_since_ts)) + + logger.info(f"Discovery Object Information") + logger.info(f"Vertex UID: {content.uid}") + logger.info(f"Object ID: {content.id}") + logger.info(f"Record UID: {content.record_uid}") + logger.info(f"Parent Record UID: {content.parent_record_uid}") + logger.info(f"Shared Folder UID: {content.shared_folder_uid}") + logger.info(f"Record Type: {content.record_type}") + logger.info(f"Object Type: {content.object_type_value}") + logger.info(f"Ignore Object: {content.ignore_object}") + logger.info(f"Rule Engine Result: {content.action_rules_result}") + logger.info(f"Name: {content.name}") + logger.info(f"Generated Title: {content.title}") + logger.info(f"Generated Description: {content.description}") + logger.info(f"Missing Since: {missing_since}") + logger.info(f"Discovery Notes:") + for note in content.notes: + logger.info(f" * {note}") + if content.error is not None: + logger.error(f" Error: {content.error}") + if content.stacktrace is not None: + logger.error(f" Stack Trace:") + logger.error(f"{content.stacktrace}") + logger.info("") + logger.info(f"Record Type Specifics") + + if content.record_type == PAM_USER: + logger.info(f"User: {content.item.user}") + logger.info(f"DN: {content.item.dn}") + logger.info(f"Database: {content.item.database}") + logger.info(f"Active: {content.item.active}") + logger.info(f"Expired: {content.item.expired}") + logger.info(f"Source: {content.item.source}") + elif content.record_type == PAM_MACHINE: + logger.info(f"Host: {content.item.host}") + logger.info(f"IP: {content.item.ip}") + logger.info(f"Port: {content.item.port}") + logger.info(f"Operating System: {content.item.os}") + logger.info(f"Provider Region: {content.item.provider_region}") + logger.info(f"Provider Group: {content.item.provider_group}") + logger.info(f"Is the Gateway: {content.item.is_gateway}") + logger.info(f"Allows Admin: {content.item.allows_admin}") + logger.info(f"Admin Reason: {content.item.admin_reason}") + logger.info("") + + if content.item.facts.id is not None and content.item.facts.name is not None: + logger.info(f"Machine Name: {content.item.facts.name}") + logger.info(f"Machine ID: {content.item.facts.id.machine_id}") + logger.info(f"Product ID: {content.item.facts.id.product_id}") + logger.info(f"Board Serial: {content.item.facts.id.board_serial}") + logger.info(f"Directories:") + if content.item.facts.directories is not None and len(content.item.facts.directories) > 0: + for directory in content.item.facts.directories: + logger.info(f" * Directory Domain: {directory.domain}") + logger.info(f" Software: {directory.software}") + logger.info(f" Login Format: {directory.login_format}") + else: + logger.info(" Machines is not using any directories.") + + logger.info("") + logger.info(f"Services (Non Builtin Users):") + if len(content.item.facts.services) > 0: + for service in content.item.facts.services: + logger.info(f" * {service.name} = {service.user}") + else: + logger.info(" Machines has no services that are using non-builtin users.") + + logger.info(f"Scheduled Tasks (Non Builtin Users)") + if len(content.item.facts.tasks) > 0: + for task in content.item.facts.tasks: + logger.info(f" * {task.name} = {task.user}") + else: + logger.info(" Machines has no schedules tasks that are using non-builtin users.") + + logger.info(f"IIS Pools (Non Builtin Users)") + if len(content.item.facts.iis_pools) > 0: + for iis_pool in content.item.facts.iis_pools: + logger.info(f" * {iis_pool.name} = {iis_pool.user}") + else: + logger.info(" Machines has no IIS Pools that are using non-builtin users.") + + else: + logger.error(f" Machine facts are not set. Discover inside may not have been " + f"performed.") + elif content.record_type == PAM_DATABASE: + logger.info(f"Host: {content.item.host}") + logger.info(f"IP: {content.item.ip}") + logger.info(f"Port: {content.item.port}") + logger.info(f"Database Type: {content.item.type}") + logger.info(f"Database: {content.item.database}") + logger.info(f"Use SSL: {content.item.use_ssl}") + logger.info(f"Provider Region: {content.item.provider_region}") + logger.info(f"Provider Group: {content.item.provider_group}") + logger.info(f"Allows Admin: {content.item.allows_admin}") + logger.info(f"Admin Reason: {content.item.admin_reason}") + elif content.record_type == PAM_DIRECTORY: + logger.info(f"Host: {content.item.host}") + logger.info(f"IP: {content.item.ip}") + logger.info(f"Port: {content.item.port}") + logger.info(f"Directory Type: {content.item.type}") + logger.info(f"Use SSL: {content.item.use_ssl}") + logger.info(f"Provider Region: {content.item.provider_region}") + logger.info(f"Provider Group: {content.item.provider_group}") + logger.info(f"Allows Admin: {content.item.allows_admin}") + logger.info(f"Admin Reason: {content.item.admin_reason}") + + logger.info("") + logger.info(f"Belongs To Vertices (Parents)") + vertices = vertex.belongs_to_vertices() + for vertex in vertices: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {content.description} ({vertex.uid})") + for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]: + edge = vertex.get_edge(vertex, edge_type=edge_type) + if edge is not None: + logger.info(f" . {edge_type}, active: {edge.active}") + + if len(vertices) == 0: + logger.error(f" Does not belong to anyone") + + logger.info("") + logger.info(f"Vertices Belonging To (Children)") + vertices = vertex.has_vertices() + for vertex in vertices: + content = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {content.description} ({vertex.uid})") + for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]: + edge = vertex.get_edge(vertex, edge_type=edge_type) + if edge is not None: + logger.info(f" . {edge_type}, active: {edge.active}") + if len(vertices) == 0: + logger.info(f" Does not have any children.") + + logger.info("") diff --git a/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py b/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py new file mode 100644 index 00000000..7d745e67 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py @@ -0,0 +1,375 @@ + +import base64 +import json +import os +import re +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + +from .... import api +from ....commands import base +from ....helpers import router_utils +from ....helpers.gateway_utils import get_all_gateways +from ..pam_config import PAM_CONFIG_RECORD_TYPES + +from keepersdk.vault import ksm_management, vault_record, vault_online +from keepersdk.helpers.pam_config_facade import PamConfigurationRecordFacade +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.proto import pam_pb2, APIRequest_pb2 +from keepersdk import utils +from keepersdk.crypto import decrypt_aes_v2, encrypt_aes_v2 +from keepersdk.helpers.keeper_dag import dag_utils + +logger = api.get_logger() + + +class MultiConfigurationException(Exception): + """ + If the gateway has multiple configuration + """ + def __init__(self, items: List[Dict]): + super().__init__() + self.items = items + + def print_items(self): + for item in self.items: + record = item["configuration_record"] + logger.info(f" * {record.record_uid} - {record.title}") + + +class GatewayContext: + + """ + Context for a gateway and a configuration. + + In the configuration record, the gateway is selected. + This means multiple configuration can use the same gateway. + Commander is gateway centric, we need to treat gateway and configuration as a `primary key` + """ + + def __init__(self, configuration: vault_record.KeeperRecord, facade: PamConfigurationRecordFacade, + gateway: pam_pb2.PAMController, application: vault_record.ApplicationRecord, + vault: vault_online.VaultOnline): + self.configuration = configuration + self.facade = facade + self.gateway = gateway + self.application = application + self._vault = vault + self._shared_folders = None + + @staticmethod + def all_gateways(vault: vault_online.VaultOnline): + return get_all_gateways(vault) + + @staticmethod + def find_gateway(vault: vault_online.VaultOnline, find_func: Callable, gateways: Optional[List] = None) \ + -> Tuple[Optional["GatewayContext"], Any]: + """ + Populate the context from matching using the function passed in. + The function needs to return a non-None value to be considered a positive match. + """ + if gateways is None: + gateways = GatewayContext.all_gateways(vault) + + configuration_records = list(vault.vault_data.find_records( + criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + for configuration_record in configuration_records: + payload = find_func( + configuration_record=configuration_record + ) + if payload is not None: + return GatewayContext.from_configuration_uid( + vault=vault, + configuration_uid=configuration_record.record_uid, + gateways=gateways + ), payload + + return None, None + + @staticmethod + def from_configuration_uid(vault: vault_online.VaultOnline, configuration_uid: str, gateways: Optional[List] = None) \ + -> Optional["GatewayContext"]: + """ + Populate context using the configuration UID. + From the configuration record, get the gateway from the settings. + """ + + if gateways is None: + gateways = GatewayContext.all_gateways(vault) + + configuration_record = vault.vault_data.load_record(configuration_uid) + if not isinstance(configuration_record, vault_record.TypedRecord): + logger.error(f'PAM Configuration [{configuration_uid}] is not available.') + return None + + configuration_facade = PamConfigurationRecordFacade() + configuration_facade.record = configuration_record + + gateway_uid = configuration_facade.controller_uid + gateway = next((x for x in gateways + if utils.base64_url_encode(x.controllerUid) == gateway_uid), + None) + + if gateway is None: + return None + + application_id = utils.base64_url_encode(gateway.applicationUid) + application = vault.vault_data.load_record(application_id) + + return GatewayContext( + configuration=configuration_record, + facade=configuration_facade, + gateway=gateway, + application=application, + vault=vault + ) + + @staticmethod + def from_gateway(vault: vault_online.VaultOnline, gateway: str, configuration_uid: Optional[str] = None) \ + -> Optional["GatewayContext"]: + """ + Populate context use the gateway, and optional configuration UID. + + This will scan all configuration to find which ones use this gateway. + If there are multiple ones, a MultiConfigurationException is thrown. + If there is only one gateway, then that gateway is used. + """ + configuration_records = list(vault.vault_data.find_records( + criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + + if configuration_uid: + logger.debug(f"find the gateway with configuration record {configuration_uid}") + + if len(configuration_records) == 0: + logger.error(f"Cannot find any PAM configuration records in the Vault") + return None + + all_gateways = get_all_gateways(vault) + found_items = [] + for configuration_record in configuration_records: + + logger.debug(f"checking configuration record {configuration_record.title}") + + # Load the configuration record and get the gateway_uid from the facade. + configuration_record = vault.vault_data.load_record(configuration_record.record_uid) + configuration_facade = PamConfigurationRecordFacade() + configuration_facade.record = configuration_record + + configuration_gateway_uid = configuration_facade.controller_uid + if configuration_gateway_uid is None: + logger.debug(f" * configuration {configuration_record.title} does not have a gateway set, skipping.") + continue + + # Get the gateway for this configuration + found_gateway = next((x for x in all_gateways if utils.base64_url_encode(x.controllerUid) == + configuration_gateway_uid), None) + if found_gateway is None: + logger.debug(f" * configuration does not use desired gateway") + continue + + if configuration_uid is not None and configuration_uid == configuration_record.record_uid: + logger.debug(f" * configuration record uses this gateway and matches desire configuration, " + "skipping the rest") + found_items = [{ + "configuration_facade": configuration_facade, + "configuration_record": configuration_record, + "gateway": found_gateway + }] + break + + if (utils.base64_url_encode(found_gateway.controllerUid) == gateway or + found_gateway.controllerName.lower() == gateway.lower()): + logger.debug(f" * configuration record uses this gateway") + found_items.append({ + "configuration_facade": configuration_facade, + "configuration_record": configuration_record, + "gateway": found_gateway + }) + + if len(found_items) > 1: + logger.debug(f"found {len(found_items)} configurations using this gateway") + raise MultiConfigurationException( + items=found_items + ) + + if len(found_items) == 1: + found_gateway = found_items[0]["gateway"] + configuration_record = found_items[0]["configuration_record"] + configuration_facade = found_items[0]["configuration_facade"] + + application_id = utils.base64_url_encode(found_gateway.applicationUid) + application = vault.vault_data.load_record(application_id) + if application is None: + logger.debug(f"cannot find application for gateway {gateway}, skipping.") + + if (utils.base64_url_encode(found_gateway.controllerUid) == gateway or + found_gateway.controllerName.lower() == gateway.lower()): + return GatewayContext( + configuration=configuration_record, + facade=configuration_facade, + gateway=found_gateway, + application=application, + vault=vault + ) + + return None + + @property + def gateway_uid(self) -> str: + return utils.base64_url_encode(self.gateway.controllerUid) + + @property + def configuration_uid(self) -> str: + return self.configuration.record_uid + + @property + def gateway_name(self) -> str: + return self.gateway.controllerName + + @property + def default_shared_folder_uid(self) -> str: + return self.facade.folder_uid + + def is_gateway(self, request_gateway: str) -> bool: + if request_gateway is None or self.gateway_name is None: + return False + return (request_gateway == utils.base64_url_encode(self.gateway.controllerUid) or + request_gateway.lower() == self.gateway_name.lower()) + + def get_shared_folders(self, vault: vault_online.VaultOnline) -> List[dict]: + if self._shared_folders is None: + self._shared_folders = [] + application_uid = utils.base64_url_encode(self.gateway.applicationUid) + app_infos = ksm_management.get_app_info(vault, application_uid) + if not app_infos: + return self._shared_folders + for shared in getattr(app_infos[0], 'shares', None) or []: + if APIRequest_pb2.ApplicationShareType.Name(shared.shareType) != 'SHARE_TYPE_FOLDER': + continue + uid_str = utils.base64_url_encode(shared.secretUid) + sf_info = vault.vault_data.get_shared_folder(uid_str) + if sf_info is None: + continue + full_sf = vault.vault_data.load_shared_folder(uid_str) + records: List[Dict[str, str]] = [] + if full_sf is not None: + records = [{"record_uid": rp.record_uid} for rp in full_sf.record_permissions] + self._shared_folders.append({ + "uid": uid_str, + "name": sf_info.name, + "folder": {"records": records}, + }) + return self._shared_folders + + def _configuration_record_key(self) -> bytes: + key = self._vault.vault_data.get_record_key(self.configuration.record_uid) + if not key: + raise RuntimeError( + f'No record key for PAM configuration {self.configuration.record_uid!r}; ' + f'ensure the vault is unlocked and records are synced.' + ) + return key + + def decrypt(self, cipher_base64: bytes) -> dict: + ciphertext = base64.b64decode(cipher_base64.decode()) + return json.loads(decrypt_aes_v2(ciphertext, self._configuration_record_key())) + + def encrypt(self, data: dict) -> str: + json_data = json.dumps(data) + ciphertext = encrypt_aes_v2(json_data.encode(), self._configuration_record_key()) + return base64.b64encode(ciphertext).decode() + + def encrypt_str(self, data: Union[bytes, str]) -> str: + if isinstance(data, str): + data = data.encode() + ciphertext = encrypt_aes_v2(data, self._configuration_record_key()) + return base64.b64encode(ciphertext).decode() + + @staticmethod + def get_configuration_records(vault: vault_online.VaultOnline) -> List[vault_record.KeeperRecord]: + + """ + Get PAM configuration records. + + The default it to find all the record version 6 records. + If the environment variable `PAM_RECORD_TYPE_MATCH` is set to a true value, the search will use both record + versions 3 and 6, and then check the record type. + """ + + configuration_list = [] + if dag_utils.value_to_boolean(os.environ.get("PAM_RECORD_TYPE_MATCH")): + for record in list(vault.vault_data.find_records(record_version=iter([3, 6]), record_type=None)): + if re.search(r"pam.+Configuration", record.record_type): + configuration_list.append(record) + else: + configuration_list = list(vault.vault_data.find_records(record_version=6, record_type=None)) + return configuration_list + + +class PAMGatewayActionDiscoverCommandBase(base.ArgparseCommand): + """ + The discover command base. + Contains static methods to get the configuration record, get and update the discovery store. These are methods + used by multiple discover actions. + """ + + # If the discovery data field does not exist, or the field contains no values, use the template to init the + # field. + + STORE_LABEL = "discoveryKey" + FIELD_MAPPING = { + "pamHostname": { + "type": "dict", + "field_input": [ + {"key": "hostName", "prompt": "Hostname"}, + {"key": "port", "prompt": "Port"} + ], + "field_format": [ + {"key": "hostName", "label": "Hostname"}, + {"key": "port", "label": "Port"}, + ] + }, + "alternativeIPs": { + "type": "csv", + }, + "privatePEMKey": { + "type": "multiline", + }, + "operatingSystem": { + "type": "choice", + "values": ["linux", "macos", "windows", "cisco_ios_xe"] + } + } + + type_name_map = { + PAM_USER: "PAM Users", + PAM_MACHINE: "PAM Machines", + PAM_DATABASE: "PAM Databases", + PAM_DIRECTORY: "PAM Directories", + } + + @staticmethod + def get_response_data(router_response: dict) -> Optional[dict]: + + if router_response is None: + return None + + response = router_response.get("response") + logger.debug(f"Router Response: {response}") + payload = router_utils.get_response_payload(router_response) + return payload.get("data") + + @staticmethod + def _p(msg): + return msg + + @staticmethod + def _n(record_type): + return PAMGatewayActionDiscoverCommandBase.type_name_map.get(record_type, "PAM Configuration") + + +def multi_conf_msg(gateway: str, err: MultiConfigurationException): + logger.info(f"Found multiple configuration records for gateway {gateway}.") + logger.info("Please use the --configuration-uid parameter to select the configuration.") + logger.info("Available configurations are: ") + err.print_items() \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/pam/discovery/discover.py b/keepercli-package/src/keepercli/commands/pam/discovery/discover.py new file mode 100644 index 00000000..cfca05fa --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/discovery/discover.py @@ -0,0 +1,2052 @@ +import argparse +import json +import os +import sys +from typing import Any, Dict, List, Optional, Tuple +from pydantic import BaseModel + +from keepersdk import crypto, utils + +from ... import base +from ....params import KeeperParams +from ....helpers import router_utils +from .... import api +from .__init__ import GatewayContext, MultiConfigurationException, multi_conf_msg, PAMGatewayActionDiscoverCommandBase +from ..pam_dto import GatewayAction, GatewayActionDiscoverJobStartInputs, GatewayActionDiscoverJobStart, GatewayActionDiscoverJobRemoveInputs, GatewayActionDiscoverJobRemove +from .rule_commands import PAMGatewayActionDiscoverRuleAddCommand, PAMGatewayActionDiscoverRuleListCommand, PAMGatewayActionDiscoverRuleRemoveCommand, PAMGatewayActionDiscoverRuleUpdateCommand +from ..pam_config import PAM_CONFIG_RECORD_TYPES + +from keepersdk.helpers.pam_user_record_facade import PamUserRecordFacade +from keepersdk.helpers.keeper_dag.jobs import Jobs +from keepersdk.helpers.keeper_dag.dag_types import (CredentialBase, DiscoveryDelta, DiscoveryObject, JobItem, UserAcl, DirectoryInfo, + BulkRecordConvert, BulkRecordAdd, BulkRecordSuccess, BulkProcessResults, NormalizedRecord, BulkRecordFail, PromptResult, + PromptActionEnum, RecordField) +from keepersdk.helpers.keeper_dag.dag_vertex import DAGVertex +from keepersdk.helpers.keeper_dag.dag import DAG +from keepersdk.helpers.keeper_dag.dag_sort import sort_infra_vertices +from keepersdk.helpers.keeper_dag.constants import VERTICES_SORT_MAP, DIS_INFRA_GRAPH_ID, PAM_USER +from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure +from keepersdk.helpers.keeper_dag.process import Process, NoDiscoveryDataException, QuitException +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.vault import vault_extensions, vault_online, vault_record +from keepersdk.proto import pam_pb2, record_pb2, router_pb2 + +logger = api.get_logger() + + +class PAMGatewayActionDiscoverJobStatusCommand(PAMGatewayActionDiscoverCommandBase): + """ + Get the status of discovery jobs. + If no parameters are given, it will check all gateways for discovery job status. + """ + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover status') + PAMGatewayActionDiscoverJobStatusCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=False, dest='gateway', action='store', + help='Show only discovery jobs from a specific gateway.') + parser.add_argument('--job-id', '-j', required=False, dest='job_id', action='store', + help='Detailed information for a specific discovery job.') + parser.add_argument('--history', required=False, dest='show_history', action='store_true', + help='Show history') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID is using --history') + + @staticmethod + def print_job_table(jobs: List[Dict], + max_gateway_name: int, + show_history: bool = False): + logger.info("") + logger.info(f"{'Job ID'.ljust(14, ' ')} " + f"{'Gateway Name'.ljust(max_gateway_name, ' ')} " + f"{'Gateway UID'.ljust(22, ' ')} " + f"{'Configuration UID'.ljust(22, ' ')} " + f"{'Status'.ljust(12, ' ')} " + f"{'Resource UID'.ljust(22, ' ')} " + f"{'Started'.ljust(19, ' ')} " + f"{'Completed'.ljust(19, ' ')} " + f"{'Duration'.ljust(19, ' ')} " + f"") + + logger.info(f"{''.ljust(14, '=')} " + f"{''.ljust(max_gateway_name, '=')} " + f"{''.ljust(22, '=')} " + f"{''.ljust(22, '=')} " + f"{''.ljust(12, '=')} " + f"{''.ljust(22, '=')} " + f"{''.ljust(19, '=')} " + f"{''.ljust(19, '=')} " + f"{''.ljust(19, '=')}") + + completed_jobs = [] + running_jobs = [] + failed_jobs = [] + + for job in jobs: + job_id = job['job_id'] + if job['status'] == "COMPLETE": + completed_jobs.append(job_id) + elif job['status'] == "RUNNING": + running_jobs.append(job_id) + elif job['status'] == "FAILED": + failed_jobs.append(job_id) + logger.info(f"{job_id} " + f"{job['gateway'].ljust(max_gateway_name, ' ')} " + f"{job['gateway_uid']} " + f"{job['configuration_uid']} " + f"{job['status'].ljust(12, ' ')} " + f"{(job.get('resource_uid') or 'NA').ljust(22, ' ')} " + f"{(job.get('start_ts_str') or 'NA').ljust(19, ' ')} " + f"{(job.get('end_ts_str') or 'NA').ljust(19, ' ')} " + f"{(job.get('duration') or 'NA').ljust(19, ' ')} " + f"") + + if len(completed_jobs) > 0 and show_history is False: + logger.info("") + if len(completed_jobs) == 1: + logger.info(f"There is one COMPLETED job. To process, use the following command.") + else: + logger.info(f"There are {len(completed_jobs)} COMPLETED jobs. " + "To process, use one of the the following commands.") + for job_id in completed_jobs: + logger.info(f" pam action discover process -j {job_id}") + + if len(running_jobs) > 0 and show_history is False: + logger.info("") + if len(running_jobs) == 1: + logger.info(f"There is one RUNNING job. " + "If there is a problem, use the following command to cancel/remove the job.") + else: + logger.info(f"There are {len(running_jobs)} RUNNING jobs. " + "If there is a problem, use one of the following commands to cancel/remove the job.") + for job_id in running_jobs: + logger.info(f" pam action discover remove -j {job_id}") + + if len(failed_jobs) > 0 and show_history is False: + logger.info("") + if len(failed_jobs) == 1: + logger.info(f"There is one FAILED job. " + "If there is a problem, use the following command to get more information.") + else: + logger.info(f"There are {len(failed_jobs)} FAILED jobs. " + "If there is a problem, use one of the following commands to get more information.") + for job_id in failed_jobs: + logger.info(f" pam action discover status -j {job_id}") + logger.info("") + if len(failed_jobs) == 1: + logger.info(f"To remove the job, use the following command.") + else: + logger.info(f"To remove the FAILED job, use one of the following commands.") + for job_id in failed_jobs: + logger.info(f" pam action discover remove -j {job_id}") + + logger.info("") + + @staticmethod + def print_job_detail(vault: vault_online.VaultOnline, + all_gateways: List, + job_id: str): + + def _find_job(configuration_record) -> Optional[Dict]: + jobs_obj = Jobs(record=configuration_record) + job_item = jobs_obj.get_job(job_id) + if job_item is not None: + return { + "jobs": jobs_obj, + } + return None + + gateway_context, payload = GatewayContext.find_gateway(vault=vault, + find_func=_find_job, + gateways=all_gateways) + + if gateway_context is not None: + jobs = payload["jobs"] + job = jobs.get_job(job_id) + infra = Infrastructure(record=gateway_context.configuration) + + status = "RUNNING" + if job.end_ts is not None and not job.error: + if job.success is None: + status = "CANCELLED" + else: + status = "COMPLETE" + elif job.error: + status = "FAILED" + + logger.info("") + logger.info(f"Job ID: {job.job_id}") + logger.info(f"Sync Point: {job.sync_point}") + logger.info(f"Gateway Name: {gateway_context.gateway_name}") + logger.info(f"Gateway UID: {gateway_context.gateway_uid}") + logger.info(f"Configuration UID: {gateway_context.configuration_uid}") + logger.info(f"Status: {status}") + logger.info(f"Resource UID: {job.resource_uid or 'NA'}") + logger.info(f"Started: {job.start_ts_str}") + logger.info(f"Completed: {job.end_ts_str}") + logger.info(f"Duration: {job.duration_sec_str}") + + if status == "FAILED": + logger.info("") + logger.info(f"Gateway Error:") + logger.info(f"{job.error}") + logger.info("") + logger.info(f"Gateway Stacktrace:") + logger.info(f"{job.stacktrace}") + + elif job.end_ts is not None: + + try: + infra.load(sync_point=0) + logger.info("") + delta_json = job.delta + if delta_json is not None: + delta = DiscoveryDelta.model_validate(delta_json) + logger.info(f"Added - {len(delta.added)} count") + for item in delta.added: + vertex = infra.dag.get_vertex(item.uid) + if vertex is None or vertex.active is False or vertex.has_data is False: + logger.debug("added: vertex is none, inactive or has no data") + continue + discovery_object = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {discovery_object.description}") + + logger.info("") + logger.info(f"Changed - {len(delta.changed)} count") + for item in delta.changed: + vertex = infra.dag.get_vertex(item.uid) + if vertex is None or vertex.active is False or vertex.has_data is False: + logger.debug("changed: vertex is none, inactive or has no data") + continue + discovery_object = DiscoveryObject.get_discovery_object(vertex) + logger.info(f" * {discovery_object.description}") + if item.changes is None: + logger.info(f" no changed, may be a object not added in prior discoveries.") + else: + for key, value in item.changes.items(): + logger.info(f" - {key} = {value}") + + logger.info("") + logger.info(f"Deleted - {len(delta.deleted)} count") + for item in delta.deleted: + logger.info(f" * discovery vertex {item.uid}") + else: + logger.info(f"There are no available delta changes for this job.") + + except Exception as err: + logger.info(f"Could not load delta from infrastructure: {str(err)}") + logger.info("Fall back to raw graph.") + logger.info("") + dag = DAG(conn=infra.conn, record=infra.record, graph_id=DIS_INFRA_GRAPH_ID) + logger.info(dag.to_dot_raw(sync_point=job.sync_point, rank_dir="RL")) + + else: + logger.info(f"Could not find the gateway with job {job_id}.") + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + vault = context.vault + + gateway_filter = kwargs.get("gateway") + + job_id = kwargs.get("job_id") + + show_history = kwargs.get("show_history") + + all_gateways = GatewayContext.all_gateways(vault) + + if gateway_filter is None: + show_history = False + + # This is used to format the table. Start with a length of 12 characters for the gateway. + max_gateway_name = 12 + + if job_id: + self.print_job_detail(vault=vault, + all_gateways=all_gateways, + job_id=job_id) + else: + selected_jobs = [] # type: List[Dict] + + # For each configuration/ gateway, we are going to get all jobs. + # We are going to query the gateway for any updated status. + + configuration_records = list(vault.vault_data.find_records( + criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + for configuration_record in configuration_records: + + gateway_context = GatewayContext.from_configuration_uid( + vault=vault, + configuration_uid=configuration_record.record_uid, + gateways=all_gateways) + + if gateway_context is None: + continue + + if gateway_filter is not None and gateway_context.is_gateway(gateway_filter) is False: + continue + + # If the gateway name is longer than the prior, set the max length to this gateway's name. + if len(gateway_context.gateway_name) > max_gateway_name: + max_gateway_name = len(gateway_context.gateway_name) + + jobs = Jobs(record=configuration_record) + if show_history is True: + job_list = reversed(jobs.history) + else: + job_list = [] + if jobs.current_job is not None: + job_list = [jobs.current_job] + + for job_item in job_list: + job = job_item.model_dump() + job["status"] = "RUNNING" + if job_item.start_ts is not None: + job["start_ts_str"] = job_item.start_ts_str + if job_item.end_ts is not None: + job["end_ts_str"] = job_item.end_ts_str + job["status"] = "COMPLETE" + + job["duration"] = job_item.duration_sec_str + + job["gateway"] = gateway_context.gateway_name + job["gateway_uid"] = gateway_context.gateway_uid + job["configuration_uid"] = gateway_context.configuration_uid + + job["gateway_context"] = gateway_context + job["job_item"] = job_item + + if job_item.success is None and job_item.end_ts: + job["status"] = "CANCELLED" + elif job_item.success is False: + job["status"] = "FAILED" + + selected_jobs.append(job) + + if len(selected_jobs) == 0: + logger.info(f"There are no discovery jobs. Use 'pam action discover start' to start a " + f"discovery job.") + return + + self.print_job_table(jobs=selected_jobs, + max_gateway_name=max_gateway_name, + show_history=show_history) + + +class PAMGatewayActionDiscoverJobStartCommand(PAMGatewayActionDiscoverCommandBase): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover start') + PAMGatewayActionDiscoverJobStartCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--resource', '-r', required=False, dest='resource_uid', action='store', + help='UID of the resource record. Set to discover specific resource.') + + parser.add_argument('--lang', required=False, dest='language', action='store', default="en_US", + help='Language') + parser.add_argument('--include-machine-dir-users', required=False, dest='include_machine_dir_users', + action='store_false', default=True, help='Include directory users found on the machine.') + parser.add_argument('--inc-azure-aadds', required=False, dest='include_azure_aadds', + action='store_true', help='Include Azure Active Directory Domain Service.') + parser.add_argument('--skip-rules', required=False, dest='skip_rules', + action='store_true', help='Skip running the rule engine.') + parser.add_argument('--skip-machines', required=False, dest='skip_machines', + action='store_true', help='Skip discovering machines.') + parser.add_argument('--skip-databases', required=False, dest='skip_databases', + action='store_true', help='Skip discovering databases.') + parser.add_argument('--skip-directories', required=False, dest='skip_directories', + action='store_true', help='Skip discovering directories.') + parser.add_argument('--skip-cloud-users', required=False, dest='skip_cloud_users', + action='store_true', help='Skip discovering cloud users.') + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + vault = context.vault + + # Load the configuration record and get the gateway_uid from the facade. + gateway = kwargs.get('gateway') + gateway_context = None + try: + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + except MultiConfigurationException as err: + multi_conf_msg(gateway, err) + return + + jobs = Jobs(record=gateway_context.configuration) + current_job_item = jobs.current_job + removed_prior_job = None + if current_job_item is not None: + if current_job_item.is_running is True: + logger.warning("A discovery job is currently running. Cannot start another until it is finished.") + logger.warning("To check the status, use the command 'pam action discover status'.") + logger.warning(f"To stop and remove the current job, use the command 'pam action discover remove -j {current_job_item.job_id}'.") + return + + logger.error(f"An active discovery job exists for this gateway.") + logger.info("") + status = PAMGatewayActionDiscoverJobStatusCommand() + status.execute(context=context) + logger.info("") + + yn = input("Do you wish to remove the active discovery job and run a new one [Y/N]> ").lower() + while True: + if yn[0] == "y": + jobs.cancel(current_job_item.job_id) + removed_prior_job = current_job_item.job_id + break + elif yn[0] == "n": + logger.error(f"Not starting a discovery job.") + return + + # Get the credentials passed in via the command line + credentials = [] + creds = kwargs.get('credentials') + if creds is not None: + for cred in creds: + parts = cred.split("|") + c = CredentialBase() + for item in parts: + kv = item.split("=") + if len(kv) != 2: + logger.error(f"A '--cred' is invalid. It does not have a value.") + return + if not hasattr(c, kv[0]): + logger.error(f"A '--cred' is invalid. The key '{kv[0]}' is invalid.") + return + if hasattr(c, kv[1]) == "": + logger.error(f"A '--cred' is invalid. The value is blank.") + return + setattr(c, kv[0], kv[1]) + credentials.append(c.model_dump()) + + # Get the credentials passed in via a credential file. + credential_files = kwargs.get('credential_file') + if credential_files is not None: + with open(credential_files, "r") as fh: + try: + creds = json.load(fh) + except FileNotFoundError: + logger.error(f"Could not find the file {credential_files}") + return + except json.JSONDecoder: + logger.error(f"The file {credential_files} is not valid JSON.") + return + except Exception as err: + logger.error(f"The JSON file {credential_files} could not be imported: {err}") + return + + if not isinstance(creds, list): + logger.error(f"Credential file is invalid. Structure is not an array.") + return + num = 1 + for obj in creds: + c = CredentialBase() + for key in obj: + if not hasattr(c, key): + logger.error(f"Object {num} has the invalid key {key}.") + return + setattr(c, key, obj[key]) + credentials.append(c.model_dump()) + + action_inputs = GatewayActionDiscoverJobStartInputs( + configuration_uid=gateway_context.configuration_uid, + resource_uid=kwargs.get('resource_uid'), + user_map=gateway_context.encrypt( + self.make_protobuf_user_map( + context=context, + gateway_context=gateway_context + )[0] + ), + + shared_folder_uid=gateway_context.default_shared_folder_uid, + languages=[kwargs.get('language')], + + # Settings + include_machine_dir_users=kwargs.get('include_machine_dir_users', True), + include_azure_aadds=kwargs.get('include_azure_aadds', False), + skip_rules=kwargs.get('skip_rules', False), + skip_machines=kwargs.get('skip_machines', False), + skip_databases=kwargs.get('skip_databases', False), + skip_directories=kwargs.get('skip_directories', False), + skip_cloud_users=kwargs.get('skip_cloud_users', False), + credentials=credentials + ) + + conversation_id = GatewayAction.generate_conversation_id() + router_response = router_utils.router_send_action_to_gateway( + context=context, + gateway_action=GatewayActionDiscoverJobStart( + inputs=action_inputs, + conversation_id=conversation_id), + message_type=pam_pb2.CMT_DISCOVERY, + is_streaming=False, + destination_gateway_uid_str=gateway_context.gateway_uid + ) + + data = self.get_response_data(router_response) + if data is None: + logger.error(f"The router returned a failure.") + return + + if "has been queued" in data.get("Response", ""): + + if removed_prior_job is None: + logger.info("The discovery job is currently running.") + else: + logger.info(f"Active discovery job {removed_prior_job} has been removed and new discovery job is running.") + logger.info(f"To check the status, use the command 'pam action discover status'.") + logger.info(f"To stop and remove the current job, use the command 'pam action discover remove -j '.") + else: + router_utils.print_router_response(router_response, "job_info", conversation_id, gateway_uid=gateway_context.gateway_uid) + + @staticmethod + def make_protobuf_user_map(context: KeeperParams, gateway_context: GatewayContext) -> List[dict]: + """ + Make a user map for PAM Users. + + The map is used to find existing records. + This map will map a login/DN and parent UID to a record UID. + """ + + vault = context.vault + user_map = [] + for record in vault.vault_data.find_records(criteria=None, record_type="pamUser", record_version=3): + user_record = vault.vault_data.load_record(record.record_uid) + user_facade = PamUserRecordFacade() + user_facade.record = user_record + + info = context.get_record_rotation(user_record.record_uid) + if info is None: + continue + + # Make sure this user is part of this gateway. + if info.configuration_uid != gateway_context.configuration_uid: + continue + + # If the user Admin Cred Record (i.e., parent) is blank, skip the mapping item + # This will be a UID string, not 16 bytes. + if info.resource_uid is None or info.resource_uid == "": + continue + + user_map.append({ + "user": user_facade.login if user_facade.login != "" else None, + "dn": user_facade.distinguishedName if user_facade.distinguishedName != "" else None, + "record_uid": user_record.record_uid, + "parent_record_uid": info.resource_uid + }) + + logger.debug(f"found {len(user_map)} user map items") + + return user_map + +class PAMGatewayActionDiscoverJobRemoveCommand(PAMGatewayActionDiscoverCommandBase): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover remove') + PAMGatewayActionDiscoverJobRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--job-id', '-j', required=True, dest='job_id', action='store', + help='Discovery job id.') + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + vault = context.vault + + job_id = kwargs.get("job_id") + + all_gateways = GatewayContext.all_gateways(vault) + + def _find_job(configuration_record) -> Optional[Dict]: + jobs_obj = Jobs(record=configuration_record) + job_item = jobs_obj.get_job(job_id) + if job_item is not None: + return { + "jobs": jobs_obj, + } + return None + + gateway_context, payload = GatewayContext.find_gateway(vault=vault, + find_func=_find_job, + gateways=all_gateways) + + if gateway_context is not None: + jobs = payload["jobs"] + try: + logger.debug("cancel job on the gateway, if running") + action_inputs = GatewayActionDiscoverJobRemoveInputs( + configuration_uid=gateway_context.configuration_uid, + job_id=job_id + ) + + conversation_id = GatewayAction.generate_conversation_id() + router_response = router_utils.router_send_action_to_gateway( + context=context, + gateway_action=GatewayActionDiscoverJobRemove( + inputs=action_inputs, + conversation_id=conversation_id), + message_type=pam_pb2.CMT_DISCOVERY, + is_streaming=False, + destination_gateway_uid_str=gateway_context.gateway_uid + ) + + data = self.get_response_data(router_response) + if data is None: + raise Exception("The router returned a failure.") + elif data.get("success") is False: + error = data.get("error") + raise Exception(f"Discovery job was not removed: {error}") + except Exception as err: + logger.debug(f"gateway return error removing discovery job: {err}") + + jobs.cancel(job_id) + jobs.close() + + logger.info(f"Discovery job has been removed or cancelled.") + return + + logger.error(f'Discovery job not found. Cannot get remove the job.') + return + + +# This is used for the admin user search +class AdminSearchResult(BaseModel): + record: Any + is_directory_user: bool + is_pam_user: bool + being_used: bool = False + + +class PAMGatewayActionDiscoverResultProcessCommand(PAMGatewayActionDiscoverCommandBase): + + EDITABLE = [ + "login", + "password", + "distinguishedName", + "alternativeIPs", + "database", + "privatePEMKey", + "connectDatabase", + "operatingSystem" + ] + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover process') + PAMGatewayActionDiscoverResultProcessCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--job-id', '-j', required=True, dest='job_id', action='store', + help='Discovery job to process.') + parser.add_argument('--add-all', required=False, dest='add_all', action='store_true', + help='Respond with ADD for all prompts.') + parser.add_argument('--preview', required=False, dest='do_preview', action='store_true', + help='Preview the results') + parser.add_argument('--debug-gs-level', required=False, dest='debug_level', action='store', + help='GraphSync debug level. Default is 0', type=int, default=0) + + + @staticmethod + def _is_directory_user(record_type: str) -> bool: + # pamAzureConfiguration has tenant users what are like a directory. + return (record_type == "pamDirectory" or + record_type == "pamAzureConfiguration") + + @staticmethod + def _get_shared_folder(vault: vault_online.VaultOnline, pad: str, gateway_context: GatewayContext) -> str: + while True: + shared_folders = gateway_context.get_shared_folders(vault) + index = 0 + for folder in shared_folders: + logger.info(f"{pad}* {str(index+1)} - {folder.get('uid')} {folder.get('name')}") + index += 1 + selected = input(f"{pad}Enter number of the shared folder>") + try: + return shared_folders[int(selected) - 1].get("uid") + except ValueError: + logger.error(f"{pad}Input was not a number.") + + @staticmethod + def get_field_values(record: vault_record.TypedRecord, field_type: str) -> List[Any]: + return next( + (f.value + for f in record.fields + if f.type == field_type), + None + ) + + def get_keys_by_record(self, context: KeeperParams, gateway_context: GatewayContext, + record: vault_record.TypedRecord) -> List[str]: + """ + For the record, get the values of fields that are key for this record type. + :param context: + :param gateway_context: + :param record: + :return: + """ + + key_field = Process.get_key_field(record.record_type) + keys = [] + if key_field == "host_port": + values = self.get_field_values(record, "pamHostname") # type: List[dict] + if len(values) == 0: + return [] + + host = values[0].get("hostName") + port = values[0].get("port") + if port is not None: + if host is not None: + keys.append(f"{host}:{port}".lower()) + + elif key_field == "host": + values = self.get_field_values(record, "pamHostname") + if len(values) == 0: + return [] + + host = values[0].get("hostName") + if host is not None: + keys.append(host.lower()) + + elif key_field == "user": + record_rotation = context.get_record_rotation(record.record_uid) + if record_rotation is not None: + controller_uid = record_rotation.configuration_uid + if controller_uid is None or controller_uid != gateway_context.configuration_uid: + return [] + + resource_uid = record_rotation.resource_uid + # If the resource uid is None, the Admin Cred Record has not been set. + if resource_uid is None: + return [] + + values = self.get_field_values(record, "login") + if len(values) == 0: + return [] + + keys.append(f"{resource_uid}:{values[0]}".lower()) + + return keys + + @staticmethod + def _record_lookup(record_uid: str, context:KeeperParams) -> Optional[NormalizedRecord]: + """ + Get the record from the Vault, normalize it, and return it. + """ + + vault = context.vault + record = vault.vault_data.get_record(record_uid) + if record is None: + return None + + normalized_record = NormalizedRecord( + record_uid=record.record_uid, + record_type=record.record_type, + title=record.title, + ) + + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.TypedRecord): + return normalized_record + + def _typed_to_normalized(tf: vault_record.TypedField) -> RecordField: + v = tf.value + if not isinstance(v, list): + v = [v] if v not in (None, '', []) else [] + return RecordField( + type=tf.type, + label=(tf.label or None), + value=list(v), + required=tf.required, + ) + + for field in record.fields: + normalized_record.fields.append(_typed_to_normalized(field)) + for field in (record.custom or ()): + normalized_record.fields.append(_typed_to_normalized(field)) + return normalized_record + + def _build_record_cache(self, context: KeeperParams, gateway_context: GatewayContext) -> dict: + """ + Make a lookup cache for all the records. + This is used to flag discovered items as existing if the record has already been added. This is used to + prevent duplicate records being added. + """ + logger.debug(f"building the PAM record cache") + + cache = { + "pamUser": {}, + "pamMachine": {}, + "pamDirectory": {}, + "pamDatabase": {} + } + + vault = context.vault + + # Set all the PAM Records + records = vault.vault_data.find_records(criteria="pam*", record_type=None, record_version=None) + for record in records: + if record.record_type not in cache: + continue + + record = vault.vault_data.load_record(record.record_uid) + + cache_keys = self.get_keys_by_record( + context=context, + gateway_context=gateway_context, + record=record + ) + if len(cache_keys) == 0: + continue + + record_info = vault.vault_data.get_record(record.record_uid) + for cache_key in cache_keys: + cache[record_info.record_type][cache_key] = record.record_uid + + return cache + + def _edit_record(self, content: DiscoveryObject, pad: str, editable: List[str]) -> bool: + + edit_label = input(f"{pad}Enter 'title' or the name of the Label to edit, RETURN to cancel> ") + + if edit_label == "": + return False + + if edit_label.lower() == "title": + new_title = input(f"{pad}Enter new title> ") + content.title = new_title + + elif edit_label in editable: + new_value = None + if edit_label in self.FIELD_MAPPING: + type_hint = self.FIELD_MAPPING[edit_label].get("type") + if type_hint == "dict": + field_input_format = self.FIELD_MAPPING[edit_label].get("field_input") + new_value = {} + for field in field_input_format: + new_value[field.get('key')] = input(f"{pad}Enter {field_input_format.get('prompt')} value> ") + elif type_hint == "csv": + new_value = input(f"{pad}Enter {edit_label} values, separate with a comma > ") + new_values = map(str.strip, new_value.split(',')) + new_value = "\n".join(new_values) + elif type_hint == "multiline": + logger.info(f"{pad}Enter multilines of text or a path, on the first line, " + "to a file that contains the value.") + logger.info(f"{pad}To end, type 'END' at the start of a new line. You can paste text.") + new_value = "" + first_line = True + while True: + line = input(f"> ").rstrip() + if line == "END": + break + + if first_line: + try: + test_file = line.strip() + logger.debug(f"is first line, check for file path for '{test_file}'") + if os.path.exists(test_file): + with open(test_file, "r") as fh: + new_value = fh.read() + fh.close() + break + else: + logger.debug(f"first line is not a file path") + except Exception as err: + logger.debug(f"exception checking if file: {err}") + first_line = False + new_value += line + "\n" + elif type_hint == "choice": + + values = self.FIELD_MAPPING[edit_label].get("values") + text_values = [x for x in values] + new_value = input(f"{pad}Enter one of the follow values: {', '.join(text_values)}> ") + new_value = new_value.strip().lower() + if new_value not in values: + logger.error(f"{pad}The value {new_value} is not one of the values allowed.") + return False + else: + new_value = input(f"{pad}Enter new value, or path to a file that contains the value > ") + + try: + if os.path.exists(new_value): + with open(new_value, "r") as fh: + new_value = fh.read() + fh.close() + except (Exception,): + pass + + for edit_field in content.fields: + if edit_field.label == edit_label: + edit_field.value = [new_value] + + else: + logger.error(f"{pad}The field is not editable.") + return False + + return True + + @staticmethod + def _add_all_preprocess(vertex: DAGVertex, content: DiscoveryObject, parent_vertex: DAGVertex, + acl: Optional[UserAcl] = None) -> Optional[PromptResult]: + + _ = vertex + _ = acl + + # Check if the directory for a domain exists. + # From the parent, find any directory objects. + # Once a directory for the domain exists, the user should not be prompted about this domain anymore. + if content.record_type == "pamDirectory": + for v in parent_vertex.has_vertices(): + other_content = DiscoveryObject.get_discovery_object(v) + if other_content.record_uid is not None and other_content.name == content.name: + return PromptResult(action=PromptActionEnum.SKIP) + return None + + def _prompt_display_fields(self, content: DiscoveryObject, pad: str) -> List[str]: + + editable = [] + for field in content.fields: + has_editable = False + if field.label in PAMGatewayActionDiscoverResultProcessCommand.EDITABLE: + editable.append(field.label) + has_editable = True + value = field.value + + if len(value) > 0 and value[0] is not None: + # PAM records will have only 1 item in the value array. + value = value[0] + if field.label in self.FIELD_MAPPING: + type_hint = self.FIELD_MAPPING[field.label].get("type") + formatted_value = [] + if type_hint == "dict": + field_input_format = self.FIELD_MAPPING[field.label].get("field_format") + for format_field in field_input_format: + formatted_value.append(f"{format_field.get('label')}: " + f"{value.get(format_field.get('key'))}") + elif type_hint == "csv": + formatted_value.append(", ".join(value.split("\n"))) + elif type_hint == "multiline": + formatted_value.append(value) + elif type_hint == "choice": + formatted_value.append(value) + value = ", ".join(formatted_value) + else: + if has_editable: + value = "MISSING" + else: + value = "None" + + rows = str(value).split("\n") + if len(rows) > 1: + value = rows[0] + f"... {len(rows)} rows." + + logger.info(f"{pad} " + f"Label: {field.label}, " + f"Type: {field.type}, " + f"Value: {value}") + + if len(content.notes) > 0: + logger.info("") + for note in content.notes: + logger.info(f"{pad}* {note}") + + return editable + + @staticmethod + def _prompt_display_relationships(vertex: DAGVertex, content: DiscoveryObject, pad: str): + + if vertex is None: + return + + if content.record_type == "pamUser": + belongs_to = [] + for v in vertex.belongs_to_vertices(): + resource_content = DiscoveryObject.get_discovery_object(v) + belongs_to.append(resource_content.name) + count = len(belongs_to) + logger.info("") + logger.info(f"{pad}This user is found on {count} resource{'s' if count > 1 else ''}") + + def _prompt(self, + content: DiscoveryObject, + acl: UserAcl, + vertex: Optional[DAGVertex] = None, + parent_vertex: Optional[DAGVertex] = None, + resource_has_admin: bool = True, + item_count: int = 0, + items_left: int = 0, + indent: int = 0, + block_auto_add: bool = False, + dry_run: bool = False, + add_all: bool = False, + vault: Optional[vault_online.VaultOnline] = None, + gateway_context: Optional[GatewayContext] = None + ) -> PromptResult: + + if gateway_context is None: + raise Exception("Context not set for processing the discovery results") + + parent_content = DiscoveryObject.get_discovery_object(parent_vertex) + + logger.info("") + + if block_auto_add: + add_all = False + + if add_all is True and vertex is not None: + result = self._add_all_preprocess(vertex, content, parent_vertex, acl) + if result is not None: + return result + + # If the record type is a pamUser, then include parent description. + if content.record_type == "pamUser" and parent_vertex is not None: + parent_pad = "" + if indent - 1 > 0: + parent_pad = "".ljust(2 * indent, ' ') + + logger.info(f"{parent_pad}{parent_content.description}") + + pad = "" + if indent > 0: + pad = "".ljust(2 * indent, ' ') + + logger.info(f"{pad}{content.description}") + + show_current_object = True + while show_current_object: + logger.info(f"{pad}Record Title: {content.title}") + + logger.debug(f"Fields: {content.fields}") + + # Display the fields and return a list of fields are editable. + editable = self._prompt_display_fields(content=content, pad=pad) + if vertex is not None: + self._prompt_display_relationships(vertex=vertex, content=content, pad=pad) + + while True: + + shared_folder_uid = content.shared_folder_uid + if shared_folder_uid is None: + shared_folder_uid = gateway_context.default_shared_folder_uid + + count_prompt = "" + if item_count > 0: + count_prompt = f"[{item_count - items_left + 1}/{item_count}]" + edit_add_prompt = f"{count_prompt} " + if len(editable) > 0: + edit_add_prompt += f"(E)dit, " + + shared_folders = gateway_context.get_shared_folders(vault) + if dry_run is False: + if len(shared_folders) > 1: + folder_name = next((x['name'] + for x in shared_folders + if x['uid'] == shared_folder_uid), + None) + edit_add_prompt += f"(A)dd to {folder_name}, "\ + f"Add to (F)older, " + else: + if dry_run is False: + edit_add_prompt += f"(A)dd, " + prompt = f"{edit_add_prompt}(S)kip, (I)gnore, (Q)uit" + + command = "a" + if add_all is False: + command = input(f"{pad}{prompt}> ").lower() + if (command == "a" or command == "f") and dry_run is False: + + logger.info(f"{pad}Adding record to save queue.") + logger.info("") + + if command == "f": + shared_folder_uid = self._get_shared_folder(vault, pad, gateway_context) + + content.shared_folder_uid = shared_folder_uid + + # This happens when the record is a pamUser and parent resource record does not have an + # administrator. + if content.record_type == "pamUser" and resource_has_admin is False: + + logger.info(f"{parent_content.description} does not have an administrator.") + if (hasattr(parent_content.item, "admin_reason") and + parent_content.item.admin_reason is not None): + logger.info("") + logger.info(parent_content.item.admin_reason) + logger.info("") + + while True: + + yn = input("Do you want to make this user the administrator? [Y/N]> ").lower() + if yn == "": + continue + if yn[0] == "n": + break + if yn[0] == "y": + acl.is_admin = True + break + + return PromptResult( + action=PromptActionEnum.ADD, + acl=acl, + content=content + ) + + elif command == "e" and dry_run is False: + self._edit_record(content, pad, editable) + break + + elif command == "i": + + logger.info(f"{pad}Creating an ignore rule for record.") + return PromptResult( + action=PromptActionEnum.IGNORE, + acl=acl, + content=content + ) + + elif command == "s": + logger.info(f"{pad}Skipping record.") + + return PromptResult( + action=PromptActionEnum.SKIP, + acl=acl, + content=content + ) + elif command == "q": + raise QuitException() + logger.info("") + + return PromptResult( + action=PromptActionEnum.SKIP, + acl=acl, + content=content + ) + + def _find_user_record(self, + bulk_convert_records: List[BulkRecordConvert], + context: Optional[KeeperParams] = None, + gateway_context: Optional[GatewayContext] = None, + record_link: Optional[RecordLink] = None) -> Tuple[Optional[vault_record.TypedRecord], bool]: + + vault = context.vault + + vault.vault_data.sync_data = True + + shared_record_uids = [] + for shared_folder in gateway_context.get_shared_folders(vault): + folder = shared_folder.get("folder") + if "records" in folder: + for record in folder["records"]: + shared_record_uids.append(record.get("record_uid")) + + converting_list = [x.record_uid for x in bulk_convert_records] + + logger.debug(f"shared folders record uid {shared_record_uids}") + + while True: + user_search = input("Enter an user to search for [ENTER/RETURN to quit]> ") + if user_search == "": + logger.error(f"No search terms, not performing search.") + return None, False + + user_records = list(vault.vault_data.find_records( + criteria=user_search, + record_version=3, + record_type=None + )) + if len(user_records) == 0: + logger.error(f"Could not find any records that contain the search text.") + return None, False + + # Find usable admin records. + admin_search_results = [] + for record in user_records: + + user_record = vault.vault_data.get_record(record.record_uid) + if user_record.record_type == "pamUser": + logger.debug(f"{record.record_uid} is a pamUser") + + if record.record_uid in converting_list: + logger.debug(f"pamUser {user_record.title}, {user_record.record_uid} is being converted; " + "BAD for search") + admin_search_results.append( + AdminSearchResult( + record=user_record, + is_directory_user=False, + is_pam_user=True, + being_used=True + ) + ) + continue + + if user_record.record_uid not in shared_record_uids: + logger.debug(f"pamUser {record.title}, {user_record.record_uid} not in shared " + "folder, BAD for search") + continue + + record_vertex = record_link.get_record_link(user_record.record_uid) + is_directory_user = False + if record_vertex is not None: + parent_record_uid = record_link.get_parent_record_uid(user_record.record_uid) + parent_record = vault.vault_data.get_record(parent_record_uid) + if parent_record is not None: + is_directory_user = self._is_directory_user(parent_record.record_type) + if not is_directory_user: + logger.debug(f"pamUser parent for {user_record.title}, " + "{user_record.record_uid} is not a directory; BAD for search") + continue + + logger.debug(f"pamUser {user_record.title}, {user_record.record_uid} is a directory user; " + "good for search") + + else: + logger.debug(f"pamUser {user_record.title}, {user_record.record_uid} does not a parent; " + "good for search") + else: + logger.debug(f"pamUser {user_record.title}, {user_record.record_uid} does not have record " + "linking vertex; good for search") + + admin_search_results.append( + AdminSearchResult( + record=user_record, + is_directory_user=is_directory_user, + is_pam_user=True, + being_used=False + ) + ) + + else: + logger.debug(f"{record.record_uid} is NOT a pamUser") + record_data = vault.vault_data.load_record(record.record_uid) + login_field = next((x for x in record_data.fields if x.type == "login"), None) + password_field = next((x for x in record_data.fields if x.type == "password"), None) + private_key_field = next((x for x in record_data.fields if x.type == "keyPair"), None) + + if login_field is not None and (password_field is not None or private_key_field is not None): + admin_search_results.append( + AdminSearchResult( + record=record, + is_directory_user=False, + is_pam_user=False + ) + ) + logger.debug(f"{record.title} is has credentials, good for search") + else: + logger.debug(f"{record.title} is missing full credentials, BAD for search") + + if len(admin_search_results) == 0: + logger.error(f"Could not find any available records.") + return None, False + + user_index = 1 + admin_search_results = sorted(admin_search_results, + key=lambda x: x.is_pam_user, + reverse=True) + + has_local_user = False + for admin_search_result in admin_search_results: + is_local_user = False + if admin_search_result.record.record_type != "pamUser": + has_local_user = True + is_local_user = True + + index_str = user_index + if admin_search_result.being_used: + index_str = "-" * len(str(index_str)) + + logger.info(f"[{index_str}] " + f"{'* ' if is_local_user is True else ''}" + f"{admin_search_result.record.title} " + f'{"(Directory User) " if admin_search_result.is_directory_user is True else ""}' + f'{"(Already taken)" if admin_search_result.being_used is True else ""}') + user_index += 1 + + if has_local_user: + logger.info(f"* Not a PAM User record. " + f"A PAM User would be generated from this record.") + + select = input("Enter line number of user record to use, enter/return to refine the search, " + f"or (Q) to quit search. > ").lower() + if select == "": + continue + elif select[0] == "q": + return None, False + else: + try: + selected = admin_search_results[int(select) - 1] + if selected.being_used: + logger.error(f"Cannot select a record that has already been taken. " + f"Another record is using this local user as its administrator.") + return None, False + admin_record = selected.record + return admin_record, selected.is_directory_user + except IndexError: + logger.error(f"Entered row index does not exists.") + continue + + return None, False + + @staticmethod + def _handle_admin_record_from_record(record: vault_record.TypedRecord, + content: DiscoveryObject, + context: Optional[KeeperParams] = None, + gateway_context: Optional[GatewayContext] = None) -> Optional[PromptResult]: + + vault = context.vault + + if record.record_type == "pamUser": + return PromptResult( + action=PromptActionEnum.ADD, + acl=UserAcl(is_admin=True), + record_uid=record.record_uid, + ) + + login_field = next((x for x in record.fields if x.type == "login"), None) + password_field = next((x for x in record.fields if x.type == "password"), None) + private_key_field = next((x for x in record.fields if x.type == "keyPair"), None) + + content.set_field_value("login", login_field.value) + if password_field is not None: + content.set_field_value("password", password_field.value) + if private_key_field is not None: + value = private_key_field.value + if value is not None and len(value) > 0: + value = value[0] + private_key = value.get("privateKey") + if private_key is not None: + content.set_field_value("private_key", private_key) + + shared_folders = gateway_context.get_shared_folders(vault) + if len(shared_folders) == 0: + while True: + yn = input(f"Create a PAM User record from {record.title}? [Y/N]> ").lower() + if yn == "": + continue + elif yn[0] == "n": + return None + elif yn[0] == "y": + content.shared_folder_uid = gateway_context.default_shared_folder_uid + else: + folder_name = next((x['name'] + for x in shared_folders + if x['uid'] == gateway_context.default_shared_folder_uid), + None) + while True: + shared_folders = gateway_context.get_shared_folders(vault) + if len(shared_folders) > 1: + afq = input(f"(A)dd user to {folder_name}, " + f"Add user to (F)older, " + f"(Q)uit > ").lower() + else: + afq = input(f"(A)dd user, " + f"(Q)uit > ").lower() + + if afq == "": + continue + if afq[0] == "a": + content.shared_folder_uid = gateway_context.default_shared_folder_uid + break + elif afq[0] == "f": + shared_folder_uid = PAMGatewayActionDiscoverResultProcessCommand._get_shared_folder( + vault, "", gateway_context) + if shared_folder_uid is not None: + content.shared_folder_uid = shared_folder_uid + break + + return PromptResult( + action=PromptActionEnum.ADD, + acl=UserAcl(is_admin=True), + content=content, + note=f"This record replaces record {record.title} ({record.record_uid}). " + "The password on that record will not be rotated." + ) + + def _prompt_admin(self, + parent_vertex: DAGVertex, + content: DiscoveryObject, + acl: UserAcl, + bulk_convert_records: List[BulkRecordConvert], + indent: int = 0, + context: Optional[KeeperParams] = None, + gateway_context: Optional[GatewayContext] = None) -> Optional[PromptResult]: + + if content is None: + raise Exception("The admin content was not passed in to prompt the user.") + + parent_content = DiscoveryObject.get_discovery_object(parent_vertex) + + logger.info("") + vault = context.vault + while True: + + logger.info(f"{parent_content.description} does not have an administrator user.") + if hasattr(parent_content.item, "admin_reason") is True and parent_content.item.admin_reason is not None: + logger.info("") + logger.info(parent_content.item.admin_reason) + logger.info("") + + action = input("Would you like to (A)dd new administrator user, (F)ind an existing admin, or (S)kip add? > ").lower() + + if action == "": + continue + + if action[0] == 'a': + prompt_result = self._prompt( + vault=vault, + gateway_context=gateway_context, + vertex=None, + parent_vertex=parent_vertex, + content=content, + acl=acl, + indent=indent + 2, + block_auto_add=True + ) + login = content.get_field_value("login") + if login is None or login == "": + logger.error("A value is needed for the login field.") + continue + + logger.info(f"Adding admin record to save queue.") + return prompt_result + elif action[0] == 'f': + logger.info("") + record, is_directory_user = self._find_user_record(context=context, + gateway_context=gateway_context, + bulk_convert_records=bulk_convert_records) + if record is not None: + admin_prompt_result = self._handle_admin_record_from_record( + record=record, + content=content, + context=context, + gateway_context=gateway_context + ) + if admin_prompt_result is not None: + if admin_prompt_result.action == PromptActionEnum.ADD: + admin_prompt_result.is_directory_user = is_directory_user + logger.info(f"Adding admin record to save queue.") + return admin_prompt_result + elif action[0] == 's': + return PromptResult( + action=PromptActionEnum.SKIP + ) + logger.info("") + + @staticmethod + def _display_auto_add_results(bulk_add_records: List[BulkRecordAdd]): + """ + Display the number of record created from rule engine ADD results and smart add function. + """ + add_count = len(bulk_add_records) + if add_count > 0: + logger.info("") + logger.info(f"From the rules, automatically queued {add_count} " + f"record{'' if add_count == 1 else 's'} to be added.") + + @staticmethod + def _prompt_confirm_add(bulk_add_records: List[BulkRecordAdd]): + + logger.info("") + count = len(bulk_add_records) + if count == 1: + msg = (f"There is 1 record queued to be added to your vault. " + f"Do you wish to add it? [Y/N]> ") + else: + msg = (f"There are {count} records queued to be added to your vault. " + f"Do you wish to add them? [Y/N]> ") + while True: + yn = input(msg).lower() + if yn == "": + continue + if yn[0] == "y": + return True + elif yn[0] == "n": + return False + logger.error("Did not get 'Y' or 'N'") + + @staticmethod + def _prepare_record(content: DiscoveryObject, context: Optional[KeeperParams] = None) -> Tuple[Any, str]: + + """ + Prepare the Vault record side. + + :params content: The discovery object instance. + :params context: Optionally, it will contain information set from the run() method. + :returns: Returns an unsaved Keeper record instance. + """ + # DEFINE V3 RECORD + # Create an instance of a vault record to structure the data + record = vault_record.TypedRecord() + record.type_name = content.record_type + record.record_uid = utils.generate_uid() + record.record_key = utils.generate_aes_key() + record.title = content.title + for field in content.fields: + record_field = vault_record.TypedField.create_field(field_type=field.type, field_label=field.label, required=field.required) + record.fields.append(record_field) + + vault = context.vault + folder = vault.vault_data.get_folder(content.shared_folder_uid) + folder_key = None + if folder.folder_type == 'shared_folder_folder': + shared_folder_uid = folder.folder_scope_uid + elif folder.folder_type == 'shared_folder': + shared_folder_uid = folder.folder_uid + else: + shared_folder_uid = None + if shared_folder_uid and shared_folder_uid in vault.vault_data._shared_folders: + shared_folder = vault.vault_data.get_folder(shared_folder_uid) + folder_key = shared_folder.folder_key + + # DEFINE PROTOBUF FOR RECORD + record_add_protobuf = record_pb2.RecordAdd() + record_add_protobuf.record_uid = utils.base64_url_decode(record.record_uid) + record_add_protobuf.record_key = crypto.encrypt_aes_v2(record.record_key, context.vault.keeper_auth.auth_context.data_key) + record_add_protobuf.client_modified_time = utils.current_milli_time() + record_add_protobuf.folder_type = record_pb2.user_folder + if folder: + record_add_protobuf.folder_uid = utils.base64_url_decode(folder.folder_uid) + if folder.folder_type == 'shared_folder': + record_add_protobuf.folder_type = record_pb2.shared_folder + elif folder.folder_type == 'shared_folder_folder': + record_add_protobuf.folder_type = record_pb2.shared_folder_folder + if folder_key: + record_add_protobuf.folder_key = crypto.encrypt_aes_v2(record.record_key, folder_key) + + data = vault_extensions.extract_typed_record_data(record, vault.vault_data.get_record_type_by_name(record.record_type)) + json_data = vault_extensions.get_padded_json_bytes(data) + record_add_protobuf.data = crypto.encrypt_aes_v2(json_data, record.record_key) + + if context.vault.keeper_auth.auth_context.enterprise_ec_public_key: + audit_data = vault_extensions.extract_audit_data(record) + if audit_data: + record_add_protobuf.audit.version = 0 + record_add_protobuf.audit.data = crypto.encrypt_ec( + json.dumps(audit_data).encode('utf-8'), context.vault.keeper_auth.auth_context.enterprise_ec_public_key) + + return record_add_protobuf, record.record_uid + + @classmethod + def _create_records(cls, bulk_add_records: List[BulkRecordAdd], context: KeeperParams, gateway_context: GatewayContext) -> BulkProcessResults: + """ + Create Vault records, setup rotation settings, and configure the resource (if resource). + """ + + if len(bulk_add_records) == 1: + logger.info("Adding the record to the Vault ...") + else: + logger.info(f"Adding {len(bulk_add_records)} records to the Vault ...") + + build_process_results = BulkProcessResults() + + ############################################################################################################## + # + # STEP 1 - Batch add new records + + # Generate a list of RecordAdd instance. + record_add_list = [r.record for r in bulk_add_records] # type: List[record_pb2.RecordAdd] + + records_per_request = 999 + + add_results = [] + logger.debug("adding record in batches") + logger.info("batch record create: ") + sys.stdout.flush() + while record_add_list: + logger.info(".") + sys.stdout.flush() + logger.debug(f"* adding batch") + rq = record_pb2.RecordsAddRequest() + rq.records.extend(record_add_list[:records_per_request]) + record_add_list = record_add_list[records_per_request:] + rs = context.vault.keeper_auth.execute_auth_rest('vault/records_add', rq, response_type=record_pb2.RecordsModifyResponse) + add_results.extend(rs.records) + logger.info("") + sys.stdout.flush() + + logger.debug(f"add_result: {add_results}") + + if len(add_results) != len(bulk_add_records): + logger.debug(f"attempted to batch add {len(bulk_add_records)} record(s), " + f"only have {len(add_results)} results.") + + ############################################################################################################## + # + # STEP 2 - Add rotation settings for user and resource configuration for resources + + created_cache = [] + + logger.info("add rotation settings: ") + sys.stdout.flush() + for bulk_record in bulk_add_records: + if bulk_record.record_uid in created_cache: + logger.debug(f"found a duplicate of record uid: {bulk_record.record_uid}") + continue + logger.info(".") + sys.stdout.flush() + + pb_add_record = bulk_record.record + title = bulk_record.title + + rotation_disabled = False + + result = None + for x in add_results: + logger.debug(f"{pb_add_record.record_uid} vs {x.record_uid}") + if pb_add_record.record_uid == x.record_uid: + result = x + break + + if result is None: + build_process_results.failure.append( + BulkRecordFail( + title=title, + error="No status on addition to Vault. Cannot determine if added or not." + ) + ) + logger.debug(f"Did not get a result when adding record {title}") + continue + + # Check if addition failed. If it did fail, don't add the rotation settings. + success = (result.status == record_pb2.RecordModifyResult.DESCRIPTOR.values_by_name['RS_SUCCESS'].number) + status = record_pb2.RecordModifyResult.DESCRIPTOR.values_by_number[result.status].name + + if not success: + build_process_results.failure.append( + BulkRecordFail( + title=title, + error=status + ) + ) + logger.debug(f"Had problem adding record for {title}: {status}") + continue + + # Only set the rotation setting if the record is a PAM User. + if bulk_record.record_type == PAM_USER: + + rq = router_pb2.RouterRecordRotationRequest() + rq.recordUid = utils.base64_url_decode(bulk_record.record_uid) + rq.revision = 0 + + # Set the gateway/configuration that this record should be connected. + rq.configurationUid = utils.base64_url_decode(gateway_context.configuration_uid) + + if bulk_record.parent_record_uid is not None: + rq.resourceUid = utils.base64_url_decode(bulk_record.parent_record_uid) + + rq.schedule = '' + rq.pwdComplexity = b'' + rq.disabled = rotation_disabled + + router_utils.router_set_record_rotation_information(context.vault, rq) + + # A LINK edge will be created between the configuration and resource. + # If there is an admin user, it will be set on the resource. + else: + + # This will create a LINK between the PAM Configuration and the resource. + rq = pam_pb2.PAMResourceConfig() + rq.recordUid = utils.base64_url_decode(bulk_record.record_uid) + rq.networkUid = utils.base64_url_decode(gateway_context.configuration_uid) + if bulk_record.admin_uid: + rq.adminUid = utils.base64_url_decode(bulk_record.admin_uid) + + router_utils.router_configure_resource(context.vault, rq) + + created_cache.append(bulk_record.record_uid) + + build_process_results.success.append( + BulkRecordSuccess( + title=title, + record_uid=bulk_record.record_uid + ) + ) + logger.info("") + sys.stdout.flush() + + context.sync_data = True + + return build_process_results + + @classmethod + def _convert_records(cls, bulk_convert_records: List[BulkRecordConvert], context: KeeperParams, gateway_context: Optional[GatewayContext] = None): + + vault = context.vault + for bulk_convert_record in bulk_convert_records: + + record = vault.vault_data.get_record(bulk_convert_record.record_uid) + + rotation_disabled = False + + rq = router_pb2.RouterRecordRotationRequest() + rq.recordUid = utils.base64_url_decode(bulk_convert_record.record_uid) + + record_rotation_revision = context.get_record_rotation(bulk_convert_record.record_uid) + rq.revision = record_rotation_revision.revision if record_rotation_revision else 0 + + # Set the gateway/configuration that this record should be connected. + rq.configurationUid = utils.base64_url_decode(gateway_context.configuration_uid) + + # Only set the resource if the record type is a PAM User. + # Machines, databases, and directories have a login/password in the record that indicates who the admin is. + if record.record_type == "pamUser" and bulk_convert_record.parent_record_uid is not None: + rq.resourceUid = utils.base64_url_decode(bulk_convert_record.parent_record_uid) + + rq.schedule = '' + rq.pwdComplexity = b'' + rq.disabled = rotation_disabled + + router_utils.router_set_record_rotation_information(context.vault, rq) + + vault.sync_down(force=True) + context.refresh_record_rotations() + + @staticmethod + def _get_directory_info(domain: str, + skip_users: bool = False, + context: Optional[KeeperParams] = None, + gateway_context: Optional[GatewayContext] = None) -> Optional[DirectoryInfo]: + """ + Get information about this record from the vault records. + """ + + directory_info = DirectoryInfo() + + vault = context.vault + + for directory_record in vault.vault_data.find_records(criteria=None, record_type="pamDirectory", record_version=None): + directory_record = vault.vault_data.load_record(directory_record.record_uid) + + info = context.get_record_rotation(directory_record.record_uid) + if info is None: + continue + + # Make sure this user is part of this gateway. + if info.configuration_uid != gateway_context.configuration_uid: + continue + + domain_field = directory_record.get_typed_field("text", label="domainName") + if len(domain_field.value) == 0 or domain_field.value[0] == "": + continue + + if domain_field.value[0].lower() != domain.lower(): + continue + + directory_info.directory_record_uids.append(directory_record.record_uid) + + if directory_info.has_directories is True and skip_users is False: + + for user_record in vault.vault_data.find_records(criteria=None, record_type="pamUser", record_version=None): + info = context.get_record_rotation(user_record.record_uid) + if info is None: + continue + + if info.resource_uid is None or info.resource_uid == "": + continue + + # If the user's belongs to a directory, and add it to the directory user list. + if info.resource_uid in directory_info.directory_record_uids: + directory_info.directory_user_record_uids.append(user_record.record_uid) + + return directory_info + + @staticmethod + def remove_job(context: KeeperParams, configuration_record: vault_record.KeeperRecord, job_id: str): + + try: + jobs = Jobs(record=configuration_record, context=context) + jobs.cancel(job_id) + logger.info(f"No items left to process. Removing completed discovery job.") + except Exception as err: + logger.error(err) + logger.error(f"No items left to process. Failed to remove discovery job.") + + def preview(self, job_item: JobItem, context: KeeperParams, gateway_context: GatewayContext, debug_level: int = 0): + + sync_point = job_item.sync_point + infra = Infrastructure(record=gateway_context.configuration, + context=context, + logger=logger, + debug_level=debug_level) + infra.load(sync_point) + + configuration = None + try: + configuration = infra.get_root.has_vertices()[0] + except (Exception,): + logger.error(f"Could not find the configuration in the infrastructure graph. " + f"Has discovery been run for this gateway?") + + record_type_to_vertices_map = sort_infra_vertices(configuration) + + def _print_resource(rt: str, rule_result: str): + + printed_something = False + + titles = { + "pamDirectory": "Directories", + "pamMachine": "Machines", + "pamDatabase": "Databases" + } + + for rv in record_type_to_vertices_map[rt]: + if not rv.active or not rv.has_data: + continue + user_vertices = rv.has_vertices() + + user_list = [] + for user_vertex in user_vertices: + if not user_vertex.active or not user_vertex.has_data: + continue + + user_content = DiscoveryObject.get_discovery_object(user_vertex) + if user_content.ignore_object or self._record_lookup(user_content.record_uid, context) is not None: + continue + + user_list.append(f" . {user_content.item.user} ({user_content.name})") + + c = DiscoveryObject.get_discovery_object(rv) + if len(user_list) == 0 and c.action_rules_result != rule_result or c.ignore_object: + continue + + has_record = "" + record_uid = c.record_uid + if record_uid is not None: + if self._record_lookup(record_uid, context): + has_record = f" (record exists: {record_uid})" + if len(user_list) == 0: + continue + else: + record_uid = None + + if c.action_rules_result != rule_result and not record_uid: + continue + + title = titles.get(c.record_type) + if title is not None: + logger.info(f" {(title)}") + titles[c.record_type] = None + + ip = "" + if c.item.host != c.item.ip: + ip = f" ({c.item.ip})" + + with_admin = "" + if c.admin_uid is not None and not record_uid: + with_admin = f" with Administrator UID {c.admin_uid}" + + logger.info(f" * {c.description}{ip}{with_admin}{has_record}") + printed_something = True + + if record_uid: + for user in user_list: + logger.info(user) + + return printed_something + + def _print_cloud_user(rt: str, rule_result: str): + + title = "Users" + + for user_vertex in record_type_to_vertices_map[rt]: + if not user_vertex.active or not user_vertex.has_data: + continue + + uc = DiscoveryObject.get_discovery_object(user_vertex) + + if (uc.action_rules_result != rule_result + or uc.ignore_object + or self._record_lookup(uc.record_uid, context) is not None): + continue + + if title is not None: + logger.info(f" {(title)}") + title = None + + logger.info(f" * {uc.item.user} ({uc.name})") + + logger.info("") + logger.info("Will Be Automatically Added") + nothing_to_print = True + for record_type in sorted(record_type_to_vertices_map, key=lambda i: VERTICES_SORT_MAP[i]['order']): + if record_type == "pamUser": + _print_cloud_user("pamUser", rule_result="add") + else: + if _print_resource(record_type, rule_result="add"): + nothing_to_print = False + if nothing_to_print: + logger.info(f" {'No records will be automatically added.'}") + + logger.info("") + logger.info("Will Be Prompted For") + nothing_to_print = True + for record_type in sorted(record_type_to_vertices_map, key=lambda i: VERTICES_SORT_MAP[i]['order']): + if record_type == "pamUser": + _print_cloud_user("pamUser", rule_result="prompt") + else: + if _print_resource(record_type, rule_result="prompt"): + nothing_to_print = False + if nothing_to_print: + logger.info(f" {'No items will be prompted.'}") + + logger.info("") + + def execute(self, context: KeeperParams, **kwargs): + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + vault = context.vault + + do_preview = kwargs.get("do_preview", False) + job_id = kwargs.get("job_id") + add_all = kwargs.get("add_all", False) + debug_level = kwargs.get("debug_level", 0) + + all_gateways = GatewayContext.all_gateways(vault) + + configuration_records = GatewayContext.get_configuration_records(vault=vault) + for configuration_record in configuration_records: + + gateway_context = GatewayContext.from_configuration_uid(vault=vault, + configuration_uid=configuration_record.record_uid, + gateways=all_gateways) + if gateway_context is None: + continue + + record_cache = self._build_record_cache( + context=context, + gateway_context=gateway_context + ) + + # Get the current job. + # There can only be one active job. + jobs = Jobs(record=configuration_record, context=context, logger=logger, debug_level=debug_level) + job_item = jobs.current_job + if job_item is None: + continue + + if job_item.job_id != job_id: + continue + + if job_item.end_ts is None: + logger.error(f'Discovery job is currently running. Cannot process.') + return + if job_item.success is False: + logger.error(f'Discovery job failed. Cannot process.') + return + + if do_preview: + self.preview( + job_item=job_item, + context=context, + gateway_context=gateway_context, + ) + return + + process = Process( + record=configuration_record, + job_id=job_item.job_id, + context=context, + logger=logger, + debug_level=debug_level, + ) + + if add_all: + logger.info(f"The ADD ALL flag has been set. All found items will be added.") + logger.info("") + + try: + results = process.run( + prompt_func=self._prompt, + + record_prepare_func=self._prepare_record, + + record_lookup_func=self._record_lookup, + + # Add record to the vault, protobuf, and record-linking graph + record_create_func=self._create_records, + + # This function will take existing pamUser record and make them belong to this + # gateway. + record_convert_func=self._convert_records, + + # If quit, confirm if the user wants to add records + prompt_confirm_add_func=self._prompt_confirm_add, + + # Prompt user for an admin for a resource + prompt_admin_func=self._prompt_admin, + + # Pass method that will display auto added records. + auto_add_result_func=self._display_auto_add_results, + + # A function to get directory users + directory_info_func=self._get_directory_info, + + # Record link will be added by Process run as "record_link" + context=context, + + # Provides a cache of the record key to record UID. + record_cache=record_cache + ) + + logger.debug(f"Results: {results}") + + logger.info("") + if results is not None and results.num_results > 0: + logger.info(f"Successfully added {results.success_count} " + f"record{'s' if results.success_count != 1 else ''}.") + if results.has_failures: + logger.info(f"There were {results.failure_count} " + f"failure{'s' if results.failure_count != 1 else ''}.") + for fail in results.failure: + logger.info(f" * {fail.title}: {fail.error}") + + if process.no_items_left is True: + self.remove_job(context=context, configuration_record=configuration_record, job_id=job_id) + else: + logger.info(f"No records have been added.") + + except NoDiscoveryDataException: + logger.info(f"All items have been added for this discovery job.") + self.remove_job(context=context, configuration_record=configuration_record, job_id=job_id) + + except Exception as err: + logger.error(f"Could not process discovery: {err}") + raise err + + return + + logger.info(f"Could not find the Discovery job.") + logger.info("") + + +class PAMDiscoveryRuleCommand(base.GroupCommand): + + def __init__(self): + super().__init__('PAM Discovery Rule') + self.register_command(PAMGatewayActionDiscoverRuleAddCommand(), 'add', 'a') + self.register_command(PAMGatewayActionDiscoverRuleListCommand(), 'list', 'l') + self.register_command(PAMGatewayActionDiscoverRuleRemoveCommand(), 'remove', 'r') + self.register_command(PAMGatewayActionDiscoverRuleUpdateCommand(), 'update', 'u') + self.default_verb = 'list' diff --git a/keepercli-package/src/keepercli/commands/pam/discovery/rule_commands.py b/keepercli-package/src/keepercli/commands/pam/discovery/rule_commands.py new file mode 100644 index 00000000..760a2658 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/discovery/rule_commands.py @@ -0,0 +1,432 @@ +import argparse +from typing import List + +from ....params import KeeperParams +from ....helpers import router_utils +from .... import api +from .__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from ..pam_dto import GatewayAction, GatewayActionDiscoverRuleValidate, GatewayActionDiscoverRuleValidateInputs + +from keepersdk.helpers.keeper_dag.dag_types import Statement +from keepersdk.helpers.keeper_dag.rule import Rules, ActionRuleItem, RuleItem, RuleTypeEnum, RuleActionEnum +from keepersdk.proto import pam_pb2 + + +logger = api.get_logger() + + +class PAMGatewayActionDiscoverRuleListCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover rule list') + PAMGatewayActionDiscoverRuleListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--search', '-s', required=False, dest='search', action='store', + help='Search for rules.') + + @staticmethod + def print_rule_table(rule_list: List[RuleItem]): + + logger.info("") + logger.info(f"{'Rule ID'.ljust(15, ' ')} " + f"{'Name'.ljust(20, ' ')} " + f"{'Action'.ljust(6, ' ')} " + f"{'Priority'.ljust(8, ' ')} " + f"{'Case'.ljust(12, ' ')} " + f"{'Added'.ljust(19, ' ')} " + f"{'Shared Folder UID'.ljust(22, ' ')} " + f"{'Admin UID'.ljust(22, ' ')} " + "Rule" + ) + + logger.info(f"{''.ljust(15, '=')} " + f"{''.ljust(20, '=')} " + f"{''.ljust(6, '=')} " + f"{''.ljust(8, '=')} " + f"{''.ljust(12, '=')} " + f"{''.ljust(19, '=')} " + f"{''.ljust(22, '=')} " + f"{''.ljust(22, '=')} " + f"{''.ljust(10, '=')} ") + + for rule in rule_list: + if rule.case_sensitive: + ignore_case_str = "Sensitive" + else: + ignore_case_str = "Insensitive" + + shared_folder_uid = "" + if rule.shared_folder_uid is not None: + shared_folder_uid = rule.shared_folder_uid + + admin_uid = "" + if rule.admin_uid is not None: + admin_uid = rule.admin_uid + + name = "" + if rule.name is not None: + name = rule.name + + action_value = f"NONE" + if rule.action is not None: + action_value = rule.action.value + + logger.info(f"{rule.rule_id.ljust(14, ' ')} " + f"{name[:20].ljust(20, ' ')} " + f"{action_value.ljust(6, ' ')} " + f"{str(rule.priority).rjust(8, ' ')} " + f"{ignore_case_str.ljust(12, ' ')} " + f"{rule.added_ts_str.ljust(19, ' ')} " + f"{shared_folder_uid.ljust(22, ' ')} " + f"{admin_uid.ljust(22, ' ')} " + f"{Rules.make_action_rule_statement_str(rule.statement)}") + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + configuration_uid = kwargs.get('configuration_uid') + vault = context.vault + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + rules = Rules(record=gateway_context.configuration, context=context) + rule_list = rules.rule_list(rule_type=RuleTypeEnum.ACTION, + search=kwargs.get("search")) # type: List[RuleItem] + if len(rule_list) == 0: + logger.info("") + text = f"There are no rules. " \ + f"Use 'pam action discover rule add -g {gateway_context.gateway_uid} " + if configuration_uid: + text += f"-c {gateway_context.configuration_uid}' " + text += f"to create rules." + logger.info(text) + return + + self.print_rule_table(rule_list=rule_list) + + +class PAMGatewayActionDiscoverRuleAddCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover rule add') + PAMGatewayActionDiscoverRuleAddCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + + parser.add_argument('--action', '-a', required=True, choices=['add', 'ignore', 'prompt'], + dest='rule_action', action='store', help='Action to take if rule matches') + parser.add_argument('--priority', '-p', required=True, dest='priority', action='store', type=int, + help='Rule execute priority') + parser.add_argument('--name', '-n', required=False, dest='name', action='store', type=str, + help='Rule name') + parser.add_argument('--ignore-case', required=False, dest='ignore_case', action='store_true', + help='Ignore value case. Rule value must be in lowercase.') + parser.add_argument('--shared-folder-uid', required=False, dest='shared_folder_uid', + action='store', help='Folder to place record.') + parser.add_argument('--admin-uid', required=False, dest='admin_uid', + action='store', help='Admin record UID to use for resource.') + parser.add_argument('--statement', '-s', required=True, dest='statement', action='store', + help='Rule statement') + + @staticmethod + def validate_rule_statement(context: KeeperParams, gateway_context: GatewayContext, statement: str) \ + -> List[Statement]: + + # Send rule the gateway to be validated. The rule is encrypted. It might contain sensitive information. + action_inputs = GatewayActionDiscoverRuleValidateInputs( + configuration_uid=gateway_context.configuration_uid, + statement=gateway_context.encrypt_str(statement) + ) + conversation_id = GatewayAction.generate_conversation_id() + router_response = router_utils.router_send_action_to_gateway( + context=context, + gateway_action=GatewayActionDiscoverRuleValidate( + inputs=action_inputs, + conversation_id=conversation_id), + message_type=pam_pb2.CMT_DISCOVERY, + is_streaming=False, + destination_gateway_uid_str=gateway_context.gateway_uid + ) + + data = PAMGatewayActionDiscoverCommandBase.get_response_data(router_response) + + if data is None: + raise Exception("The router returned a failure.") + elif data.get("success") is False: + error = data.get("error") + raise Exception(f"The rule does not appear valid: {error}") + + statement_struct = data.get("statementStruct") + logger.debug(f"Rule Structure = {statement_struct}") + if not isinstance(statement_struct, list): + raise Exception(f"The structured rule statement is not a list.") + ret = [] + for item in statement_struct: + ret.append( + Statement( + field=item.get("field"), + operator=item.get("operator"), + value=item.get("value") + ) + ) + + return ret + + def execute(self, context: KeeperParams, **kwargs): + try: + gateway_uid = kwargs.get("gateway") + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway_uid, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway_uid}.") + return + + shared_folder_uid = kwargs.get("shared_folder_uid") + if shared_folder_uid is not None: + shared_folder_uids = gateway_context._shared_folders if gateway_context._shared_folders is not None else [] + exists = next((x for x in shared_folder_uids if x["uid"] == shared_folder_uid), None) + if exists is None: + logger.error(f"The shared folder UID {shared_folder_uid} is not part of this " + f"application/gateway. Valid shared folder UID are:") + for item in shared_folder_uids: + logger.error(f"* {item['uid']} - {item['name']}") + return + + statement = kwargs.get("statement") + statement_struct = self.validate_rule_statement( + context=context, + gateway_context=gateway_context, + statement=statement + ) + + shared_folder_uid = kwargs.get("shared_folder_uid") + if shared_folder_uid is not None and len(shared_folder_uid) != 22: + logger.error(f"The shared folder UID {shared_folder_uid} is not the correct length.") + return + + admin_uid = kwargs.get("admin_uid") + if admin_uid is not None and len(admin_uid) != 22: + logger.error(f"The admin UID {admin_uid} is not the correct length.") + return + + # If the rule passes its validation, then add control DAG + rules = Rules(record=gateway_context.configuration, context=context) + new_rule = ActionRuleItem( + name=kwargs.get("name"), + action=kwargs.get("rule_action"), + priority=kwargs.get("priority"), + case_sensitive=not kwargs.get("ignore_case", False), + shared_folder_uid=shared_folder_uid, + admin_uid=admin_uid, + statement=statement_struct, + enabled=True + ) + rules.add_rule(new_rule) + + logger.info(f"Rule has been added") + except Exception as err: + logger.error(f"Rule was not added: {err}") + + +class PAMGatewayActionDiscoverRuleUpdateCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover rule update') + PAMGatewayActionDiscoverRuleUpdateCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--rule-id', '-i', required=True, dest='rule_id', action='store', + help='Identifier for the rule') + parser.add_argument('--action', '-a', required=False, choices=['add', 'ignore', 'prompt'], + dest='rule_action', action='store', help='Update the action to take if rule matches') + parser.add_argument('--priority', '-p', required=False, dest='priority', action='store', type=int, + help='Update the rule execute priority') + parser.add_argument('--name', '-n', required=False, dest='name', action='store', type=str, + help='Rule name') + parser.add_argument('--ignore-case', required=False, dest='ignore_case', action='store_true', + help='Update the rule to ignore case') + parser.add_argument('--no-ignore-case', required=False, dest='ignore_case', action='store_false', + help='Update the rule to not ignore case') + parser.add_argument('--shared-folder-uid', required=False, dest='shared_folder_uid', + action='store', help='Update the folder to place record.') + parser.add_argument('--admin-uid', required=False, dest='admin_uid', + action='store', help='Admin record UID to use for resource.') + parser.add_argument('--clear-shared-folder-uid', required=False, dest='clear_shared_folder_uid', + action='store_true', help='Clear shared folder UID, use default.') + parser.add_argument('--clear-admin-uid', required=False, dest='clear_admin_uid', + action='store_true', help='Clear admin UID') + parser.add_argument('--statement', '-s', required=False, dest='statement', action='store', + help='Update the rule statement') + parser.add_argument('--active', required=False, dest='active', action='store_true', + help='Enable rule.') + parser.add_argument('--disable', required=False, dest='active', action='store_false', + help='Disable rule.') + parser.set_defaults(active=None, ignore_case=None) + + def execute(self, context: KeeperParams, **kwargs): + gateway = kwargs.get("gateway") + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + try: + rule_id = kwargs.get("rule_id") + rules = Rules(record=gateway_context.configuration, context=context) + rule_item = rules.get_rule_item(rule_type=RuleTypeEnum.ACTION, rule_id=rule_id) + if rule_item is None: + raise ValueError("Rule Id does not exist.") + + rule_action = kwargs.get("rule_action") + if rule_action is not None: + action = RuleActionEnum.find_enum(rule_action) + if action is None: + raise ValueError(f"The action does not look correct: {rule_action}") + rule_item.action = action + + priority = kwargs.get("priority") + if priority is not None: + logger.info(" * Changing the priority of the rule.") + rule_item.priority = priority + + ignore_case = kwargs.get("ignore_case") + if ignore_case is not None: + if ignore_case: + logger.info(" * Ignore the case of text.") + else: + logger.info(" * Make rule text case sensitive.") + + rule_item.case_sensitive = not ignore_case + + if kwargs.get("clear_shared_folder_uid"): + logger.info(" * Clearing shared folder.") + rule_item.shared_folder_uid = None + else: + shared_folder_uid = kwargs.get("shared_folder_uid") + if shared_folder_uid is not None: + if len(shared_folder_uid) != 22: + logger.error(f"The shared folder UID {shared_folder_uid} is not the correct length.") + logger.info(" * Changing shared folder UID.") + rule_item.shared_folder_uid = shared_folder_uid + + if kwargs.get("clear_admin_uid"): + logger.info(" * Clearing resource admin UID.") + rule_item.admin_uid = None + else: + admin_uid = kwargs.get("admin_uid") + if admin_uid is not None: + if len(admin_uid) != 22: + logger.error(f"The admin UID {admin_uid} is not the correct length.") + return + logger.info(" * Changing the resource admin UID.") + rule_item.admin_uid = admin_uid + + statement = kwargs.get("statement") + if statement is not None: + statement_struct = PAMGatewayActionDiscoverRuleAddCommand.validate_rule_statement( + context=context, + gateway_context=gateway_context, + statement=statement + ) + + logger.info(" * Changing the rule statement.") + rule_item.statement = statement_struct + + name = kwargs.get("name") + if name is not None: + logger.info(" * Changing the rule name.") + rule_item.name = name + + enabled = kwargs.get("active") + if enabled is not None: + if enabled: + logger.info(" * Enabling the rule.") + else: + logger.info(" * Disabling the rule.") + rule_item.enabled = enabled + + rules.update_rule(rule_item) + logger.info(f"Rule has been updated") + except Exception as err: + logger.error(f"Rule was not updated: {err}") + + +class PAMGatewayActionDiscoverRuleRemoveCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action discover rule remove') + PAMGatewayActionDiscoverRuleRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--rule-id', '-i', required=False, dest='rule_id', action='store', + help='Identifier for the rule') + parser.add_argument('--remove-all', required=False, dest='remove_all', action='store_true', + help='Remove all the rules.') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + gateway_context = GatewayContext.from_gateway(vault=context.vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + rule_id = kwargs.get("rule_id") + remove_all = kwargs.get("remove_all") + + if rule_id is None and remove_all is None: + logger.error(f'Either --rule-id or --remove-all are required.') + return + + try: + rules = Rules(record=gateway_context.configuration, context=context) + if remove_all: + rules.remove_all(RuleTypeEnum.ACTION) + logger.info(f"All rules removed.") + else: + + rule_item = rules.get_rule_item(rule_type=RuleTypeEnum.ACTION, rule_id=rule_id) + if rule_item is None: + raise ValueError("Rule Id does not exist.") + rules.remove_rule(rule_item) + + logger.info(f"Rule has been removed.") + except Exception as err: + if remove_all: + logger.error(f"Rules have NOT been removed: {err}") + else: + logger.error(f"Rule was not removed: {err}") diff --git a/keepercli-package/src/keepercli/commands/pam/keeper_pam.py b/keepercli-package/src/keepercli/commands/pam/keeper_pam.py index 92f4b52c..cd99959b 100644 --- a/keepercli-package/src/keepercli/commands/pam/keeper_pam.py +++ b/keepercli-package/src/keepercli/commands/pam/keeper_pam.py @@ -3,16 +3,27 @@ import requests from datetime import datetime +from .pam_config import PAMConfigListCommand, PAMConfigNewCommand, PAMConfigEditCommand, PAMConfigRemoveCommand +from .pam_gateway_action import (PAMGatewayActionServerInfoCommand, PAMGatewayActionRotateCommand, + PAMGatewayActionJobCommand, PAMDiscoveryCommand, PAMActionServiceCommand, + PAMActionSaasCommand, PAMDebugCommand) +from .pam_rotation import PAMCreateRecordRotationCommand, PAMListRecordRotationCommand, PAMRouterGetRotationInfo, PAMRouterScriptCommand +from .pam_connection import PAMConnectionEditCommand +from .pam_rbi import PAMRbiEditCommand +from .. import enterprise_utils from .. import base from ... import api from ...helpers import report_utils, router_utils, gateway_utils from ...params import KeeperParams + from keepersdk import utils +from keepersdk.vault import ksm_management logger = api.get_logger() + # Constants DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' MILLISECONDS_TO_SECONDS = 1000 @@ -67,6 +78,11 @@ class PAMControllerCommand(base.GroupCommand): def __init__(self): super().__init__('PAM Controller') self.register_command(PAMGatewayCommand(), 'gateway', 'g') + self.register_command(PAMConfigCommand(), 'config', 'c') + self.register_command(PAMGatewayActionCommand(), 'action', 'a') + self.register_command(PAMRotationCommand(), 'rotation', 'r') + self.register_command(PAMConnectionCommand(), 'connection', 'n') + self.register_command(PAMRbiCommand(), 'rbi', 'b') class PAMGatewayCommand(base.GroupCommand): @@ -75,10 +91,59 @@ def __init__(self): super().__init__('PAM Gateway') self.register_command(PAMGatewayListCommand(), 'list', 'l') self.register_command(PAMGatewayNewCommand(), 'new', 'n') + self.register_command(PAMGatewayEditCommand(), 'edit', 'e') self.register_command(PAMGatewayRemoveCommand(), 'remove', 'rm') self.register_command(PAMGatewaySetMaxInstancesCommand(), 'set-max-instances', 'smi') self.default_verb = 'list' + +class PAMConfigCommand(base.GroupCommand): + + def __init__(self): + super().__init__('PAM Configurations') + self.register_command(PAMConfigListCommand(), 'list', 'l') + self.register_command(PAMConfigNewCommand(), 'new', 'n') + self.register_command(PAMConfigEditCommand(), 'edit', 'e') + self.register_command(PAMConfigRemoveCommand(), 'remove', 'rm') + self.default_verb = 'list' + + +class PAMGatewayActionCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Gateway Action') + self.register_command(PAMGatewayActionServerInfoCommand(), 'gateway-info', 'i') + self.register_command(PAMGatewayActionRotateCommand(), 'rotate', 'r') + self.register_command(PAMGatewayActionJobCommand(), 'job-info', 'ji') + self.register_command(PAMGatewayActionJobCommand(), 'job-cancel', 'jc') + self.register_command(PAMDiscoveryCommand(), 'discover', 'd') + self.register_command(PAMActionServiceCommand(), 'service', 's') + self.register_command(PAMActionSaasCommand(), 'saas', 'sa') + self.register_command(PAMDebugCommand(), 'debug', 'd') + + +class PAMRotationCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Rotation') + self.register_command(PAMListRecordRotationCommand(), 'list', 'l') + self.register_command(PAMRouterGetRotationInfo(), 'info', 'i') + self.register_command(PAMCreateRecordRotationCommand(), 'edit', 'new') + self.register_command(PAMRouterScriptCommand(), 'script', 's') + self.default_verb = 'list' + + +class PAMConnectionCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Connection') + self.register_command(PAMConnectionEditCommand(), 'edit', 'e') + self.default_verb = 'edit' + +class PAMRbiCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Remote Browser Isolation') + self.register_command(PAMRbiEditCommand(), 'edit', 'e') + self.default_verb = 'edit' + + class PAMGatewayListCommand(base.ArgparseCommand): def __init__(self): @@ -104,7 +169,10 @@ def execute(self, context: KeeperParams, **kwargs): format_type = kwargs.get('format', 'table') enterprise_controllers_connected, is_router_down = self._fetch_connected_gateways(vault, is_force) - enterprise_controllers_all = gateway_utils.get_all_gateways(vault) + enterprise_controllers_all = list(context.pam_plugin.controllers.get_all_entities()) + if not enterprise_controllers_all: + context.pam_plugin.sync_down() + enterprise_controllers_all = list(context.pam_plugin.controllers.get_all_entities()) if not enterprise_controllers_all: return self._handle_no_gateways(format_type) @@ -190,8 +258,8 @@ def _process_gateways(self, vault, enterprise_controllers_all, connected_control gateways_data = [] for controller in enterprise_controllers_all: - gateway_uid_bytes = controller.controllerUid - gateway_uid_str = utils.base64_url_encode(controller.controllerUid) + gateway_uid_bytes = utils.base64_url_decode(controller.controller_uid) + gateway_uid_str = controller.controller_uid connected_instances = connected_controllers_dict.get(gateway_uid_bytes, []) ksm_app_info = self._get_ksm_app_info(vault, controller) @@ -213,7 +281,7 @@ def _process_gateways(self, vault, enterprise_controllers_all, connected_control def _get_ksm_app_info(self, vault, controller): """Retrieves KSM application information for a controller.""" - ksm_app_uid_str = utils.base64_url_encode(controller.applicationUid) + ksm_app_uid_str = controller.application_uid ksm_app = vault.vault_data.load_record(ksm_app_uid_str) if ksm_app: @@ -276,7 +344,7 @@ def _process_single_gateway(self, controller, gateway_uid_str, connected_instanc "ksm_app_name": ksm_app_info['ksm_app_name'], "ksm_app_uid": ksm_app_info['ksm_app_uid_str'], "ksm_app_accessible": ksm_app_info['ksm_app_accessible'], - "gateway_name": controller.controllerName, + "gateway_name": controller.controller_name, "gateway_uid": gateway_uid_str, "status": overall_status, "gateway_version": version @@ -285,11 +353,11 @@ def _process_single_gateway(self, controller, gateway_uid_str, connected_instanc if is_verbose: os_name, os_release, machine_type, os_version = self._extract_version_info(version_parts) gateway_data.update({ - "device_name": controller.deviceName, - "device_token": controller.deviceToken, + "device_name": controller.device_name, + "device_token": controller.device_token, "created_on": self._format_timestamp(controller.created), - "last_modified": self._format_timestamp(controller.lastModified), - "node_id": controller.nodeId, + "last_modified": self._format_timestamp(controller.last_modified), + "node_id": controller.node_id, "os": os_name, "os_release": os_release, "machine_type": machine_type, @@ -309,7 +377,7 @@ def _build_single_gateway_table_row(self, controller, gateway_uid_str, ksm_app_i """Builds a table row for a single gateway.""" row = [ ksm_app_info['ksm_app_info_plain'], - controller.controllerName, + controller.controller_name, gateway_uid_str, overall_status, version @@ -318,11 +386,11 @@ def _build_single_gateway_table_row(self, controller, gateway_uid_str, ksm_app_i if is_verbose: os_name, os_release, machine_type, os_version = self._extract_version_info(version_parts) row.extend([ - controller.deviceName, - controller.deviceToken, + controller.device_name, + controller.device_token, datetime.fromtimestamp(controller.created / MILLISECONDS_TO_SECONDS), - datetime.fromtimestamp(controller.lastModified / MILLISECONDS_TO_SECONDS), - controller.nodeId, + datetime.fromtimestamp(controller.last_modified / MILLISECONDS_TO_SECONDS), + controller.node_id, os_name, os_release, machine_type, @@ -341,7 +409,7 @@ def _process_pool_gateway(self, controller, gateway_uid_str, connected_instances "ksm_app_name": ksm_app_info['ksm_app_name'], "ksm_app_uid": ksm_app_info['ksm_app_uid_str'], "ksm_app_accessible": ksm_app_info['ksm_app_accessible'], - "gateway_name": controller.controllerName, + "gateway_name": controller.controller_name, "gateway_uid": gateway_uid_str, "status": overall_status, "instances": instances_data @@ -349,11 +417,11 @@ def _process_pool_gateway(self, controller, gateway_uid_str, connected_instances if is_verbose: gateway_data.update({ - "device_name": controller.deviceName, - "device_token": controller.deviceToken, + "device_name": controller.device_name, + "device_token": controller.device_token, "created_on": self._format_timestamp(controller.created), - "last_modified": self._format_timestamp(controller.lastModified), - "node_id": controller.nodeId + "last_modified": self._format_timestamp(controller.last_modified), + "node_id": controller.node_id }) gateways_data.append(gateway_data) @@ -401,7 +469,7 @@ def _build_pool_gateway_table_row(self, controller, gateway_uid_str, ksm_app_inf """Builds a table row for a pool gateway header.""" row = [ ksm_app_info['ksm_app_info_plain'], - controller.controllerName, + controller.controller_name, gateway_uid_str, overall_status, '' @@ -409,11 +477,11 @@ def _build_pool_gateway_table_row(self, controller, gateway_uid_str, ksm_app_inf if is_verbose: row.extend([ - controller.deviceName, - controller.deviceToken, + controller.device_name, + controller.device_token, datetime.fromtimestamp(controller.created / MILLISECONDS_TO_SECONDS), - datetime.fromtimestamp(controller.lastModified / MILLISECONDS_TO_SECONDS), - controller.nodeId, + datetime.fromtimestamp(controller.last_modified / MILLISECONDS_TO_SECONDS), + controller.node_id, '', '', '', '' ]) @@ -527,7 +595,8 @@ def execute(self, context: KeeperParams, **kwargs): token_expire_in_min = kwargs.get('token_expire_in_min') self._log_gateway_creation_params(gateway_name, ksm_app, token_expire_in_min) - one_time_token = gateway_utils.create_gateway(vault, gateway_name, ksm_app, token_expire_in_min) + ksm_app_info = ksm_management.get_secrets_manager_app(vault, ksm_app) + one_time_token = gateway_utils.create_gateway(vault, gateway_name, ksm_app_info.uid, token_expire_in_min) if is_return_value: return one_time_token @@ -557,6 +626,66 @@ def _display_token_info(self, ksm_app, gateway_name, token_expire_in_min, one_ti logger.info(TOKEN_SEPARATOR) +class PAMGatewayEditCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='dr-edit-gateway') + PAMGatewayEditCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', + help='Gateway UID or Name', action='store') + parser.add_argument('--name', '-n', required=False, dest='gateway_name', + help='Name of the Gateway', action='store') + parser.add_argument('--node-id', '-i', required=False, dest='node_id', + help='Node ID', action='store') + + def execute(self, context: KeeperParams, **kwargs): + self._validate_vault_and_permissions(context) + vault = context.vault + + gateway_uid = kwargs.get('gateway') + gateway_name = kwargs.get('gateway_name') + node_id = kwargs.get('node_id') + if not gateway_uid: + raise base.CommandError('Gateway UID is required') + + gateway = self._find_gateway(context, gateway_uid) + if not gateway: + raise base.CommandError(f'Gateway {gateway_uid} not found') + + if not node_id and not gateway_name: + logger.info("Nothing to do. Provide new name or node ID to edit the gateway.") + return + + if node_id: + node = enterprise_utils.NodeUtils.resolve_single_node(context.enterprise_data, node_id) + if not node: + raise base.CommandError(f'Node {node_id} not found') + node_id = node.node_id + else: + node_id = gateway.node_id + + if not gateway_name: + gateway_name = gateway.controller_name + + gateway_utils.edit_gateway( + vault, utils.base64_url_decode(gateway.controller_uid), gateway_name, node_id) + logger.info('Gateway %s has been edited.', gateway.controller_name) + + def _validate_vault_and_permissions(self, context: KeeperParams): + """Validates that vault is initialized and user has enterprise admin permissions.""" + if not context.vault: + raise ValueError(ERROR_VAULT_NOT_INITIALIZED) + base.require_enterprise_admin(context) + + def _find_gateway(self, context: KeeperParams, gateway_uid): + """Finds a gateway by UID or name.""" + gateway = context.pam_plugin.controllers.get_entity(gateway_uid) + return gateway + class PAMGatewayRemoveCommand(base.ArgparseCommand): def __init__(self): @@ -574,11 +703,11 @@ def execute(self, context: KeeperParams, **kwargs): vault = context.vault gateway_uid = kwargs.get('gateway') - gateway = self._find_gateway(vault, gateway_uid) + gateway = self._find_gateway(context, gateway_uid) if gateway: - gateway_utils.remove_gateway(vault, gateway.controllerUid) - logger.info('Gateway %s has been removed.', gateway.controllerName) + gateway_utils.remove_gateway(vault, utils.base64_url_decode(gateway.controller_uid)) + logger.info('Gateway %s has been removed.', gateway.controller_name) else: logger.warning('Gateway %s not found', gateway_uid) @@ -588,12 +717,9 @@ def _validate_vault_and_permissions(self, context: KeeperParams): raise ValueError(ERROR_VAULT_NOT_INITIALIZED) base.require_enterprise_admin(context) - def _find_gateway(self, vault, gateway_uid): + def _find_gateway(self, context: KeeperParams, gateway_uid): """Finds a gateway by UID or name.""" - gateways = gateway_utils.get_all_gateways(vault) - return next((x for x in gateways - if utils.base64_url_encode(x.controllerUid) == gateway_uid - or x.controllerName.lower() == gateway_uid.lower()), None) + return context.pam_plugin.controllers.get_entity(gateway_uid) class PAMGatewaySetMaxInstancesCommand(base.ArgparseCommand): @@ -618,7 +744,7 @@ def execute(self, context: KeeperParams, **kwargs): max_instances = kwargs.get('max_instances') self._validate_max_instances(max_instances) - gateway = self._find_gateway(vault, gateway_uid) + gateway = self._find_gateway(context, gateway_uid) if not gateway: raise base.CommandError(f'Gateway "{gateway_uid}" not found') @@ -636,18 +762,15 @@ def _validate_max_instances(self, max_instances): if max_instances < MIN_INSTANCES: raise base.CommandError(f'pam gateway set-max-instances: --max-instances must be at least {MIN_INSTANCES}') - def _find_gateway(self, vault, gateway_uid): + def _find_gateway(self, context: KeeperParams, gateway_uid): """Finds a gateway by UID or name.""" - gateways = gateway_utils.get_all_gateways(vault) - return next((x for x in gateways - if utils.base64_url_encode(x.controllerUid) == gateway_uid - or x.controllerName.lower() == gateway_uid.lower()), None) + return context.pam_plugin.controllers.get_entity(gateway_uid) def _set_max_instances(self, vault, gateway, max_instances): """Sets the maximum number of instances for a gateway.""" try: - gateway_utils.set_gateway_max_instances(vault, gateway.controllerUid, max_instances) - logger.info('%s: max instance count set to %d', gateway.controllerName, max_instances) + gateway_utils.set_gateway_max_instances( + vault, utils.base64_url_decode(gateway.controller_uid), max_instances) + logger.info('%s: max instance count set to %d', gateway.controller_name, max_instances) except Exception as e: raise base.CommandError(f'Error setting max instances: {e}') - diff --git a/keepercli-package/src/keepercli/commands/pam/pam_config.py b/keepercli-package/src/keepercli/commands/pam/pam_config.py new file mode 100644 index 00000000..066dc892 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_config.py @@ -0,0 +1,1155 @@ +import argparse +import json +import re + +from .. import base +from ... import api +from ...helpers import report_utils, gateway_utils, folder_utils +from ...params import KeeperParams +from ..record_edit import RecordEditMixin + + +from keepersdk import utils +from keepersdk.proto import pam_pb2, record_pb2 +from keepersdk.helpers import config_utils +from keepersdk.vault import vault_online, vault_utils, vault_record, record_management +from keepersdk.helpers.pam_config_facade import PamConfigurationRecordFacade +from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG, tunnel_utils +from keepersdk.helpers.keeper_dag import dag_utils +from .. import record_edit + + +logger = api.get_logger() + + +# PAM Configuration record types +PAM_CONFIG_RECORD_TYPES = ( + 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', + 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration' +) + + +class PAMConfigListCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam config list') + PAMConfigListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--config', '-c', required=False, dest='pam_configuration', action='store', + help='Specific PAM Configuration UID') + parser.add_argument('--verbose', '-v', required=False, dest='verbose', action='store_true', help='Verbose') + parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], default='table', + help='Output format (table, json)') + + def execute(self, context: KeeperParams, **kwargs): + self._validate_vault_and_permissions(context) + + vault = context.vault + pam_configuration_uid = kwargs.get('pam_configuration') + is_verbose = kwargs.get('verbose') + format_type = kwargs.get('format', 'table') + + if not pam_configuration_uid: + result = self._list_all_configurations(vault, is_verbose, format_type) + if format_type == 'json' and result: + return result + else: + result = self._list_single_configuration(vault, pam_configuration_uid, is_verbose, format_type) + if format_type == 'json' and result: + return result + + if format_type == 'table': + self._print_tunneling_config(vault, pam_configuration_uid) + + def _validate_vault_and_permissions(self, context: KeeperParams): + """Validates that vault is initialized and user has enterprise admin permissions.""" + if not context.vault: + raise ValueError("Vault is not initialized, login to initialize the vault.") + base.require_enterprise_admin(context) + + def _print_tunneling_config(self, vault: vault_online.VaultOnline, config_uid: str): + """Prints tunneling configuration for a specific PAM configuration.""" + encrypted_session_token, encrypted_transmission_key, _ = tunnel_utils.get_keeper_tokens(vault) + tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid, is_config=True) + tmp_dag.print_tunneling_config(config_uid, None) + + def _list_single_configuration(self, vault: vault_online.VaultOnline, config_uid: str, + is_verbose: bool, format_type: str): + """Lists details for a single PAM configuration.""" + configuration = self._load_and_validate_configuration(vault, config_uid, format_type) + if format_type == 'json' and isinstance(configuration, str): + return configuration + facade = self._create_facade(configuration) + shared_folder = self._load_shared_folder(vault, facade.folder_uid) + + if format_type == 'json': + return self._format_single_config_json(configuration, facade, shared_folder) + else: + self._format_single_config_table(configuration, facade, shared_folder) + + def _list_all_configurations(self, vault: vault_online.VaultOnline, is_verbose: bool, format_type: str): + """Lists all PAM configurations.""" + configs_data = [] + table = [] + headers = self._build_list_headers(is_verbose, format_type) + + for config_record in self._find_pam_configurations(vault): + full_record = vault.vault_data.load_record(config_record.record_uid) + if not full_record or not isinstance(full_record, vault_record.TypedRecord): + logger.warning( + 'Skipping PAM configuration that could not be loaded as a typed record: UID: %s, Title: %s', + config_record.record_uid, config_record.title) + continue + + facade = self._create_facade(full_record) + shared_folder_parents = vault_utils.get_folders_for_record(vault.vault_data, config_record.record_uid) + + if not shared_folder_parents: + logger.warning(f'Following configuration is not in the shared folder: UID: %s, Title: %s', + config_record.record_uid, config_record.title) + continue + + shared_folder = shared_folder_parents[0] + + if format_type == 'json': + config_data = self._build_config_json_data(config_record, facade, shared_folder, full_record, is_verbose) + configs_data.append(config_data) + else: + row = self._build_config_table_row(config_record, facade, shared_folder, full_record, is_verbose) + table.append(row) + + return self._format_output(configs_data, table, headers, format_type) + + def _load_and_validate_configuration(self, vault: vault_online.VaultOnline, config_uid: str, format_type: str): + """Loads and validates a PAM configuration record.""" + configuration = vault.vault_data.load_record(config_uid) + if not configuration: + return self._handle_error(format_type, f'Configuration {config_uid} not found') + + if configuration.version != 6 or not isinstance(configuration, vault_record.TypedRecord): + return self._handle_error(format_type, f'{config_uid} is not PAM Configuration') + + return configuration + + def _handle_error(self, format_type: str, error_message: str): + """Handles errors based on output format.""" + if format_type == 'json': + return json.dumps({"error": error_message}) + else: + raise Exception(error_message) + + def _create_facade(self, configuration): + """Creates a PAM configuration facade for the given record.""" + facade = PamConfigurationRecordFacade() + facade.record = configuration + return facade + + def _load_shared_folder(self, vault: vault_online.VaultOnline, folder_uid: str): + """Loads shared folder if it exists.""" + if folder_uid and folder_uid in vault.vault_data._shared_folders: + return vault.vault_data.load_shared_folder(folder_uid) + return None + + def _find_pam_configurations(self, vault: vault_online.VaultOnline): + """Finds all PAM configuration records.""" + for record in vault.vault_data.find_records(criteria='', record_type=None, record_version=6): + if record.record_type in PAM_CONFIG_RECORD_TYPES: + yield record + else: + logger.warning(f'Following configuration has unsupported type: UID: %s, Title: %s', + record.record_uid, record.title) + + def _build_list_headers(self, is_verbose: bool, format_type: str): + """Builds headers for the configuration list output.""" + if format_type == 'json': + headers = ['uid', 'config_name', 'config_type', 'shared_folder', 'gateway_uid', 'resource_record_uids'] + if is_verbose: + headers.append('fields') + else: + headers = ['UID', 'Config Name', 'Config Type', 'Shared Folder', 'Gateway UID', 'Resource Record UIDs'] + if is_verbose: + headers.append('Fields') + return headers + + def _extract_config_fields(self, record, is_verbose: bool): + """Extracts field data from a configuration record.""" + fields_data = {} if is_verbose else [] + + for field in record.fields: + if field.type in ('pamResources', 'fileRef'): + continue + + values = list(field.get_external_value()) + if not values: + continue + + field_name = field.external_name() + if field.type == 'schedule': + field_name = 'Default Schedule' + + value_str = ', '.join(field.get_external_value()) + if is_verbose: + fields_data[field_name] = value_str + else: + fields_data.append(f'{field_name}: {value_str}') + + return fields_data + + def _build_config_json_data(self, config_record, facade, shared_folder, full_record, is_verbose: bool): + """Builds JSON data structure for a configuration.""" + config_data = { + "uid": config_record.record_uid, + "config_name": config_record.title, + "config_type": config_record.record_type, + "shared_folder": { + "name": shared_folder.name, + "uid": shared_folder.folder_uid + }, + "gateway_uid": facade.controller_uid, + "resource_record_uids": facade.resource_ref + } + + if is_verbose: + config_data["fields"] = self._extract_config_fields(full_record, is_verbose=True) + + return config_data + + def _build_config_table_row(self, config_record, facade, shared_folder, full_record, is_verbose: bool): + """Builds a table row for a configuration.""" + row = [ + config_record.record_uid, + config_record.title, + config_record.record_type, + f'{shared_folder.name} ({shared_folder.folder_uid})', + facade.controller_uid, + facade.resource_ref + ] + + if is_verbose: + fields = self._extract_config_fields(full_record, is_verbose=False) + row.append(fields) + + return row + + def _format_output(self, configs_data, table, headers, format_type: str): + """Formats and outputs the final result.""" + if format_type == 'json': + configs_data.sort(key=lambda x: x['config_name'] or '') + return json.dumps({"configurations": configs_data}, indent=2) + else: + table.sort(key=lambda x: (x[1] or '')) + report_utils.dump_report_data(table, headers, fmt='table', filename="", row_number=False, column_width=None) + + def _format_single_config_json(self, configuration, facade, shared_folder): + """Formats a single configuration as JSON.""" + config_data = { + "uid": configuration.record_uid, + "name": configuration.title, + "config_type": configuration.record_type, + "shared_folder": { + "name": shared_folder.name if shared_folder else None, + "uid": shared_folder.shared_folder_uid if shared_folder else None + } if shared_folder else None, + "gateway_uid": facade.controller_uid, + "resource_record_uids": facade.resource_ref, + "fields": {} + } + + for field in configuration.fields: + if field.type in ('pamResources', 'fileRef'): + continue + + values = list(field.get_external_value()) + if not values: + continue + + field_name = field.external_name() + if field.type == 'schedule': + field_name = 'Default Schedule' + + config_data["fields"][field_name] = values + + return json.dumps(config_data, indent=2) + + def _format_single_config_table(self, configuration, facade, shared_folder): + """Formats a single configuration as a table.""" + table = [] + header = ['name', 'value'] + + table.append(['UID', configuration.record_uid]) + table.append(['Name', configuration.title]) + table.append(['Config Type', configuration.record_type]) + table.append(['Shared Folder', f'{shared_folder.name} ({shared_folder.shared_folder_uid})' if shared_folder else '']) + table.append(['Gateway UID', facade.controller_uid]) + table.append(['Resource Record UIDs', facade.resource_ref]) + + for field in configuration.fields: + if field.type in ('pamResources', 'fileRef'): + continue + + values = list(field.get_external_value()) + if not values: + continue + + field_name = field.external_name() + if field.type == 'schedule': + field_name = 'Default Schedule' + + table.append([field_name, values]) + + report_utils.dump_report_data(table, header, no_header=True, right_align=(0,)) + + +class PamConfigurationEditMixin(record_edit.RecordEditMixin): + pam_record_types = None + + def __init__(self): + super().__init__() + + @staticmethod + def get_pam_record_types(vault: vault_online.VaultOnline): + """Gets cached list of PAM record types.""" + if PamConfigurationEditMixin.pam_record_types is None: + rts = [x for x in vault.vault_data._custom_record_types if x.scope // 1000000 == record_pb2.RT_PAM] + PamConfigurationEditMixin.pam_record_types = [rt.id for rt in rts] + return PamConfigurationEditMixin.pam_record_types + + def parse_pam_configuration(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, **kwargs): + """Parses PAM configuration fields: gateway, shared folder, and resource records.""" + field = self._get_or_create_pam_resources_field(record) + value = self._ensure_pam_resources_value(field) + + self._parse_gateway_uid(vault, value, kwargs) + self._parse_shared_folder_uid(vault, record, value, kwargs) + self._parse_resource_records(vault, value, kwargs) + + def _get_or_create_pam_resources_field(self, record: vault_record.TypedRecord): + """Gets or creates the pamResources field.""" + field = record.get_typed_field('pamResources') + if not field: + field = vault_record.TypedField.create_field('pamResources', '', required=False) + record.fields.append(field) + return field + + def _ensure_pam_resources_value(self, field): + """Ensures the pamResources field has a value dictionary.""" + if len(field.value) == 0: + field.value.append({}) + return field.value[0] + + def _parse_gateway_uid(self, vault: vault_online.VaultOnline, value: dict, kwargs: dict): + """Resolves and sets the gateway UID from kwargs.""" + gateway = kwargs.get('gateway_uid') + if not gateway: + return + + gateways = gateway_utils.get_all_gateways(vault) + gateway_uid = next( + (utils.base64_url_encode(x.controllerUid) for x in gateways + if utils.base64_url_encode(x.controllerUid) == gateway + or x.controllerName.casefold() == gateway.casefold()), + None + ) + + if gateway_uid: + value['controllerUid'] = gateway_uid + + def _parse_shared_folder_uid(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + value: dict, kwargs: dict): + """Resolves and sets the shared folder UID from kwargs or existing record.""" + folder_name = kwargs.get('shared_folder_uid') + shared_folder_uid = None + + if folder_name: + shared_folder_uid = self._find_shared_folder_by_name_or_uid(vault, folder_name) + + if not shared_folder_uid: + shared_folder_uid = self._get_existing_shared_folder_uid(record) + + if shared_folder_uid: + value['folderUid'] = shared_folder_uid + else: + raise base.CommandError('Shared Folder not found') + + def _find_shared_folder_by_name_or_uid(self, vault: vault_online.VaultOnline, folder_name: str): + """Finds a shared folder by UID or name.""" + shared_folder_cache = vault.vault_data._shared_folders + + if folder_name in shared_folder_cache: + return folder_name + + for sf_uid in shared_folder_cache: + sf = vault.vault_data.load_shared_folder(sf_uid) + if sf and sf.name.casefold() == folder_name.casefold(): + return sf_uid + + return None + + def _get_existing_shared_folder_uid(self, record: vault_record.TypedRecord): + """Gets the existing shared folder UID from the record.""" + for f in record.fields: + if f.type == 'pamResources' and f.value and len(f.value) > 0: + return f.value[0].get('folderUid') + return None + + def _parse_resource_records(self, vault: vault_online.VaultOnline, value: dict, kwargs: dict): + """Removes resource records from the configuration.""" + remove_records = kwargs.get('remove_records') + if not remove_records: + return + + pam_record_lookup = self._build_pam_record_lookup(vault) + record_uids = set(value.get('resourceRef', [])) + + if isinstance(remove_records, list): + for r in remove_records: + record_uid = pam_record_lookup.get(r) or pam_record_lookup.get(r.lower()) + if record_uid: + record_uids.discard(record_uid) + else: + logger.warning(f'Failed to find PAM record: {r}') + + value['resourceRef'] = list(record_uids) + + def _build_pam_record_lookup(self, vault: vault_online.VaultOnline): + """Builds a lookup dictionary for PAM records by UID and title.""" + pam_record_lookup = {} + rti = PamConfigurationEditMixin.get_pam_record_types(vault) + + for r in vault.vault_data.records(): + if r.record_type in rti: + pam_record_lookup[r.record_uid] = r.record_uid + pam_record_lookup[r.title.lower()] = r.record_uid + + return pam_record_lookup + + @staticmethod + def resolve_single_record(vault: vault_online.VaultOnline, record_name: str, rec_type: str = ''): + """Resolves a single record by name and optional type.""" + for r in vault.vault_data.records(): + if r.title == record_name and (not rec_type or rec_type == r.record_type): + return r + return None + + def parse_properties(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, **kwargs): + """Parses all configuration properties based on record type.""" + self.parse_pam_configuration(vault, record, **kwargs) + + extra_properties = [] + self._parse_common_properties(extra_properties, kwargs) + self._parse_type_specific_properties(vault, record, extra_properties, kwargs) + + if extra_properties: + parsed_fields = [record_edit.RecordEditMixin.parse_field(x) for x in extra_properties] + self.assign_typed_fields(record, parsed_fields) + + def _parse_common_properties(self, extra_properties: list, kwargs: dict): + """Parses properties common to all PAM configuration types.""" + port_mapping = kwargs.get('port_mapping') + if isinstance(port_mapping, list) and len(port_mapping) > 0: + pm = "\n".join(port_mapping) + extra_properties.append(f'multiline.portMapping={pm}') + + schedule = kwargs.get('default_schedule') + if schedule: + valid, err = validate_cron_expression(schedule, for_rotation=True) + if not valid: + raise base.CommandError(f'Invalid CRON "{schedule}" Error: {err}') + extra_properties.append(f'schedule.defaultRotationSchedule=$JSON:{{"type": "CRON", "cron": "{schedule}", "tz": "Etc/UTC"}}') + else: + extra_properties.append('schedule.defaultRotationSchedule=On-Demand') + + def _parse_type_specific_properties(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + extra_properties: list, kwargs: dict): + """Parses properties specific to each configuration type.""" + record_type = record.record_type + + if record_type == 'pamNetworkConfiguration': + self._parse_network_properties(extra_properties, kwargs) + elif record_type == 'pamAwsConfiguration': + self._parse_aws_properties(extra_properties, kwargs) + elif record_type == 'pamGcpConfiguration': + self._parse_gcp_properties(extra_properties, kwargs) + elif record_type == 'pamAzureConfiguration': + self._parse_azure_properties(extra_properties, kwargs) + elif record_type == 'pamDomainConfiguration': + self._parse_domain_properties(vault, record, extra_properties, kwargs) + elif record_type == 'pamOciConfiguration': + self._parse_oci_properties(extra_properties, kwargs) + + def _parse_network_properties(self, extra_properties: list, kwargs: dict): + """Parses network configuration properties.""" + network_id = kwargs.get('network_id') + if network_id: + extra_properties.append(f'text.networkId={network_id}') + + network_cidr = kwargs.get('network_cidr') + if network_cidr: + extra_properties.append(f'text.networkCIDR={network_cidr}') + + def _parse_aws_properties(self, extra_properties: list, kwargs: dict): + """Parses AWS configuration properties.""" + aws_id = kwargs.get('aws_id') + if aws_id: + extra_properties.append(f'text.awsId={aws_id}') + + access_key_id = kwargs.get('access_key_id') + if access_key_id: + extra_properties.append(f'secret.accessKeyId={access_key_id}') + + access_secret_key = kwargs.get('access_secret_key') + if access_secret_key: + extra_properties.append(f'secret.accessSecretKey={access_secret_key}') + + region_names = kwargs.get('region_names') + if region_names: + regions = '\n'.join(region_names) + extra_properties.append(f'multiline.regionNames={regions}') + + def _parse_gcp_properties(self, extra_properties: list, kwargs: dict): + """Parses GCP configuration properties.""" + gcp_id = kwargs.get('gcp_id') + if gcp_id: + extra_properties.append(f'text.pamGcpId={gcp_id}') + + service_account_key = kwargs.get('service_account_key') + if service_account_key: + extra_properties.append(f'json.pamServiceAccountKey={service_account_key}') + + google_admin_email = kwargs.get('google_admin_email') + if google_admin_email: + extra_properties.append(f'email.pamGoogleAdminEmail={google_admin_email}') + + gcp_region = kwargs.get('region_names') + if gcp_region: + regions = '\n'.join(gcp_region) + extra_properties.append(f'multiline.pamGcpRegionName={regions}') + + def _parse_azure_properties(self, extra_properties: list, kwargs: dict): + """Parses Azure configuration properties.""" + azure_id = kwargs.get('azure_id') + if azure_id: + extra_properties.append(f'text.azureId={azure_id}') + + client_id = kwargs.get('client_id') + if client_id: + extra_properties.append(f'secret.clientId={client_id}') + + client_secret = kwargs.get('client_secret') + if client_secret: + extra_properties.append(f'secret.clientSecret={client_secret}') + + subscription_id = kwargs.get('subscription_id') + if subscription_id: + extra_properties.append(f'secret.subscriptionId={subscription_id}') + + tenant_id = kwargs.get('tenant_id') + if tenant_id: + extra_properties.append(f'secret.tenantId={tenant_id}') + + resource_groups = kwargs.get('resource_groups') + if isinstance(resource_groups, list) and len(resource_groups) > 0: + rg = '\n'.join(resource_groups) + extra_properties.append(f'multiline.resourceGroups={rg}') + + def _parse_domain_properties(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + extra_properties: list, kwargs: dict): + """Parses domain configuration properties.""" + domain_id = kwargs.get('domain_id') + if domain_id: + extra_properties.append(f'text.pamDomainId={domain_id}') + + self._parse_domain_hostname(extra_properties, kwargs) + self._parse_domain_ssl_settings(extra_properties, kwargs) + self._parse_domain_network_settings(extra_properties, kwargs) + self._parse_domain_admin_credential(vault, record, kwargs) + + def _parse_domain_hostname(self, extra_properties: list, kwargs: dict): + """Parses domain hostname and port settings.""" + host = str(kwargs.get('domain_hostname') or '').strip() + port = str(kwargs.get('domain_port') or '').strip() + if host or port: + val = json.dumps({"hostName": host, "port": port}) + extra_properties.append(f"f.pamHostname=$JSON:{val}") + + def _parse_domain_ssl_settings(self, extra_properties: list, kwargs: dict): + """Parses domain SSL and scan settings.""" + domain_use_ssl = dag_utils.value_to_boolean(kwargs.get('domain_use_ssl')) + if domain_use_ssl is not None: + val = 'true' if domain_use_ssl else 'false' + extra_properties.append(f'checkbox.useSSL={val}') + + domain_scan_dc_cidr = dag_utils.value_to_boolean(kwargs.get('domain_scan_dc_cidr')) + if domain_scan_dc_cidr is not None: + val = 'true' if domain_scan_dc_cidr else 'false' + extra_properties.append(f'checkbox.scanDCCIDR={val}') + + def _parse_domain_network_settings(self, extra_properties: list, kwargs: dict): + """Parses domain network CIDR settings.""" + domain_network_cidr = kwargs.get('domain_network_cidr') + if domain_network_cidr: + extra_properties.append(f'text.networkCIDR={domain_network_cidr}') + + def _parse_domain_admin_credential(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + kwargs: dict): + """Parses and validates domain administrative credential.""" + domain_administrative_credential = kwargs.get('domain_administrative_credential') + dac = str(domain_administrative_credential or '') + + if not dac: + return + + if kwargs.get('force_domain_admin', False) is True: + if not re.search('^[A-Za-z0-9-_]{22}$', dac): + logger.warning(f'Invalid Domain Admin User UID: "{dac}" (skipped)') + dac = '' + else: + adm_rec = PamConfigurationEditMixin.resolve_single_record(vault, dac, 'pamUser') + if adm_rec and isinstance(adm_rec, vault_record.TypedRecord) and adm_rec.record_type == 'pamUser': + dac = adm_rec.record_uid + else: + logger.warning(f'Domain Admin User UID: "{dac}" not found (skipped).') + dac = '' + + if dac: + prf = record.get_typed_field('pamResources') + prf.value = prf.value or [{}] + prf.value[0]["adminCredentialRef"] = dac + + def _parse_oci_properties(self, extra_properties: list, kwargs: dict): + """Parses OCI configuration properties.""" + oci_id = kwargs.get('oci_id') + if oci_id: + extra_properties.append(f'text.pamOciId={oci_id}') + + oci_admin_id = kwargs.get('oci_admin_id') + if oci_admin_id: + extra_properties.append(f'secret.adminOcid={oci_admin_id}') + + oci_admin_public_key = kwargs.get('oci_admin_public_key') + if oci_admin_public_key: + extra_properties.append(f'secret.adminPublicKey={oci_admin_public_key}') + + oci_admin_private_key = kwargs.get('oci_admin_private_key') + if oci_admin_private_key: + extra_properties.append(f'secret.adminPrivateKey={oci_admin_private_key}') + + oci_tenancy = kwargs.get('oci_tenancy') + if oci_tenancy: + extra_properties.append(f'text.tenancyOci={oci_tenancy}') + + oci_region = kwargs.get('oci_region') + if oci_region: + extra_properties.append(f'text.regionOci={oci_region}') + + def verify_required(self, record: vault_record.TypedRecord): + """Verifies and sets default values for required fields.""" + for field in record.fields: + if field.required and len(field.value) == 0: + if field.type == 'schedule': + field.value = [{'type': 'ON_DEMAND'}] + else: + self.warnings.append(f'Empty required field: "{field.external_name()}"') + + for custom in record.custom: + if custom.required: + custom.required = False + + +# Configuration type mapping +CONFIG_TYPE_TO_RECORD_TYPE = { + 'aws': 'pamAwsConfiguration', + 'azure': 'pamAzureConfiguration', + 'local': 'pamNetworkConfiguration', + 'network': 'pamNetworkConfiguration', + 'gcp': 'pamGcpConfiguration', + 'domain': 'pamDomainConfiguration', + 'oci': 'pamOciConfiguration' +} + +common_parser = argparse.ArgumentParser(add_help=False) +common_parser.add_argument('--environment', '-env', dest='config_type', action='store', + choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci'], help='PAM Configuration Type') +common_parser.add_argument('--title', '-t', dest='title', action='store', help='Title of the PAM Configuration') +common_parser.add_argument('--gateway', '-g', dest='gateway_uid', action='store', help='Gateway UID or Name') +common_parser.add_argument('--shared-folder', '-sf', dest='shared_folder_uid', action='store', + help='Share Folder where this PAM Configuration is stored. Should be one of the folders to ' + 'which the gateway has access to.') +common_parser.add_argument('--schedule', '-sc', dest='default_schedule', action='store', + help='Default Schedule: Use CRON syntax') +common_parser.add_argument('--port-mapping', '-pm', dest='port_mapping', action='append', help='Port Mapping') +common_parser.add_argument('--identity-provider', '-idp', dest='identity_provider_uid', + action='store', help='Identity Provider UID') +network_group = common_parser.add_argument_group('network', 'Local network configuration') +network_group.add_argument('--network-id', dest='network_id', action='store', help='Network ID') +network_group.add_argument('--network-cidr', dest='network_cidr', action='store', help='Network CIDR') +aws_group = common_parser.add_argument_group('aws', 'AWS configuration') +aws_group.add_argument('--aws-id', dest='aws_id', action='store', help='AWS ID') +aws_group.add_argument('--access-key-id', dest='access_key_id', action='store', help='Access Key Id') +aws_group.add_argument('--access-secret-key', dest='access_secret_key', action='store', help='Access Secret Key') +aws_group.add_argument('--region-name', dest='region_names', action='append', help='Region Names') +azure_group = common_parser.add_argument_group('azure', 'Azure configuration') +azure_group.add_argument('--azure-id', dest='azure_id', action='store', help='Azure Id') +azure_group.add_argument('--client-id', dest='client_id', action='store', help='Client Id') +azure_group.add_argument('--client-secret', dest='client_secret', action='store', help='Client Secret') +azure_group.add_argument('--subscription_id', dest='subscription_id', action='store', + help='Subscription Id') +azure_group.add_argument('--tenant-id', dest='tenant_id', action='store', help='Tenant Id') +azure_group.add_argument('--resource-group', dest='resource_groups', action='append', help='Resource Group') +domain_group = common_parser.add_argument_group('domain', 'Domain configuration') +domain_group.add_argument('--domain-id', dest='domain_id', action='store', help='Domain ID') +domain_group.add_argument('--domain-hostname', dest='domain_hostname', action='store', help='Domain hostname') +domain_group.add_argument('--domain-port', dest='domain_port', action='store', help='Domain port') +domain_group.add_argument('--domain-use-ssl', dest='domain_use_ssl', choices=['true', 'false'], + help='Domain use SSL flag') +domain_group.add_argument('--domain-scan-dc-cidr', dest='domain_scan_dc_cidr', choices=['true', 'false'], + help='Domain scan DC CIDR flag') +domain_group.add_argument('--domain-network-cidr', dest='domain_network_cidr', action='store', + help='Domain Network CIDR') +domain_group.add_argument('--domain-admin', dest='domain_administrative_credential', action='store', + help='Domain administrative credential') +oci_group = common_parser.add_argument_group('oci', 'OCI configuration') +oci_group.add_argument('--oci-id', dest='oci_id', action='store', help='OCI ID') +oci_group.add_argument('--oci-admin-id', dest='oci_admin_id', action='store', help='OCI Admin ID') +oci_group.add_argument('--oci-admin-public-key', dest='oci_admin_public_key', action='store', + help='OCI admin public key') +oci_group.add_argument('--oci-admin-private-key', dest='oci_admin_private_key', action='store', + help='OCI admin private key') +oci_group.add_argument('--oci-tenancy', dest='oci_tenancy', action='store', help='OCI tenancy') +oci_group.add_argument('--oci-region', dest='oci_region', action='store', help='OCI region') + +gcp_group = common_parser.add_argument_group('gcp', 'GCP configuration') +gcp_group.add_argument('--gcp-id', dest='gcp_id', action='store', help='GCP Id') +gcp_group.add_argument('--service-account-key', dest='service_account_key', action='store', + help='Service Account Key (JSON format)') +gcp_group.add_argument('--google-admin-email', dest='google_admin_email', action='store', + help='Google Workspace Administrator Email Address') +gcp_group.add_argument('--gcp-region', dest='region_names', action='append', help='GCP Region Names') + + +class PAMConfigNewCommand(base.ArgparseCommand, PamConfigurationEditMixin): + + def __init__(self): + self.choices = ['on', 'off', 'default'] + parser = argparse.ArgumentParser(prog='pam config new', parents=[common_parser]) + PAMConfigNewCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + choices = ['on', 'off', 'default'] + parser.add_argument('--connections', '-c', dest='connections', choices=choices, + help='Set connections permissions') + parser.add_argument('--tunneling', '-u', dest='tunneling', choices=choices, + help='Set tunneling permissions') + parser.add_argument('--rotation', '-r', dest='rotation', choices=choices, + help='Set rotation permissions') + parser.add_argument('--remote-browser-isolation', '-rbi', dest='remotebrowserisolation', choices=choices, + help='Set remote browser isolation permissions') + parser.add_argument('--connections-recording', '-cr', dest='recording', choices=choices, + help='Set recording connections permissions for the resource') + parser.add_argument('--typescript-recording', '-tr', dest='typescriptrecording', choices=choices, + help='Set TypeScript recording permissions for the resource') + parser.add_argument('--ai-threat-detection', dest='ai_threat_detection', choices=choices, + help='Set AI threat detection permissions') + parser.add_argument('--ai-terminate-session-on-detection', dest='ai_terminate_session_on_detection', + choices=choices, + help='Set AI session termination on threat detection permissions') + + def execute(self, context: KeeperParams, **kwargs): + self.warnings.clear() + self._validate_vault(context) + + vault = context.vault + record_type = self._resolve_record_type(kwargs) + title = self._validate_title(kwargs) + + record = self._create_record(vault, record_type, title) + self._resolve_shared_folder_path(context, kwargs) + self.parse_properties(vault, record, **kwargs) + + gateway_uid, shared_folder_uid, admin_cred_ref = self._extract_pam_resources(record, kwargs) + self._validate_shared_folder(shared_folder_uid, kwargs) + self._warn_if_gateway_missing(gateway_uid, kwargs) + + self.verify_required(record) + self._create_and_configure_record(vault, record, shared_folder_uid, gateway_uid, admin_cred_ref, kwargs) + + self._log_warnings() + return record.record_uid + + def _validate_vault(self, context: KeeperParams): + """Validates that vault is initialized.""" + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + def _resolve_record_type(self, kwargs: dict) -> str: + """Resolves the record type from config type parameter.""" + config_type = kwargs.get('config_type') + if not config_type: + raise base.CommandError('--config-type parameter is required') + + record_type = CONFIG_TYPE_TO_RECORD_TYPE.get(config_type) + if not record_type: + supported = ', '.join(CONFIG_TYPE_TO_RECORD_TYPE.keys()) + raise base.CommandError(f'--config-type {config_type} is not supported - supported options: {supported}') + + return record_type + + def _validate_title(self, kwargs: dict) -> str: + """Validates that title is provided.""" + title = kwargs.get('title') + if not title: + raise base.CommandError('--title parameter is required') + return title + + def _create_record(self, vault: vault_online.VaultOnline, record_type: str, title: str): + """Creates a new typed record with the specified type and title.""" + record = vault_record.TypedRecord() + record.record_type = record_type + record.title = title + record.record_version = 6 + + record_type_def = vault.vault_data.get_record_type_by_name(record_type) + if record_type_def and record_type_def.fields: + RecordEditMixin.adjust_typed_record_fields(record, record_type_def.fields) + + return record + + def _resolve_shared_folder_path(self, context: KeeperParams, kwargs: dict): + """Resolves shared folder path to UID.""" + sf_name = kwargs.get('shared_folder_uid', '') + if not sf_name: + return + + fpath = folder_utils.try_resolve_path(context, sf_name) + if fpath and len(fpath) >= 2 and fpath[-1] == '': + sfuid = fpath[-2].folder_uid + if sfuid: + kwargs['shared_folder_uid'] = sfuid + + def _extract_pam_resources(self, record: vault_record.TypedRecord, kwargs: dict): + """Extracts gateway UID, shared folder UID, and admin credential ref from record.""" + field = record.get_typed_field('pamResources') + if not field: + raise base.CommandError('PAM configuration record does not contain resource field') + + value = field.get_default_value(dict) + if not value: + return None, None, None + + gateway_uid = value.get('controllerUid') + shared_folder_uid = value.get('folderUid') + admin_cred_ref = None + + if record.record_type == 'pamDomainConfiguration' and not kwargs.get('force_domain_admin', False): + admin_cred_ref = value.get('adminCredentialRef') + + return gateway_uid, shared_folder_uid, admin_cred_ref + + def _validate_shared_folder(self, shared_folder_uid: str, kwargs: dict): + """Validates that shared folder UID is present.""" + if not shared_folder_uid: + raise base.CommandError('--shared-folder parameter is required to create a PAM configuration') + + def _warn_if_gateway_missing(self, gateway_uid: str, kwargs: dict): + """Warns if gateway is not found.""" + if not gateway_uid: + gw_name = kwargs.get('gateway_uid') or '' + logger.warning(f'Gateway "{gw_name}" not found.') + + def _create_and_configure_record(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + shared_folder_uid: str, gateway_uid: str, admin_cred_ref: str, kwargs: dict): + """Creates the record and configures tunneling, DAG, and controller.""" + config_utils.pam_configuration_create_record_v6(vault, record, shared_folder_uid) + + self._configure_tunneling(vault, record, admin_cred_ref, kwargs) + + vault.sync_down() + record_management.move_vault_objects(vault, [record.record_uid], shared_folder_uid) + vault.sync_down() + + if gateway_uid: + self._set_configuration_controller(vault, record.record_uid, gateway_uid) + + def _configure_tunneling(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + admin_cred_ref: str, kwargs: dict): + """Configures tunneling settings for the configuration.""" + encrypted_session_token, encrypted_transmission_key, _ = tunnel_utils.get_keeper_tokens(vault) + tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, + record_uid=record.record_uid, is_config=True) + + tmp_dag.edit_tunneling_config( + kwargs.get('connections'), + kwargs.get('tunneling'), + kwargs.get('rotation'), + kwargs.get('recording'), + kwargs.get('typescriptrecording'), + kwargs.get('remotebrowserisolation') + ) + + if admin_cred_ref: + tmp_dag.link_user_to_config_with_options(admin_cred_ref, is_admin='on') + + tmp_dag.print_tunneling_config(record.record_uid, None) + + def _set_configuration_controller(self, vault: vault_online.VaultOnline, config_uid: str, gateway_uid: str): + """Sets the controller for the PAM configuration.""" + pcc = pam_pb2.PAMConfigurationController() + pcc.configurationUid = utils.base64_url_decode(config_uid) + pcc.controllerUid = utils.base64_url_decode(gateway_uid) + vault.keeper_auth.execute_auth_rest('pam/set_configuration_controller', pcc) + + def _log_warnings(self): + """Logs all warnings.""" + for w in self.warnings: + logger.warning(w) + + +class PAMConfigEditCommand(base.ArgparseCommand, PamConfigurationEditMixin): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam config edit', parents=[common_parser]) + PAMConfigEditCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + choices = ['on', 'off', 'default'] + parser.add_argument('uid', type=str, action='store', help='The Config UID to edit') + parser.add_argument('--remove-resource-record', '-rrr', dest='remove_records', action='append', + help='Resource Record UID to remove') + parser.add_argument('--connections', '-c', dest='connections', choices=choices, + help='Set connections permissions') + parser.add_argument('--tunneling', '-u', dest='tunneling', choices=choices, + help='Set tunneling permissions') + parser.add_argument('--rotation', '-r', dest='rotation', choices=choices, + help='Set rotation permissions') + parser.add_argument('--remote-browser-isolation', '-rbi', dest='remotebrowserisolation', choices=choices, + help='Set remote browser isolation permissions') + parser.add_argument('--connections-recording', '-cr', dest='recording', choices=choices, + help='Set recording connections permissions for the resource') + parser.add_argument('--typescript-recording', '-tr', dest='typescriptrecording', choices=choices, + help='Set TypeScript recording permissions for the resource') + + def execute(self, context: KeeperParams, **kwargs): + self.warnings.clear() + self._validate_vault(context) + + vault = context.vault + configuration = self._find_configuration(vault, kwargs.get('uid')) + self._validate_configuration(vault, configuration, kwargs.get('uid')) + + self._update_record_type_if_needed(vault, configuration, kwargs) + self._update_title_if_provided(configuration, kwargs) + + orig_gateway_uid, orig_shared_folder_uid = self._get_original_values(configuration) + self.parse_properties(vault, configuration, **kwargs) + self.verify_required(configuration) + + record_management.update_record(vault, configuration) + self._update_controller_and_folder_if_changed(vault, configuration, orig_gateway_uid, orig_shared_folder_uid) + + self._log_warnings() + vault.sync_down() + + def _validate_vault(self, context: KeeperParams): + """Validates that vault is initialized.""" + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + def _find_configuration(self, vault: vault_online.VaultOnline, config_name: str): + """Finds a PAM configuration by UID or name.""" + if not config_name: + return None + info = vault.vault_data.get_record(config_name) + if info and info.version == 6 and info.record_type in PAM_CONFIG_RECORD_TYPES: + loaded = vault.vault_data.load_record(config_name) + if loaded and isinstance(loaded, vault_record.TypedRecord): + return loaded + name_lower = config_name.casefold() + for record in vault.vault_data.find_records( + criteria=None, + record_type=PAM_CONFIG_RECORD_TYPES, + record_version=6): + if record.record_uid == config_name or record.title.casefold() == name_lower: + loaded = vault.vault_data.load_record(record.record_uid) + if loaded and isinstance(loaded, vault_record.TypedRecord): + return loaded + return None + + def _validate_configuration(self, vault: vault_online.VaultOnline, configuration, config_name: str): + """Validates that the configuration exists and is a v6 PAM config in the vault index.""" + if not configuration: + raise base.CommandError(f'PAM configuration "{config_name}" not found') + if not isinstance(configuration, vault_record.TypedRecord): + raise base.CommandError(f'PAM configuration "{config_name}" not found') + # Storage format is on KeeperRecordInfo, not TypedRecord.version() (that method returns 3). + info = vault.vault_data.get_record(configuration.record_uid) + if not info or info.version != 6 or info.record_type not in PAM_CONFIG_RECORD_TYPES: + raise base.CommandError(f'PAM configuration "{config_name}" not found') + + def _update_record_type_if_needed(self, vault: vault_online.VaultOnline, configuration: vault_record.TypedRecord, + kwargs: dict): + """Updates the record type if config_type is provided and different.""" + config_type = kwargs.get('config_type') + if not config_type: + return + + record_type = CONFIG_TYPE_TO_RECORD_TYPE.get(config_type, configuration.record_type) + + if record_type != configuration.record_type: + configuration.record_type = record_type + record_type_def = vault.vault_data.get_record_type_by_name(record_type) + if record_type_def and record_type_def.fields: + RecordEditMixin.adjust_typed_record_fields(configuration, record_type_def.fields) + + def _update_title_if_provided(self, configuration: vault_record.TypedRecord, kwargs: dict): + """Updates the title if provided.""" + title = kwargs.get('title') + if title: + configuration.title = title + + def _get_original_values(self, configuration: vault_record.TypedRecord): + """Gets the original gateway and shared folder UIDs before updates.""" + field = configuration.get_typed_field('pamResources') + if not field: + raise base.CommandError('PAM configuration record does not contain resource field') + + value = field.get_default_value(dict) + if value: + return value.get('controllerUid') or '', value.get('folderUid') or '' + + return '', '' + + def _update_controller_and_folder_if_changed(self, vault: vault_online.VaultOnline, + configuration: vault_record.TypedRecord, + orig_gateway_uid: str, orig_shared_folder_uid: str): + """Updates controller and shared folder if they changed.""" + field = configuration.get_typed_field('pamResources') + value = field.get_default_value(dict) + if not value: + return + + gateway_uid = value.get('controllerUid') or '' + if gateway_uid != orig_gateway_uid: + self._set_configuration_controller(vault, configuration.record_uid, gateway_uid) + + shared_folder_uid = value.get('folderUid') or '' + if shared_folder_uid != orig_shared_folder_uid: + record_management.move_vault_objects(vault, [configuration.record_uid], shared_folder_uid) + + def _set_configuration_controller(self, vault: vault_online.VaultOnline, config_uid: str, gateway_uid: str): + """Sets the controller for the PAM configuration.""" + pcc = pam_pb2.PAMConfigurationController() + pcc.configurationUid = utils.base64_url_decode(config_uid) + pcc.controllerUid = utils.base64_url_decode(gateway_uid) + vault.keeper_auth.execute_auth_rest('pam/set_configuration_controller', pcc) + + def _log_warnings(self): + """Logs all warnings.""" + for w in self.warnings: + logger.warning(w) + + +class PAMConfigRemoveCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam config remove') + PAMConfigRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('config', type=str, action='store', + help='PAM Configuration UID. To view all rotation settings with their UIDs, use command `pam config list`') + + def execute(self, context: KeeperParams, **kwargs): + self._validate_vault(context) + + vault = context.vault + pam_config_name = kwargs.get('config') + pam_config_uid = self._find_configuration_uid(vault, pam_config_name) + + if not pam_config_uid: + raise base.CommandError(f'Configuration "{pam_config_name}" not found') + + record_management.delete_vault_objects(vault, [pam_config_uid]) + vault.sync_down() + + def _validate_vault(self, context: KeeperParams): + """Validates that vault is initialized.""" + if not context.vault: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + def _find_configuration_uid(self, vault: vault_online.VaultOnline, config_name: str) -> str: + """Finds a PAM configuration UID by UID or name.""" + if not config_name: + return None + info = vault.vault_data.get_record(config_name) + if info and info.version == 6 and info.record_type in PAM_CONFIG_RECORD_TYPES: + return config_name + name_lower = config_name.casefold() + for record in vault.vault_data.find_records( + criteria=None, + record_type=PAM_CONFIG_RECORD_TYPES, + record_version=6): + if record.record_uid == config_name or record.title.casefold() == name_lower: + return record.record_uid + return None + + +def validate_cron_field(field: str, min_val: int, max_val: int) -> bool: + # Accept *, single number, range, step, list, and L suffix for last day/week + pattern = r'^(\*|\d+L?|L[W]?|\d+-\d+|\*/\d+|\d+(,\d+)*|\d+-\d+/\d+)$' + if not re.match(pattern, field): + return False + + def is_valid_number(n: str) -> bool: + # Strip L and W suffix if present (for last day/week expressions) + n_stripped = n.rstrip('LW') + return n_stripped and n_stripped.isdigit() and min_val <= int(n_stripped) <= max_val + + parts = re.split(r'[,\-/]', field) + return all(part == '*' or part in ('L', 'LW') or is_valid_number(part) for part in parts if part != '*') + + +def validate_cron_expression(expr: str, for_rotation: bool = False) -> tuple[bool, str]: + parts = expr.strip().split() + + if for_rotation is True: + if len(parts) != 6: + return False, f"CRON: Rotation schedules require all 6 parts incl. seconds - ex. Daily at 04:00:00 cron: 0 0 4 * * ? got {len(parts)} parts" + if not(parts[3] == '?' or parts[5] == "?"): + logger.warning("CRON: Rotation schedule CRON format - must use ? character in one of these fields: day-of-week, day-of-month") + parts[3] = '*' if parts[3] == '?' else parts[3] + parts[5] = '*' if parts[5] == '?' else parts[5] + logger.debug("WARNING! Validating CRON expression for rotation - if you get 500 type errors make sure to validate your CRON using web vault UI") + + if len(parts) not in [5, 6]: + return False, f"CRON: Expected 5 or 6 fields, got {len(parts)}" + + if len(parts) == 6: + seconds, minute, hour, dom, month, dow = parts + if not validate_cron_field(seconds, 0, 59): + return False, "CRON: Invalid seconds field" + else: + minute, hour, dom, month, dow = parts + + validators = [ + (minute, 0, 59, "minute"), + (hour, 0, 23, "hour"), + (dom, 1, 31, "day of month"), + (month, 1, 12, "month"), + (dow, 0, 7, "day of week") + ] + + for field, min_val, max_val, name in validators: + if not validate_cron_field(field, min_val, max_val): + return False, f"CRON: Invalid {name} field" + + return True, "Valid cron expression" + diff --git a/keepercli-package/src/keepercli/commands/pam/pam_connection.py b/keepercli-package/src/keepercli/commands/pam/pam_connection.py new file mode 100644 index 00000000..85e8eae0 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_connection.py @@ -0,0 +1,274 @@ +import os +import argparse + +from keepersdk import utils +from keepersdk.helpers.keeper_dag import dag_utils +from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG +from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid +from keepersdk.vault import record_management, vault_record + +from .. import base +from ... import api +from ...params import KeeperParams + + +logger = api.get_logger() + + +protocols = ['', 'http', 'kubernetes', 'mysql', 'postgresql', 'rdp', 'sql-server', 'ssh', 'telnet', 'vnc'] +choices = ['on', 'off', 'default'] + + +class PAMConnectionEditCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam connection edit') + PAMConnectionEditCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('record', type=str, action='store', help='The record UID or path of the PAM ' + 'resource record with network information to use for connections') + parser.add_argument('--configuration', '-c', required=False, dest='config', action='store', + help='The PAM Configuration UID or path to use for connections. ' + 'Use command `pam config list` to view available PAM Configurations.') + + parser.add_argument('--admin-user', '-a', required=False, dest='admin', action='store', + help='The record path or UID of the PAM User record to configure the admin ' + 'credential on the PAM Resource') + parser.add_argument('--launch-user', '-lu', required=False, dest='launch_user', action='store', + help='The record path or UID of the PAM User record to configure as the launch ' + 'credential on the PAM Resource') + parser.add_argument('--protocol', '-p', dest='protocol', choices=protocols, + help='Set connection protocol') + parser.add_argument('--connections', '-cn', dest='connections', choices=choices, + help='Set connections permissions') + parser.add_argument('--connections-recording', '-cr', dest='recording', choices=choices, + help='Set recording connections permissions for the resource') + parser.add_argument('--typescript-recording', '-tr', dest='typescriptrecording', choices=choices, + help='Set TypeScript recording permissions for the resource') + parser.add_argument('--connections-override-port', '-cop', required=False, dest='connections_override_port', + action='store', help='Port to use for connections. If not provided, ' + 'the port from the record will be used.') + parser.add_argument('--key-events', '-k', dest='key_events', choices=choices, + help='Toggle Key Events settings') + parser.add_argument('--silent', '-s', required=False, dest='silent', action='store_true', + help='Silent mode - don\'t print PAM User, PAM Config etc.') + + def execute(self, context: KeeperParams, **kwargs): + connection_override_port = kwargs.get('connections_override_port', None) + + # Convert on/off/default to True/False/None + _connections = TunnelDAG._convert_allowed_setting(kwargs.get('connections', None)) + _recording = TunnelDAG._convert_allowed_setting(kwargs.get('recording', None)) + _typescript_recording = TunnelDAG._convert_allowed_setting(kwargs.get('typescriptrecording', None)) + + vault = context.vault + + if connection_override_port: + try: + connection_override_port = int(connection_override_port) + except ValueError: + raise base.CommandError(f'--connections-override-port must be an integer') + + record_name = kwargs.get('record') + if not record_name: + raise base.CommandError(f'Record parameter is required.') + record = vault.vault_data.load_record(record_name) + if not record: + raise base.CommandError(f'Record \"{record_name}\" not found.') + if not isinstance(record, vault_record.TypedRecord): + raise base.CommandError(f'Record \"{record_name}\" can not be edited.') + + config_name = kwargs.get('config', None) + cfg_rec = vault.vault_data.load_record(config_name) + if not cfg_rec and record.version == 6: + cfg_rec = record + config_uid = cfg_rec.record_uid if cfg_rec else None + + record_uid = record.record_uid + record_type = record.record_type + if record_type not in ("pamMachine pamDatabase pamDirectory pamNetworkConfiguration pamAwsConfiguration " + "pamRemoteBrowser pamAzureConfiguration").split(): + raise base.CommandError(f"This record's type is not supported for connections. " + f"Connections are only supported on pamMachine, pamDatabase, pamDirectory, " + f"pamRemoteBrowser, pamNetworkConfiguration pamAwsConfiguration, and " + f"pamAzureConfiguration records") + + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(vault) + if record_type in "pamNetworkConfiguration pamAwsConfiguration pamAzureConfiguration".split(): + tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, record_uid, is_config=True) + tdag.edit_tunneling_config(connections=_connections, session_recording=_recording, typescript_recording=_typescript_recording) + if not kwargs.get("silent", False): tdag.print_tunneling_config(record_uid, None) + else: + traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') + + seed = os.urandom(32) + dirty = False + if not traffic_encryption_key or not traffic_encryption_key.value: + base64_seed = utils.base64_url_encode(seed) + record_seed = vault_record.TypedField.create_field('trafficEncryptionSeed', base64_seed, required=False) + + record_types_with_seed = ("pamDatabase", "pamDirectory", "pamMachine", "pamRemoteBrowser") + if traffic_encryption_key: + traffic_encryption_key.value = [base64_seed] + elif record.record_type in record_types_with_seed: + record.fields.append(record_seed) + else: + record.custom.append(record_seed) + dirty = True + + protocol = kwargs.get("protocol", None) + pam_settings = record.get_typed_field('pamSettings') + if not pam_settings: + pre_settings = {"connection": {}, "portForward": {}} + if _connections: + if connection_override_port: + pre_settings["connection"]["port"] = connection_override_port + if protocol: + pre_settings["connection"]["protocol"] = protocol + elif protocol or connection_override_port: + logger.warning(f'Connection override port and protocol can be set only when connections are enabled ' + f'with --connections=on option') + if pre_settings: + pam_settings = vault_record.TypedField.create_field('pamSettings', required=False) + pam_settings.value = [pre_settings] + record.custom.append(pam_settings) + dirty = True + else: + if not pam_settings.value: + pam_settings.value.append({"connection": {}, "portForward": {}}) + if not pam_settings.value[0]: + pam_settings.value[0] = {"connection": {}, "portForward": {}} + if _connections: + if connection_override_port: + pam_settings.value[0]["connection"]["port"] = connection_override_port + elif connection_override_port is not None: + pam_settings.value[0]["connection"].pop("port", None) + if protocol: + pam_settings.value[0]["connection"]["protocol"] = protocol + elif protocol is not None: + pam_settings.value[0]["connection"].pop("protocol", None) + dirty = True + elif protocol or connection_override_port: + logger.warning(f'Connection override port and protocol can be set only when connections are enabled ' + f'with --connections=on option') + + key_events = kwargs.get('key_events') # on/off/default + if key_events: + psv = pam_settings.value[0] if pam_settings and pam_settings.value else {} + vcon = psv.get('connection', {}) if isinstance(psv, dict) else {} + rik = vcon.get('recordingIncludeKeys') if isinstance(vcon, dict) else None + if key_events == 'default': + if rik is not None: + pam_settings.value[0]["connection"].pop('recordingIncludeKeys', None) + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already set to "default" on record={record_uid}') + elif key_events == 'on': + if dag_utils.value_to_boolean(key_events) != dag_utils.value_to_boolean(rik): + pam_settings.value[0]["connection"]["recordingIncludeKeys"] = True + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already enabled on record={record_uid}') + elif key_events == 'off': + if dag_utils.value_to_boolean(key_events) != dag_utils.value_to_boolean(rik): + pam_settings.value[0]["connection"]["recordingIncludeKeys"] = False + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already disabled on record={record_uid}') + else: + logger.debug(f'Unexpected value for --key-events {key_events} (ignored)') + + if dirty: + record_management.update_record(vault, record) + vault.sync_down() + + traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') + if not traffic_encryption_key: + raise base.CommandError(f"Unable to add Seed to record {record_uid}. " + f"Please make sure you have edit rights to record {record_uid}") + dirty = False + + existing_config_uid = get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) + + tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid) + old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid) + + if config_uid and existing_config_uid != config_uid: + old_dag.remove_from_dag(record_uid) + tdag.link_resource_to_config(record_uid) + + if tdag is None or not tdag.linking_dag.has_graph: + raise base.CommandError(f"No PAM Configuration UID set. " + f"This must be set or supplied for connections to work. This can be done by adding " + f"' --config [ConfigUID] " + f" The ConfigUID can be found by running " + f"'pam config list'") + + if not tdag.check_tunneling_enabled_config(enable_connections=_connections, + enable_session_recording=_recording, + enable_typescript_recording=_typescript_recording): + if not kwargs.get("silent", False): tdag.print_tunneling_config(config_uid, None) + command = f"'pam connection edit {config_uid}" + if _connections and not tdag.check_tunneling_enabled_config(enable_connections=_connections): + command += f" --connections=on" if _connections else "" + if _recording and not tdag.check_tunneling_enabled_config(enable_session_recording=_recording): + command += f" --connections-recording=on" if _recording else "" + if _typescript_recording and not tdag.check_tunneling_enabled_config(enable_typescript_recording=_typescript_recording): + command += f" --typescript-recording=on" if _typescript_recording else "" + + logger.info(f"The settings are denied by PAM Configuration: {config_uid}. " + f"Please enable settings for the configuration by running\n" + f"{command}'") + return + + if not tdag.is_tunneling_config_set_up(record_uid): + tdag.link_resource_to_config(record_uid) + + if not tdag.is_tunneling_config_set_up(record_uid): + logger.info(f"No PAM Configuration UID set. This must be set for connections to work. " + f"This can be done by running " + f"'pam connection edit {record_uid} --config [ConfigUID] --enable-connections' " + f"The ConfigUID can be found by running 'pam config list'") + return + allowed_settings_name = "allowedSettings" + if record.record_type == "pamRemoteBrowser": + allowed_settings_name = "pamRemoteBrowserSettings" + + if _connections is not None and tdag.check_if_resource_allowed(record_uid, "connections") != _connections: + dirty = True + if _recording is not None and tdag.check_if_resource_allowed(record_uid, "sessionRecording") != _recording: + dirty = True + if _typescript_recording is not None and tdag.check_if_resource_allowed(record_uid, "typescriptRecording") != _typescript_recording: + dirty = True + + if dirty: + tdag.set_resource_allowed(resource_uid=record_uid, + allowed_settings_name=allowed_settings_name, + connections=kwargs.get('connections', None), + session_recording=kwargs.get('recording', None), + typescript_recording=kwargs.get('typescriptrecording', None)) + + admin_name = kwargs.get('admin') + adm_rec = vault.vault_data.load_record(admin_name) + admin_uid = adm_rec.record_uid if adm_rec else None + if admin_uid and record_type in ("pamDatabase", "pamDirectory", "pamMachine"): + tdag.link_user_to_resource(admin_uid, record_uid, is_admin=True, belongs_to=True) + + launch_user_name = kwargs.get('launch_user') + if launch_user_name: + launch_rec = vault.vault_data.load_record(launch_user_name) + if not launch_rec: + raise base.CommandError(f'Launch user record "{launch_user_name}" not found.') + if not isinstance(launch_rec, vault_record.TypedRecord) or launch_rec.record_type != 'pamUser': + raise base.CommandError(f'Launch user record must be a pamUser record type.') + launch_uid = launch_rec.record_uid + if record_type in ("pamDatabase", "pamDirectory", "pamMachine"): + tdag.clear_launch_credential_for_resource(record_uid, exclude_user_uid=launch_uid) + tdag.link_user_to_resource(launch_uid, record_uid, is_admin=True, belongs_to=True) + tdag.upgrade_resource_meta_to_v1(record_uid) + + # Print out PAM Settings + if not kwargs.get("silent", False): tdag.print_tunneling_config(record_uid, record.get_typed_field('pamSettings'), config_uid) diff --git a/keepercli-package/src/keepercli/commands/pam/pam_dto.py b/keepercli-package/src/keepercli/commands/pam/pam_dto.py new file mode 100644 index 00000000..1638102c --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_dto.py @@ -0,0 +1,158 @@ + +import abc +import json + +from keepersdk import crypto, utils + + +class GatewayAction(metaclass=abc.ABCMeta): + + def __init__(self, action, is_scheduled, gateway_destination=None, inputs=None, conversation_id=None, message_id=None): + self.action = action + self.is_scheduled = is_scheduled + self.gateway_destination = gateway_destination + self.inputs = inputs + self.conversationId = conversation_id + self.messageId = message_id + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + @staticmethod + def generate_conversation_id(is_bytes=False): + message_id_bytes = crypto.get_random_bytes(16) + if is_bytes: + return message_id_bytes + else: + message_id = utils.base64_url_encode(message_id_bytes) + return message_id + + @staticmethod + def conversation_id_to_message_id(conversation_id): + """Convert conversationId to messageId format (replace + with -, / with _)""" + if conversation_id: + return conversation_id.rstrip('=').replace('+', '-').replace('/', '_') + return None + + +class GatewayActionRotateInputs: + + def __init__(self, record_uid, configuration_uid, pwd_complexity_encrypted, resource_uid=None): + self.recordUid = record_uid + self.configurationUid = configuration_uid + self.pwdComplexity = pwd_complexity_encrypted + self.resourceRef = resource_uid + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionRotate(GatewayAction): + + def __init__(self, inputs: GatewayActionRotateInputs, conversation_id=None, gateway_destination=None): + super().__init__('rotate', inputs=inputs, conversation_id=conversation_id, + gateway_destination=gateway_destination, is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionJobInfoInputs: + + def __init__(self, job_id): + self.jobId = job_id + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionJobInfo(GatewayAction): + + def __init__(self, inputs: GatewayActionJobInfoInputs, conversation_id=None): + super().__init__('job-info', inputs=inputs, conversation_id=conversation_id, is_scheduled=False) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionDiscoverJobStartInputs: + + def __init__(self, configuration_uid, user_map, shared_folder_uid, resource_uid=None, languages=None, + # Settings + include_machine_dir_users=False, + include_azure_aadds=False, + skip_rules=False, + skip_machines=False, + skip_databases=False, + skip_directories=False, + skip_cloud_users=False, + credentials=None): + if languages is None: + languages = ["en_US"] + self.configurationUid = configuration_uid + self.resourceUid = resource_uid + self.userMap = user_map + self.sharedFolderUid = shared_folder_uid + self.languages = languages + self.includeMachineDirUsers = include_machine_dir_users + self.includeAzureAadds = include_azure_aadds + self.skipRules = skip_rules + self.skipMachines = skip_machines + self.skipDatabases = skip_databases + self.skipDirectories = skip_directories + self.skipCloudUsers = skip_cloud_users + + if credentials is None: + credentials = [] + self.credentials = credentials + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionDiscoverJobStart(GatewayAction): + + def __init__(self, inputs: GatewayActionDiscoverJobStartInputs, conversation_id=None): + super().__init__('discover-job-start', inputs=inputs, conversation_id=conversation_id, is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionDiscoverJobRemoveInputs: + + def __init__(self, configuration_uid, job_id): + self.configurationUid = configuration_uid + self.jobId = job_id + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + +class GatewayActionDiscoverJobRemove(GatewayAction): + + def __init__(self, inputs: GatewayActionDiscoverJobRemoveInputs, conversation_id=None): + super().__init__('discover-job-remove', inputs=inputs, conversation_id=conversation_id, is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionDiscoverRuleValidateInputs: + + def __init__(self, configuration_uid, statement): + self.configurationUid = configuration_uid + self.statement = statement + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionDiscoverRuleValidate(GatewayAction): + + def __init__(self, inputs: GatewayActionDiscoverRuleValidateInputs, conversation_id=None): + super().__init__('discover-rule-validate', inputs=inputs, conversation_id=conversation_id, + is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + diff --git a/keepercli-package/src/keepercli/commands/pam/pam_gateway_action.py b/keepercli-package/src/keepercli/commands/pam/pam_gateway_action.py new file mode 100644 index 00000000..777ce710 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_gateway_action.py @@ -0,0 +1,521 @@ +import argparse +import re +import time +from typing import Dict +from urllib.parse import urlparse, urlunparse + +from keepersdk import crypto, utils +from keepersdk.proto import APIRequest_pb2 +from keepersdk.vault import vault_record, vault_online + +from ...commands import base +from ... import api +from ...params import KeeperParams +from ...helpers import router_utils, timeout_utils, email_utils, record_utils +from ...commands.pam.pam_dto import GatewayAction, GatewayActionRotate, GatewayActionRotateInputs, GatewayActionJobInfoInputs, GatewayActionJobInfo +from .discovery.discover import (PAMGatewayActionDiscoverJobStartCommand, PAMGatewayActionDiscoverJobStatusCommand, + PAMGatewayActionDiscoverJobRemoveCommand, PAMGatewayActionDiscoverResultProcessCommand, PAMDiscoveryRuleCommand) +from .service.service_commands import PAMActionServiceListCommand, PAMActionServiceAddCommand, PAMActionServiceRemoveCommand +from .saas.saas_commands import PAMActionSaasConfigCommand, PAMActionSaasSetCommand, PAMActionSaasRemoveCommand, PAMActionSaasUserCommand, PAMActionSaasUpdateCommand +from .debug.debug_info import PAMDebugInfoCommand +from .debug.debug_gateway import PAMDebugGatewayCommand +from .debug.debug_graph import PAMDebugGraphCommand +from .debug.debug_acl import PAMDebugACLCommand +from .debug.debug_link import PAMDebugLinkCommand +from .debug.debug_rs import PAMDebugRotationSettingsCommand +from .debug.debug_vertex import PAMDebugVertexCommand + +from keepersdk.proto import pam_pb2 +from keepersdk.helpers import pam_config_facade, config_utils +from keepersdk.helpers.tunnel import tunnel_graph, tunnel_utils +from keepersdk.helpers.keeper_dag import record_link as record_link_utils, dag_utils + +logger = api.get_logger() + + +class PAMGatewayActionServerInfoCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='dr-info-command') + PAMGatewayActionServerInfoCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=False, dest='gateway_uid', action='store', help='Gateway UID') + parser.add_argument('--verbose', '-v', required=False, dest='verbose', action='store_true', help='Verbose Output') + + def execute(self, context: KeeperParams, **kwargs): + destination_gateway_uid_str = kwargs.get('gateway_uid') + is_verbose = kwargs.get('verbose') + router_response = router_utils.router_send_action_to_gateway( + context=context, + gateway_action=GatewayAction(action='server_info', is_scheduled=False), + message_type=pam_pb2.CMT_GENERAL, + is_streaming=False, + destination_gateway_uid_str=destination_gateway_uid_str + ) + + router_utils.print_router_response(router_response, 'gateway_info', is_verbose=is_verbose, gateway_uid=destination_gateway_uid_str) + + +class PAMGatewayActionRotateCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action rotate') + PAMGatewayActionRotateCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--record-uid', '-r', dest='record_uid', action='store', help='Record UID to rotate') + parser.add_argument('--folder', '-f', dest='folder', action='store', help='Shared folder UID or title pattern to rotate') + parser.add_argument('--dry-run', '-n', dest='dry_run', default=False, action='store_true', help='Enable dry-run mode') + parser.add_argument('--self-destruct', dest='self_destruct', action='store', + metavar='[(m)inutes|(h)ours|(d)ays]', + help='Create one-time share link that expires after duration') + parser.add_argument('--email-config', dest='email_config', action='store', + help='Email configuration name to use for sending (required with --send-email)') + parser.add_argument('--send-email', dest='send_email', action='store', + help='Email address to send credentials after rotation') + parser.add_argument('--email-message', dest='email_message', action='store', + help='Custom message to include in email') + + def execute(self, context: KeeperParams, **kwargs): + if context.vault is None: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + + vault = context.vault + + record_uid = kwargs.get('record_uid', '') + folder = kwargs.get('folder', '') + recursive = kwargs.get('recursive', False) + pattern = kwargs.get('pattern', '') # additional record title match pattern + dry_run = kwargs.get('dry_run', False) + + # Store email/share arguments as instance variables + self.self_destruct = kwargs.get('self_destruct') + self.email_config = kwargs.get('email_config') + self.send_email = kwargs.get('send_email') + self.email_message = kwargs.get('email_message') + + vault = context.vault + + if self.send_email: + if not self.email_config: + raise base.CommandError('--send-email requires --email-config to specify email configuration') + + # Find and load email config to validate provider and dependencies + try: + config_uid = email_utils.find_email_config_record(vault, self.email_config) + email_config_obj = email_utils.load_email_config_from_record(vault, config_uid) + + is_valid, error_message = email_utils.validate_email_provider_dependencies(email_config_obj.provider) + + if not is_valid: + raise base.CommandError(f'\n{error_message}') + + except Exception as e: + if isinstance(e, base.CommandError): + raise + raise base.CommandError(f'Failed to validate email configuration: {e}') + + if not record_uid and not folder: + logger.info(f'the following arguments are required: --record-uid/-r or --folder/-f') + return + + if not folder: + self.record_rotate(context, record_uid) + return + + folders = [] # root folders matching UID or title pattern + records = [] # record UIDs of all v3/pamUser records + + if folder in vault.vault_data.folders(): + fldr = vault.vault_data.get_folder(folder) + + if fldr.folder_type in ('shared_folder', 'shared_folder_folder'): + folders.append(folder) + else: + logger.debug(f'Folder skipped (not a shared folder/subfolder) - {folder} {fldr.name}') + else: + rx_name = self.str_to_regex(folder) + for fuid in vault.vault_data.folders(): + fldr = vault.vault_data.get_folder(fuid.folder_uid) + if fldr.folder_type in ('shared_folder', 'shared_folder_folder'): + if fldr.name and rx_name.search(fldr.name): + folders.append(fldr.folder_uid) + + folders = list(set(folders)) + + if recursive and len(folders) > 1: + roots: Dict[str, list] = {} + for fuid in folders: + roots.setdefault(vault.vault_data.get_folder(fuid).folder_scope_uid, []).append(fuid) + uniq = [] + for fuid in roots: + fldrs = list(set(roots[fuid])) + if len(fldrs) == 1: + uniq.append(fldrs[0]) + elif fuid in fldrs: + uniq.append(fuid) + else: + fldrset = set(fldrs) + for fldr in fldrs: + path = [] + child = fldr + while vault.vault_data.get_folder(child).folder_uid != fuid: + path.append(child) + child = vault.vault_data.get_folder(child).parent_uid + path.append(child) + path = path[1:] if path else [] + if not set(path) & fldrset: + uniq.append(fldr) + folders = list(set(uniq)) + + for fldr in folders: + if recursive: + logger.warning('--recursive/-a option not implemented (ignored)') + + folder_sub = vault.vault_data.get_folder(fldr) + folder_records = folder_sub.records + + if folder_records: + logger.debug(f"folder {fldr} empty - no records in folder(skipped)") + continue + for ruid in folder_records: + record = vault.vault_data.get_record(ruid) + if record and record.record_type == 'pamUser': + records.append(ruid) + records = list(set(records)) + + logger.info(f'Selected for rotation - folders: {len(folders)}, records: {len(records)}, recursive={recursive}') + + if logger.isEnabledFor(logger.DEBUG): + for fldr in folders: + fobj = vault.vault_data.get_folder(fldr) + title = fobj.name if fobj else '' + logger.debug(f'Rotation Folder UID: {fldr} {title}') + for rec in records: + record = vault.vault_data.get_record(rec) + title = record.title if record else '' + logger.debug(f'Rotation Record UID: {rec} {title}') + + if dry_run: + return + + for record_uid in records: + delay = 0 + while True: + try: + self.record_rotate(context, record_uid, True) + break + except Exception as e: + msg = str(e) + if re.search(r"throttle", msg, re.IGNORECASE): + delay = (delay+10) % 100 + logger.debug(f'Record UID: {record_uid} was throttled (retry in {delay} sec)') + time.sleep(1+delay) + else: + logger.error(f'Record UID: {record_uid} skipped: non-throttling, non-recoverable error: {msg}') + break + + def record_rotate(self, context: KeeperParams, record_uid, slient:bool = False): + vault = context.vault + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.TypedRecord): + logger.error(f'Record [{record_uid}] is not available.') + return + + ri = record_utils.record_rotation_get(vault, utils.base64_url_decode(record.record_uid)) + ri_pwd_complexity_encrypted = ri.pwdComplexity + if not ri_pwd_complexity_encrypted: + rule_list_dict = { + 'length': 20, + 'caps': 1, + 'lowercase': 1, + 'digits': 1, + 'special': 1, + } + record_key = vault.vault_data.get_record_key(record.record_uid) + ri_pwd_complexity_encrypted = utils.base64_url_encode(router_utils.encrypt_pwd_complexity(rule_list_dict, record_key)) + + resource_uid = None + + encrypted_session_token, encrypted_transmission_key, transmission_key = tunnel_utils.get_keeper_tokens(vault) + config_uid = tunnel_utils.get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) + if not config_uid: + ri_rotation_setting_uid = utils.base64_url_encode(ri.configurationUid) + resource_uid = utils.base64_url_encode(ri.resourceUid) + pam_config = vault.vault_data.load_record(ri_rotation_setting_uid) + if not isinstance(pam_config, vault_record.TypedRecord): + logger.error(f'PAM Configuration [{ri_rotation_setting_uid}] is not available.') + return + facade = pam_config_facade.PamConfigurationRecordFacade() + facade.record = pam_config + + config_uid = facade.controller_uid + + if not resource_uid: + tmp_dag = tunnel_graph.TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, record.record_uid) + resource_uid = tmp_dag.get_resource_uid(record_uid) + if not resource_uid: + is_noop = False + pam_config = vault.vault_data.load_record(config_uid) + + record_link = record_link_utils.RecordLink(record=pam_config, + context=context, + fail_on_corrupt=False) + acl = record_link.get_acl(record_uid, pam_config.record_uid) + if acl is not None and acl.rotation_settings is not None: + is_noop = acl.rotation_settings.noop + + if is_noop is False: + noop_field = record.get_typed_field('text', 'NOOP') + is_noop = dag_utils.value_to_boolean(noop_field.value[0]) if noop_field and noop_field.value else False + + if not is_noop: + logger.error(f'Resource UID not found for record [{record_uid}]. please configure it ' + f'"pam rotation user {record_uid} --resource RESOURCE_UID"') + return + + controller = config_utils.configuration_controller_get(vault, utils.base64_url_decode(config_uid)) + if not controller.controllerUid: + raise base.CommandError(f'Gateway UID not found for configuration ' + f'{config_uid}.') + + enterprise_controllers_connected = router_utils.router_get_connected_gateways(vault) + + controller_from_config_bytes = controller.controllerUid + gateway_uid = utils.base64_url_encode(controller.controllerUid) + if enterprise_controllers_connected: + router_controllers = {controller.controllerUid: controller for controller in + list(enterprise_controllers_connected.controllers)} + connected_controller = router_controllers.get(controller_from_config_bytes) + + if not connected_controller: + logger.warning(f'The Gateway "{gateway_uid}" is down.') + return + else: + logger.warning(f'There are no connected gateways.') + return + + action_inputs = GatewayActionRotateInputs( + record_uid=record_uid, + configuration_uid=config_uid, + pwd_complexity_encrypted=ri_pwd_complexity_encrypted, + resource_uid=resource_uid + ) + + conversation_id = GatewayAction.generate_conversation_id() + + router_response = router_utils.router_send_action_to_gateway( + context=context, gateway_action=GatewayActionRotate(inputs=action_inputs, conversation_id=conversation_id, + gateway_destination=gateway_uid), + message_type=pam_pb2.CMT_ROTATE, is_streaming=False, + transmission_key=transmission_key, + encrypted_transmission_key=encrypted_transmission_key, + encrypted_session_token=encrypted_session_token) + + if (self.self_destruct or self.send_email) and router_response: + try: + vault.sync_down(force=True) + record = vault.vault_data.load_record(record_uid) + if isinstance(record, vault_record.TypedRecord): + self._handle_post_rotation_email(vault, record) + except Exception as e: + logger.warning(f'Post-rotation email handling failed: {e}') + + if not slient: + router_utils.print_router_response(router_response, 'job_info', conversation_id, gateway_uid=gateway_uid) + + def _handle_post_rotation_email(self, vault: vault_online.VaultOnline, record): + """Handle email sending and share link creation after successful rotation.""" + try: + if self.send_email and not self.email_config: + logger.warning(f'--send-email requires --email-config. Skipping email.') + return + + user_requested_self_destruct = bool(self.self_destruct) + + if self.send_email and not self.self_destruct: + self.self_destruct = '24h' + logger.info('--send-email used without --self-destruct, creating 24 hour time-based share link') + + share_url = None + expiration_text = None + if self.self_destruct: + try: + expiration_period = timeout_utils.parse_timeout(self.self_destruct) + expire_seconds = int(expiration_period.total_seconds()) + + if expire_seconds <= 0: + logger.warning(f'Invalid --self-destruct value. Skipping share link.') + return + + if expire_seconds >= 86400: + days = expire_seconds // 86400 + expiration_text = f"{days} day{'s' if days > 1 else ''}" + elif expire_seconds >= 3600: + hours = expire_seconds // 3600 + expiration_text = f"{hours} hour{'s' if hours > 1 else ''}" + else: + minutes = expire_seconds // 60 + expiration_text = f"{minutes} minute{'s' if minutes > 1 else ''}" + + logger.info(f'Creating one-time share link expiring in {self.self_destruct}...') + record_uid = record.record_uid + record_key = record.record_key + client_key = utils.generate_aes_key() + client_id = crypto.hmac_sha512(client_key, 'KEEPER_SECRETS_MANAGER_CLIENT_ID'.encode()) + rq = APIRequest_pb2.AddExternalShareRequest() + rq.recordUid = utils.base64_url_decode(record_uid) + rq.encryptedRecordKey = crypto.encrypt_aes_v2(record_key, client_key) + rq.clientId = client_id + rq.accessExpireOn = utils.current_milli_time() + int(expiration_period.total_seconds() * 1000) + rq.isSelfDestruct = user_requested_self_destruct + vault.keeper_auth.execute_auth_rest( + rest_endpoint='vault/external_share_add', + request=rq, + response_type=APIRequest_pb2.Device + ) + parsed = urlparse(vault.keeper_auth.keeper_endpoint.server) + server_netloc = parsed.netloc if parsed.netloc else parsed.path + share_url = urlunparse(('https', server_netloc, '/vault/share', None, None, utils.base64_url_encode(client_key))) + logger.info(f'Share link created successfully') + except Exception as e: + logger.warning(f'Failed to create share link: {e}') + return + + if self.send_email and self.email_config and share_url: + try: + logger.info(f'Loading email configuration: {self.email_config}') + config_uid = email_utils.find_email_config_record(vault, self.email_config) + if not config_uid: + logger.warning(f'Email configuration "{self.email_config}" not found. Skipping email.') + return + + email_config = email_utils.load_email_config_from_record(vault, config_uid) + + custom_message = self.email_message or 'Your password has been rotated. Click the link below to view your new credentials.' + + html_content = email_utils.build_onboarding_email( + share_url=share_url, + custom_message=custom_message, + record_title=record.title, + expiration=expiration_text + ) + + logger.info(f'Sending email to {self.send_email}...') + email_sender = email_utils.EmailSender(email_config) + email_sender.send( + to=self.send_email, + subject=f"Password Rotated: {record.title}", + body=html_content, + html=True + ) + + if email_config.is_oauth_provider() and email_config._oauth_tokens_updated: + logger.info('Updating OAuth tokens in email configuration record...') + email_utils.update_oauth_tokens_in_record( + vault, + config_uid, + email_config.oauth_access_token, + email_config.oauth_refresh_token, + email_config.oauth_token_expiry + ) + + logger.info(f'Email sent successfully to {self.send_email}') + + except Exception as e: + logger.warning(f'Failed to send email: {e}') + return + + except Exception as e: + logger.warning(f'Error in post-rotation email handler: {e}') + + def str_to_regex(self, text): + text = str(text) + try: + pattern = re.compile(text, re.IGNORECASE) + except: + pattern = re.compile(re.escape(text), re.IGNORECASE) + logger.debug(f"regex pattern {text} failed to compile (using it as plaintext pattern)") + return pattern + +class PAMGatewayActionJobCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action job') + PAMGatewayActionJobCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=False, dest='gateway_uid', action='store', + help='Gateway UID. Needed only if there are more than one gateway running') + parser.add_argument('job_id', help='Job ID') + + def execute(self, context: KeeperParams, **kwargs): + job_id = kwargs.get('job_id') + gateway_uid = kwargs.get('gateway_uid') + + logger.info(f"Job id to check [{job_id}]") + + action_inputs = GatewayActionJobInfoInputs(job_id) + + conversation_id = GatewayAction.generate_conversation_id() + router_response = router_utils.router_send_action_to_gateway( + context=context, + gateway_action=GatewayActionJobInfo(inputs=action_inputs, conversation_id=conversation_id), + message_type=pam_pb2.CMT_GENERAL, + is_streaming=False, + destination_gateway_uid_str=gateway_uid, + ) + + router_utils.print_router_response(router_response, 'job_info', original_conversation_id=conversation_id, gateway_uid=gateway_uid) + + +class PAMDiscoveryCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Discovery') + self.register_command(PAMGatewayActionDiscoverJobStartCommand(), 'start', 's') + self.register_command(PAMGatewayActionDiscoverJobStatusCommand(), 'status', 'st') + self.register_command(PAMGatewayActionDiscoverJobRemoveCommand(), 'remove', 'r') + self.register_command(PAMGatewayActionDiscoverResultProcessCommand(), 'process', 'p') + self.register_command(PAMDiscoveryRuleCommand(), 'rule', 'r') + + self.default_verb = 'status' + + +class PAMActionServiceCommand(base.GroupCommand): + + def __init__(self): + super().__init__('PAM Action Service') + self.register_command(PAMActionServiceListCommand(), 'list', 'l') + self.register_command(PAMActionServiceAddCommand(), 'add', 'a') + self.register_command(PAMActionServiceRemoveCommand(), 'remove', 'r') + self.default_verb = 'list' + + +class PAMActionSaasCommand(base.GroupCommand): + + def __init__(self): + super().__init__('PAM Action Saas') + self.register_command(PAMActionSaasConfigCommand(), 'config', 'c') + self.register_command(PAMActionSaasSetCommand(), 'set', 's') + self.register_command(PAMActionSaasRemoveCommand(), 'remove', 'r') + self.register_command(PAMActionSaasUserCommand(), 'user', 'i') + self.register_command(PAMActionSaasUpdateCommand(), 'update', 'u') + + +class PAMDebugCommand(base.GroupCommand): + + def __init__(self): + super().__init__('PAM Debug') + self.register_command(PAMDebugInfoCommand(), 'info', 'i') + self.register_command(PAMDebugGatewayCommand(), 'gateway', 'g') + self.register_command(PAMDebugGraphCommand(), 'graph', 'r') + self.register_command(PAMDebugACLCommand(), 'acl', 'c') + self.register_command(PAMDebugLinkCommand(), 'link', 'l') + self.register_command(PAMDebugRotationSettingsCommand(), 'rs-reset', 'rs') + self.register_command(PAMDebugVertexCommand(), 'vertex', 'v') + + diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py new file mode 100644 index 00000000..10f6ff58 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py @@ -0,0 +1,416 @@ +import os +import argparse + +from keepersdk import utils +from keepersdk.helpers.keeper_dag import dag_utils +from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG +from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid +from keepersdk.vault import record_management, vault_record + +from .. import base +from ... import api +from ...params import KeeperParams + +choices = ['on', 'off', 'default'] + +logger = api.get_logger() + + +class PAMRbiEditCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rbi edit') + PAMRbiEditCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + # Record and Configuration + parser.add_argument('--record', '-r', type=str, required=True, dest='record', action='store', + help='The record UID or path of the RBI record.') + parser.add_argument('--configuration', '-c', required=False, dest='config', action='store', + help='The PAM Configuration UID or path to use for connections. ' + 'Use command `pam config list` to view available PAM Configurations.') + + # RBI and Recording Settings + parser.add_argument('--remote-browser-isolation', '-rbi', dest='rbi', choices=choices, + help='Set RBI permissions') + parser.add_argument('--connections-recording', '-cr', dest='recording', choices=choices, + help='Set recording connections permissions for the resource') + parser.add_argument('--key-events', '-k', dest='key_events', choices=choices, + help='Toggle Key Events settings') + + # Browser Settings + parser.add_argument('--allow-url-navigation', '-nav', dest='allow_url_navigation', choices=choices, + help='Allow navigation via direct URL manipulation (on/off/default)') + parser.add_argument('--ignore-server-cert', '-isc', dest='ignore_server_cert', choices=choices, + help='Ignore server certificate errors (on/off/default)') + + # URL Filtering + parser.add_argument('--allowed-urls', '-au', dest='allowed_urls', action='append', + help='Allowed URL patterns (can specify multiple times)') + parser.add_argument('--allowed-resource-urls', '-aru', dest='allowed_resource_urls', action='append', + help='Allowed resource URL patterns (can specify multiple times)') + + # Autofill Settings + parser.add_argument('--autofill-credentials', '-a', type=str, required=False, dest='autofill', action='store', + help='The record UID or path of the RBI Autofill Credentials record.') + parser.add_argument('--autofill-targets', '-at', dest='autofill_targets', action='append', + help='Autofill target selectors (can specify multiple times)') + + # Clipboard Settings + parser.add_argument('--allow-copy', '-cpy', dest='allow_copy', choices=choices, + help='Allow copying to clipboard (on/off/default)') + parser.add_argument('--allow-paste', '-p', dest='allow_paste', choices=choices, + help='Allow pasting from clipboard (on/off/default)') + + # Audio Settings + parser.add_argument('--disable-audio', '-da', dest='disable_audio', choices=choices, + help='Disable audio for RBI sessions (on/off/default)') + parser.add_argument('--audio-channels', '-ac', dest='audio_channels', type=int, + help='Number of audio channels (e.g., 1 for mono, 2 for stereo)') + parser.add_argument('--audio-bit-depth', '-bd', dest='audio_bit_depth', type=int, choices=[8, 16], + help='Audio bit depth (8 or 16)') + parser.add_argument('--audio-sample-rate', '-sr', dest='audio_sample_rate', type=int, + help='Audio sample rate in Hz (e.g., 44100, 48000)') + + # Utility + parser.add_argument('--silent', '-s', required=False, dest='silent', action='store_true', + help='Silent mode - don\'t print PAM User, PAM Config etc.') + + def execute(self, context: KeeperParams, **kwargs): + record_name = kwargs.get('record') or '' + config_name = kwargs.get('config') or '' + autofill = kwargs.get('autofill') or '' + key_events = kwargs.get('key_events') # on/off/default + rbi = kwargs.get('rbi') # on/off/default + recording = kwargs.get('recording') # on/off/default + silent = kwargs.get('silent') or False + + # New RBI settings (Phase 1 - KC-1034) + allow_url_navigation = kwargs.get('allow_url_navigation') # on/off/default/None + ignore_server_cert = kwargs.get('ignore_server_cert') # on/off/default/None + allowed_urls = kwargs.get('allowed_urls') # list or None + allowed_resource_urls = kwargs.get('allowed_resource_urls') # list or None + autofill_targets = kwargs.get('autofill_targets') # list or None + allow_copy = kwargs.get('allow_copy') # on/off/default/None + allow_paste = kwargs.get('allow_paste') # on/off/default/None + disable_audio = kwargs.get('disable_audio') # on/off/default/None + audio_channels = kwargs.get('audio_channels') # int or None + audio_bit_depth = kwargs.get('audio_bit_depth') # int or None + audio_sample_rate = kwargs.get('audio_sample_rate') # int or None + + if not record_name: + raise base.CommandError('Record parameter is required.') + + has_new_settings = any([ + allow_url_navigation is not None, + ignore_server_cert is not None, + allowed_urls is not None, + allowed_resource_urls is not None, + autofill_targets is not None, + allow_copy is not None, + allow_paste is not None, + disable_audio is not None, + audio_channels is not None, + audio_bit_depth is not None, + audio_sample_rate is not None + ]) + + if not (autofill or key_events or config_name or rbi or recording or has_new_settings): + raise base.CommandError('At least one parameter is required. ' + 'If the record is not linked to PAM Config, -c option is required.') + + vault = context.vault + + record = vault.vault_data.load_record(record_name) + if not record: + raise base.CommandError(f'Record \"{record_name}\" not found.') + if not isinstance(record, vault_record.TypedRecord): + raise base.CommandError(f'Record \"{record_name}\" can not be edited.') + + record_uid = record.record_uid + record_type = record.record_type + if record_type != "pamRemoteBrowser": + raise base.CommandError(f"Record {record_uid} of type {record_type} " + "cannot be set up for RBI connections. " + f"RBI connection records must be of type: pamRemoteBrowser") + + dirty = False + traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') + if not traffic_encryption_key or not traffic_encryption_key.value: + seed = os.urandom(32) + base64_seed = utils.base64_url_encode(seed) + record_seed = vault_record.TypedField.create_field('trafficEncryptionSeed', base64_seed, required=False) + if traffic_encryption_key: + traffic_encryption_key.value = [base64_seed] + else: + record.fields.append(record_seed) + dirty = True + + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + if not rbs_fld: + rbsettings = {'connection': {'protocol': 'http', 'httpCredentialsUid': ''}} + pam_rbsettings = vault_record.TypedField.create_field('pamRemoteBrowserSettings', required=False) + pam_rbsettings.value = [rbsettings] + record.fields.append(pam_rbsettings) + dirty = True + elif not rbs_fld.value: + rbs_fld.value.append({'connection': {'protocol': 'http'}}) # type: ignore + dirty = True + + if autofill: + af_rec = vault.vault_data.load_record(autofill) + if not af_rec: + raise base.CommandError(f'Record \"{autofill}\" not found.') + if not isinstance(af_rec, vault_record.TypedRecord) or af_rec.version != 3 or af_rec.record_type not in ("login", "pamUser"): + raise base.CommandError(f'Autofill credentials record \"{af_rec.record_uid}\" can not be linked. ' + ' RBI autofill credential records must be of type "login" or "pamUser"') + + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + val1 = rbs_fld.value[0] if isinstance(rbs_fld, vault_record.TypedField) and rbs_fld.value else {} + hcuid = val1.get('connection', {}).get('httpCredentialsUid') or '' if isinstance(val1, dict) else '' + if af_rec.record_uid == hcuid: + logger.debug(f'httpCredentialsUid={af_rec.record_uid} is already set up on record={record_uid}') + elif rbs_fld and rbs_fld.value and isinstance(rbs_fld.value[0], dict): + rbs_fld.value[0]["connection"]["httpCredentialsUid"] = af_rec.record_uid + dirty = True + if hcuid: + logger.debug(f'Updated existing httpCredentialsUid from: {hcuid} to: {af_rec.record_uid}') + else: + raise base.CommandError(f'Failed to set httpCredentialsUid={af_rec.record_uid}') + + if key_events: + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + val1 = rbs_fld.value[0] if isinstance(rbs_fld, vault_record.TypedField) and rbs_fld.value else {} + vcon = val1.get('connection', {}) if isinstance(val1, dict) else {} + rik = vcon.get('recordingIncludeKeys') if isinstance(vcon, dict) else None + if key_events == 'default': + if rik is not None: + rbs_fld.value[0]["connection"].pop('recordingIncludeKeys', None) + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already set to "default" on record={record_uid}') + elif key_events == 'on': + if dag_utils.value_to_boolean(key_events) != dag_utils.value_to_boolean(rik): + rbs_fld.value[0]["connection"]["recordingIncludeKeys"] = True + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already enabled on record={record_uid}') + elif key_events == 'off': + if dag_utils.value_to_boolean(key_events) != dag_utils.value_to_boolean(rik): + rbs_fld.value[0]["connection"]["recordingIncludeKeys"] = False + dirty = True + else: + logger.debug(f'recordingIncludeKeys is already disabled on record={record_uid}') + else: + logger.debug(f'Unexpected value for --key-events {key_events} (ignored)') + + def update_connection_toggle(field_name, setting_value, invert=False): + """Update a connection field using on/off/default pattern. + Args: + field_name: The field name in the connection dict + setting_value: 'on', 'off', or 'default' + invert: If True, 'on' sets False and 'off' sets True (for disableCopy/disablePaste) + """ + nonlocal dirty + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + if rbs_fld and rbs_fld.value and isinstance(rbs_fld.value[0], dict): + connection = rbs_fld.value[0].get('connection', {}) + current_value = connection.get(field_name) + + if setting_value == 'default': + if current_value is not None: + rbs_fld.value[0]['connection'].pop(field_name, None) + dirty = True + logger.debug(f'Removed {field_name} (set to default) on record={record_uid}') + else: + logger.debug(f'{field_name} is already set to default on record={record_uid}') + elif setting_value == 'on': + target_value = False if invert else True + if current_value != target_value: + rbs_fld.value[0]['connection'][field_name] = target_value + dirty = True + logger.debug(f'Set {field_name}={target_value} on record={record_uid}') + else: + logger.debug(f'{field_name} is already set to {target_value} on record={record_uid}') + elif setting_value == 'off': + target_value = True if invert else False + if current_value != target_value: + rbs_fld.value[0]['connection'][field_name] = target_value + dirty = True + logger.debug(f'Set {field_name}={target_value} on record={record_uid}') + else: + logger.debug(f'{field_name} is already set to {target_value} on record={record_uid}') + else: + logger.debug(f'Unexpected value for {field_name}: {setting_value} (ignored)') + + def update_connection_string(field_name, values): + nonlocal dirty + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + if rbs_fld and rbs_fld.value and isinstance(rbs_fld.value[0], dict): + connection = rbs_fld.value[0].get('connection', {}) + new_value = '\n'.join(values) if values else '' + if connection.get(field_name) != new_value: + rbs_fld.value[0]['connection'][field_name] = new_value + dirty = True + logger.debug(f'Set {field_name}={new_value!r} on record={record_uid}') + else: + logger.debug(f'{field_name} is already set to {new_value!r} on record={record_uid}') + + def update_connection_int(field_name, value): + nonlocal dirty + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + if rbs_fld and rbs_fld.value and isinstance(rbs_fld.value[0], dict): + connection = rbs_fld.value[0].get('connection', {}) + if connection.get(field_name) != value: + rbs_fld.value[0]['connection'][field_name] = value + dirty = True + logger.debug(f'Set {field_name}={value} on record={record_uid}') + else: + logger.debug(f'{field_name} is already set to {value} on record={record_uid}') + + # Browser Settings - allowUrlManipulation (on/off/default) + if allow_url_navigation: + update_connection_toggle('allowUrlManipulation', allow_url_navigation) + + # Browser Settings - ignoreInitialSslCert (on/off/default) + if ignore_server_cert: + update_connection_toggle('ignoreInitialSslCert', ignore_server_cert) + + # URL Filtering - allowedUrlPatterns (multi-value, joined with newlines) + if allowed_urls is not None: + update_connection_string('allowedUrlPatterns', allowed_urls) + + # URL Filtering - allowedResourceUrlPatterns (multi-value, joined with newlines) + if allowed_resource_urls is not None: + update_connection_string('allowedResourceUrlPatterns', allowed_resource_urls) + + # Autofill Targets - autofillConfiguration (multi-value, joined with newlines) + if autofill_targets is not None: + update_connection_string('autofillConfiguration', autofill_targets) + + # Clipboard Settings - disableCopy (inverted: on -> disableCopy=False, off -> disableCopy=True) + if allow_copy: + update_connection_toggle('disableCopy', allow_copy, invert=True) + + # Clipboard Settings - disablePaste (inverted: on -> disablePaste=False, off -> disablePaste=True) + if allow_paste: + update_connection_toggle('disablePaste', allow_paste, invert=True) + + # Audio Settings - disableAudio (on -> disableAudio=True, off -> disableAudio=False) + if disable_audio: + update_connection_toggle('disableAudio', disable_audio) + + # Audio Settings - audioChannels (integer) - same location as disableAudio (inside connection) + if audio_channels is not None: + update_connection_int('audioChannels', audio_channels) + + # Audio Settings - audioBps (integer) + if audio_bit_depth is not None: + update_connection_int('audioBps', audio_bit_depth) + + # Audio Settings - audioSampleRate (integer) + if audio_sample_rate is not None: + update_connection_int('audioSampleRate', audio_sample_rate) + + if dirty: + record_management.update_record(vault, record) + vault.sync_down() + + traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') + if not traffic_encryption_key: + raise base.CommandError(f"Unable to add Seed to record {record_uid}. " + f"Please make sure you have edit rights to record {record_uid}") + vault.sync_data = True + + # DAG manipulation options: config, rbi/connections, recording + dirty = False + if not (config_name or rbi or recording): + return + + # resolve PAM Config + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(vault) + existing_config_uid = get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) + existing_config_uid = str(existing_config_uid) if existing_config_uid else '' + + # config parameter is optional and may be (auto)resolved from RBI record + cfg_rec = None + if config_name: + cfg_rec = vault.vault_data.load_record(config_name) + msg = ("not found" if cfg_rec is None else "not the right type" + if not isinstance(cfg_rec, vault_record.TypedRecord) or cfg_rec.version != 6 else "") + if msg: + logger.warning(f'PAM Config record "{config_name}" {msg}') + cfg_rec = None + if not cfg_rec: + logger.debug(f"PAM Config - using config from record {record_uid}") + cfg_rec = vault.vault_data.load_record(existing_config_uid) + msg = ("not found" if cfg_rec is None else "not the right type" + if not isinstance(cfg_rec, vault_record.TypedRecord) or cfg_rec.version != 6 else "") + if msg: + logger.warning(f'PAM Config record "{existing_config_uid}" {msg}') + cfg_rec = None + + config_uid = cfg_rec.record_uid if cfg_rec else None + if not config_uid: + raise base.CommandError(f'PAM Config record not found.') + + tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid) + if tdag is None or not tdag.linking_dag.has_graph: + raise base.CommandError(f"No valid PAM Configuration UID set. " + "This must be set or supplied for connections to work. " + "The ConfigUID can be found by running " + f"'pam config list'") + + if config_uid: + if existing_config_uid and existing_config_uid != config_uid: + old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid) + old_dag.remove_from_dag(record_uid) + logger.debug(f'Updated existing PAM Config UID from: {existing_config_uid} to: {config_uid}') + tdag.link_resource_to_config(record_uid) + + # connections=on needed alongside remoteBrowserIsolation=on in PAM Config for RBI to work + cfg_con_state = tdag.get_resource_setting(config_uid, 'allowedSettings', 'connections') + cfg_rbi_state = tdag.get_resource_setting(config_uid, 'allowedSettings', 'remoteBrowserIsolation') + cfg_rec_state = tdag.get_resource_setting(config_uid, 'allowedSettings', 'sessionRecording') + if cfg_con_state != 'on' or cfg_rbi_state != 'on' or cfg_rec_state != 'on': + if not silent: + tdag.print_tunneling_config(config_uid, None) + command = f"'pam connection edit {config_uid}" + command += ' --connections=on' if cfg_con_state != 'on' else '' + command += ' --remote-browser-isolation=on' if cfg_rbi_state != 'on' else '' + command += ' --connections-recording=on' if cfg_rec_state != 'on' else '' + logger.info(f"Some settings may be denied by PAM Configuration: {config_uid} " + f" [ --connections={cfg_con_state} --remote-browser-isolation={cfg_rbi_state} " + f" --connections-recording={cfg_rec_state} ] " + f"To enable these settings for the configuration run\n" + f"{command}'") + + if not tdag.is_tunneling_config_set_up(record_uid): + tdag.link_resource_to_config(record_uid) + + if not tdag.is_tunneling_config_set_up(record_uid): + logger.info(f"No PAM Configuration UID set. This must be set for connections to work. " + f"This can be done by running " + f"'pam connection edit {record_uid} --config [ConfigUID] --enable-connections' " + f"The ConfigUID can be found by running 'pam config list'") + return + + con_val, rec_val = None, None + rec_con_state = tdag.get_resource_setting(record_uid, 'allowedSettings', 'connections') + rec_rec_state = tdag.get_resource_setting(record_uid, 'allowedSettings', 'sessionRecording') + if (rbi is not None and rbi != rec_con_state) or (recording is not None and recording != rec_rec_state): + con_val = rbi if rbi != rec_con_state else None + rec_val = recording if recording != rec_rec_state else None + dirty = True + + allowed_settings_name = "allowedSettings" + + if dirty: + tdag.set_resource_allowed(resource_uid=record_uid, + allowed_settings_name=allowed_settings_name, + connections=con_val, + session_recording=rec_val) + + vault.sync_data = True diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py new file mode 100644 index 00000000..cbae422c --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py @@ -0,0 +1,1388 @@ +import fnmatch +import json +import os +import argparse +import re + +from keepersdk import crypto, utils +from keepersdk.errors import KeeperApiError +from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG +from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens +from keepersdk.vault import record_management, vault_record, vault_types, vault_utils, record_facades, attachment +from keepersdk.proto import pam_pb2, router_pb2 + +from .. import base +from ... import api, prompt_utils +from ...params import KeeperParams +from ...helpers import gateway_utils, router_utils, report_utils, folder_utils, record_utils +from .pam_config import PAM_CONFIG_RECORD_TYPES + + +logger = api.get_logger() + + +choices = ['on', 'off', 'default'] + +PAM_DEFAULT_SPECIAL_CHAR = '''!@#$%^?();',.=+[]<>{}-_/\\*&:"`~|''' + + +class PAMListRecordRotationCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotation list') + parser.add_argument('--verbose', '-v', required=False, default=False, dest='is_verbose', action='store_true', + help='Verbose output') + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs): + self._validate_vault_and_permissions(context) + vault = context.vault + + is_verbose = kwargs.get('is_verbose') + + rq = pam_pb2.PAMGenericUidsRequest() + schedules_proto = router_utils.router_get_rotation_schedules(vault, rq) + if schedules_proto: + schedules = list(schedules_proto.schedules) + else: + schedules = [] + + enterprise_all_controllers = list(gateway_utils.get_all_gateways(vault)) + enterprise_controllers_connected_resp = router_utils.router_get_connected_gateways(vault) + enterprise_controllers_connected_uids_bytes = \ + [x.controllerUid for x in enterprise_controllers_connected_resp.controllers] + + all_pam_config_records = record_utils.pam_configurations_get_all(vault) + table = [] + + headers = [] + headers.append('Record UID') + headers.append('Record Title') + headers.append('Record Type') + headers.append('Schedule') + + headers.append('Gateway') + if is_verbose: + headers.append('Gateway UID') + + headers.append('PAM Configuration (Type)') + if is_verbose: + headers.append('PAM Configuration UID') + + for s in schedules: + row = [] + + record_uid = utils.base64_url_encode(s.recordUid) + controller_uid = s.controllerUid + controller_details = next( + (ctr for ctr in enterprise_all_controllers if ctr.controllerUid == controller_uid), None) + configuration_uid = s.configurationUid + configuration_uid_str = utils.base64_url_encode(configuration_uid) + pam_configuration = next((pam_config for pam_config in all_pam_config_records if + pam_config.get('record_uid') == configuration_uid_str), None) + + is_controller_online = any( + (poc for poc in enterprise_controllers_connected_uids_bytes if poc == controller_uid)) + + if record_uid in vault.vault_data._records: + rec = vault.vault_data._records[record_uid] + + record_title = rec.info.title + record_type = rec.info.record_type + else: + record_title = '[record inaccessible]' + record_type = '[record inaccessible]' + + if record_type != "pamUser": + continue + + row.append(f'{record_uid}') + row.append(record_title) + row.append(record_type) + + if s.noSchedule is True: + schedule_str = '[Manual Rotation]' + else: + if s.scheduleData: + schedule_arr = s.scheduleData.replace('RotateActionJob|', '').split('.') + if len(schedule_arr) == 4: + schedule_str = f'{schedule_arr[0]} on {schedule_arr[1]} at {schedule_arr[2]} UTC with interval count of {schedule_arr[3]}' + elif len(schedule_arr) == 3: + schedule_str = f'{schedule_arr[0]} at {schedule_arr[1]} UTC with interval count of {schedule_arr[2]}' + else: + schedule_str = s.scheduleData + else: + schedule_str = '[empty]' + + row.append(f'{schedule_str}') + + enterprise_controllers_connected = router_utils.router_get_connected_gateways(vault) + connected_controller = None + if enterprise_controllers_connected and controller_details: + router_controllers = {controller.controllerUid: controller for controller in + list(enterprise_controllers_connected.controllers)} + connected_controller = router_controllers.get(controller_details.controllerUid) + + if controller_details: + row.append(f'{controller_details.controllerName}') + else: + row.append(f'[Does not exist]') + + if not pam_configuration: + if not is_verbose: + row.append(f"[No config found]") + else: + row.append( + f"[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified]") + + else: + pam_data_decrypted = record_utils.pam_decrypt_configuration_data(pam_configuration) + pam_config_name = pam_data_decrypted.get('title') + pam_config_type = pam_data_decrypted.get('type') + row.append(f"{pam_config_name} ({pam_config_type})") + + if is_verbose: + row.append(f'{utils.base64_url_encode(configuration_uid)}') + + table.append(row) + + table.sort(key=lambda x: (x[1])) + + report_utils.dump_report_data(table, headers, fmt='table', filename="", row_number=False, column_width=None) + + print(f"\n----------------------------------------------------------") + print(f"Example to rotate record to which this user has access to:") + print(f"\tpam action rotate -r [RECORD UID]") + + +def validate_cron_field(field, min_val, max_val): + # Accept *, single number, range, step, list, and L suffix for last day/week + pattern = r'^(\*|\d+L?|L[W]?|\d+-\d+|\*/\d+|\d+(,\d+)*|\d+-\d+/\d+)$' + if not re.match(pattern, field): + return False + + def is_valid_number(n): + # Strip L and W suffix if present (for last day/week expressions) + n_stripped = n.rstrip('LW') + return n_stripped and n_stripped.isdigit() and min_val <= int(n_stripped) <= max_val + + parts = re.split(r'[,\-/]', field) + return all(part == '*' or part in ('L', 'LW') or is_valid_number(part) for part in parts if part != '*') + +def validate_cron_expression(expr, for_rotation=False): + parts = expr.strip().split() + + if for_rotation is True: + if len(parts) != 6: + return False, f"CRON: Rotation schedules require all 6 parts incl. seconds - ex. Daily at 04:00:00 cron: 0 0 4 * * ? got {len(parts)} parts" + if not (parts[3] == '?' or parts[5] == "?"): + logger.warning( + "CRON: Rotation schedule CRON format - must use ? character in one of these fields: day-of-week, day-of-month") + parts[3] = '*' if parts[3] == '?' else parts[3] + parts[5] = '*' if parts[5] == '?' else parts[5] + logger.debug( + "WARNING! Validating CRON expression for rotation - if you get 500 type errors make sure to validate your CRON using web vault UI") + + if len(parts) not in [5, 6]: + return False, f"CRON: Expected 5 or 6 fields, got {len(parts)}" + + if len(parts) == 6: + seconds, minute, hour, dom, month, dow = parts + if not validate_cron_field(seconds, 0, 59): + return False, "CRON: Invalid seconds field" + else: + minute, hour, dom, month, dow = parts + + validators = [ + (minute, 0, 59, "minute"), + (hour, 0, 23, "hour"), + (dom, 1, 31, "day of month"), + (month, 1, 12, "month"), + (dow, 0, 7, "day of week") + ] + + for field, min_val, max_val, name in validators: + if not validate_cron_field(field, min_val, max_val): + return False, f"CRON: Invalid {name} field" + + return True, "Valid cron expression" + + +def parse_schedule_data(kwargs): + schedule_json_data = kwargs.get('schedule_json_data') + schedule_cron_data = kwargs.get('schedule_cron_data') + schedule_on_demand = kwargs.get('on_demand') is True + schedule_data = None + if isinstance(schedule_json_data, list): + schedule_data = [json.loads(x) for x in schedule_json_data] + elif isinstance(schedule_cron_data, list): + if schedule_cron_data and isinstance(schedule_cron_data[0], str): + valid, err = validate_cron_expression(schedule_cron_data[0], for_rotation=True) + if valid: + schedule_data = [{"type": "CRON", "cron": schedule_cron_data[0], "tz": "Etc/UTC"}] + else: + logger.error(f'Invalid CRON "{schedule_cron_data[0]}" Error: {err}') + elif schedule_on_demand is True: + schedule_data = [] + return schedule_data + + +class PAMCreateRecordRotationCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotation edit') + PAMCreateRecordRotationCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + record_group = parser.add_mutually_exclusive_group(required=True) + record_group.add_argument('--record', '-r', dest='record_name', action='store', + help='Record UID, name, or pattern to be rotated manually or via schedule') + record_group.add_argument('--folder', '-fd', dest='folder_name', action='store', + help='Used for bulk rotation setup. The folder UID or name that holds records to be ' + 'configured') + parser.add_argument('--force', '-f', dest='force', action='store_true', help='Do not ask for confirmation') + parser.add_argument('--config', '-c', required=False, dest='config', action='store', + help='UID or path of the configuration record.') + parser.add_argument('--iam-aad-config', '-iac', dest='iam_aad_config_uid', action='store', + help='UID of a PAM Configuration. Used for an IAM or Azure AD user in place of --resource.') + parser.add_argument('--rotation-profile', '-rp', dest='rotation_profile', action='store', + choices=['general', 'iam_user', 'scripts_only'], + help='Rotation profile type: general (resource-based), iam_user (IAM/Azure user), ' + 'scripts_only (run PAM scripts only)') + parser.add_argument('--resource', '-rs', dest='resource', action='store', + help='UID or path of the resource record.') + schedule_group = parser.add_mutually_exclusive_group() + schedule_group.add_argument('--schedulejson', '-sj', required=False, dest='schedule_json_data', + action='append', help='JSON of the scheduler. Example: -sj \'{"type": "WEEKLY", ' + '"utcTime": "15:44", "weekday": "SUNDAY", "intervalCount": 1}\'') + schedule_group.add_argument('--schedulecron', '-sc', required=False, dest='schedule_cron_data', + action='append', help='Cron tab string of the scheduler. Example: to run job daily at ' + '5:56PM UTC enter following cron -sc "56 17 * * *"') + schedule_group.add_argument('--on-demand', '-od', required=False, dest='on_demand', + action='store_true', help='Schedule On Demand') + schedule_group.add_argument('--schedule-config', '-sf', required=False, dest='schedule_config', + action='store_true', help='Schedule from Configuration') + parser.add_argument('--schedule-only', '-so', dest='schedule_only', action='store_true', + help='Only update the rotation schedule without changing other settings') + parser.add_argument('--complexity', '-x', required=False, dest='pwd_complexity', action='store', + help='Password complexity: length, upper, lower, digits, symbols. Ex. 32,5,5,5,5[,SPECIAL CHARS]') + parser.add_argument('--admin-user', '-a', required=False, dest='admin', action='store', + help='UID or path for the PAMUser record to configure the admin credential on the PAM Resource as the Admin when rotating') + state_group = parser.add_mutually_exclusive_group() + state_group.add_argument('--enable', '-e', dest='enable', action='store_true', help='Enable rotation') + state_group.add_argument('--disable', '-d', dest='disable', action='store_true', help='Disable rotation') + + def execute(self, context: KeeperParams, **kwargs): + """Configure rotation settings for one or multiple PAM records. + + The command accepts either ``--record`` or ``--folder`` to target + records. It validates schedule options, password complexity and + resource linkage and then submits rotation requests to the Keeper + PAM router service. + """ + + vault = context.vault + context.refresh_record_rotations() + + def config_resource(_dag, target_record, target_config_uid, silent=None): + if not _dag.linking_dag.has_graph: + if target_config_uid: + _dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, target_config_uid) + _dag.edit_tunneling_config(rotation=True) + else: + raise base.CommandError(f'Resource "{target_record.record_uid}" is not associated ' + f'with any configuration. ' + f'pam rotation edit -rs {target_record.record_uid} ' + f'--config CONFIG') + resource_dag = None + if not _dag.resource_belongs_to_config(target_record.record_uid): + resource_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, + target_record.record_uid) + _dag.link_resource_to_config(target_record.record_uid) + + admin = kwargs.get('admin') + adm_rec = vault.vault_data.load_record(admin) + + if adm_rec and isinstance(adm_rec, vault_record.TypedRecord): + admin = adm_rec.record_uid + + if admin and target_record.record_type != 'pamRemoteBrowser': + _dag.link_user_to_resource(admin, target_record.record_uid, is_admin=True) + + _rotation_enabled = True if kwargs.get('enable') else False if kwargs.get('disable') else None + + if _rotation_enabled is not None: + _dag.set_resource_allowed(target_record.record_uid, rotation=_rotation_enabled, + allowed_settings_name="rotation") + + if resource_dag is not None and resource_dag.linking_dag.has_graph: + resource_dag.remove_from_dag(target_record.record_uid) + + if not silent: + _dag.print_tunneling_config(target_record.record_uid, config_uid=target_config_uid) + + def config_iam_aad_user(_dag, target_record, target_iam_aad_config_uid): + current_record_rotation = context.get_record_rotation(target_record.record_uid) + schedule_only = kwargs.get('schedule_only') + + if schedule_only: + if kwargs.get('folder_name') and ( + not current_record_rotation or current_record_rotation.disabled): + skipped_records.append([target_record.record_uid, target_record.title, + 'Rotation not enabled', 'Skipped']) + return + if not current_record_rotation: + skipped_records.append([target_record.record_uid, target_record.title, + 'No rotation info', 'Skipped']) + return + + record_config_uid = current_record_rotation.configuration_uid + record_pam_config = pam_configurations.get(record_config_uid, pam_config) + record_schedule_data = schedule_data + if record_schedule_data is None: + try: + cs = current_record_rotation.schedule + record_schedule_data = json.loads(cs) if cs else [] + except: + record_schedule_data = [] + pwd_complexity_rule_list_encrypted = current_record_rotation.pwd_complexity or b'' + record_resource_uid = current_record_rotation.resource_uid + + if record_resource_uid == record_config_uid: + record_resource_uid = None + disabled = current_record_rotation.disabled + + schedule = 'On-Demand' + if isinstance(record_schedule_data, list) and len(record_schedule_data) > 0: + if isinstance(record_schedule_data[0], dict): + schedule = record_schedule_data[0].get('type') + complexity = '' + if pwd_complexity_rule_list_encrypted: + try: + decrypted_complexity = crypto.decrypt_aes_v2(pwd_complexity_rule_list_encrypted, + target_record.record_key) + c = json.loads(decrypted_complexity.decode()) + complexity = f"{c.get('length', 0)},{c.get('caps', 0)},{c.get('lowercase', 0)},{c.get('digits', 0)},{c.get('special', 0)},{c.get('specialChars', PAM_DEFAULT_SPECIAL_CHAR)}" + except Exception: + pass + + valid_records.append([ + target_record.record_uid, target_record.title, not disabled, record_config_uid, + record_resource_uid, schedule, complexity]) + + noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE' + if target_record and not noop_rotation: + noop_field = target_record.get_typed_field('text', 'NOOP') + if (noop_field and noop_field.value and + isinstance(noop_field.value, list) and + str(noop_field.value[0]).upper() == 'TRUE'): + noop_rotation = True + + rq = router_pb2.RouterRecordRotationRequest() + rq.revision = current_record_rotation.revision + rq.recordUid = utils.base64_url_decode(target_record.record_uid) + rq.configurationUid = utils.base64_url_decode(record_config_uid) + rq.resourceUid = utils.base64_url_decode(record_resource_uid) if record_resource_uid else b'' + rq.schedule = json.dumps(record_schedule_data) if record_schedule_data else '' + rq.pwdComplexity = pwd_complexity_rule_list_encrypted + rq.disabled = disabled + if noop_rotation: + rq.noop = True + rq.resourceUid = b'' + r_requests.append(rq) + return + + if _dag and not _dag.linking_dag.has_graph: + _dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, target_iam_aad_config_uid) + if not _dag or not _dag.linking_dag.has_graph: + _dag.edit_tunneling_config(rotation=True) + old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, target_record.record_uid) + if old_dag.linking_dag.has_graph and old_dag.record.record_uid != target_iam_aad_config_uid: + old_dag.remove_from_dag(target_record.record_uid) + + if not _dag.user_belongs_to_config(target_record.record_uid): + old_resource_uid = _dag.get_resource_uid(target_record.record_uid) + if old_resource_uid is not None: + logger.info( + f'User "{target_record.record_uid}" is associated with another resource: ' + f'{old_resource_uid}. ' + f'Now moving it to {target_iam_aad_config_uid} and it will no longer be rotated on {old_resource_uid}.') + if old_resource_uid == _dag.record.record_uid: + _dag.unlink_user_from_resource(target_record.record_uid, old_resource_uid) + _dag.link_user_to_resource(target_record.record_uid, old_resource_uid, belongs_to=False) + _dag.link_user_to_config(target_record.record_uid) + + record_config_uid = _dag.record.record_uid + record_pam_config = pam_config + if not record_config_uid: + if current_record_rotation: + record_config_uid = current_record_rotation.configuration_uid + pc = vault.vault_data.load_record(record_config_uid) + if pc is None: + skipped_records.append( + [target_record.record_uid, target_record.title, 'PAM Configuration was deleted', + 'Specify a configuration UID parameter [--config]']) + return + if not isinstance(pc, vault_record.TypedRecord) or pc.version != 6: + skipped_records.append( + [target_record.record_uid, target_record.title, 'PAM Configuration is invalid', + 'Specify a configuration UID parameter [--config]']) + return + record_pam_config = pc + else: + skipped_records.append( + [target_record.record_uid, target_record.title, 'No current PAM Configuration', + 'Specify a configuration UID parameter [--config]']) + return + + record_schedule_data = schedule_data + if record_schedule_data is None: + if current_record_rotation and not schedule_config: + try: + current_schedule = current_record_rotation.schedule + if current_schedule: + record_schedule_data = json.loads(current_schedule) + except: + pass + else: + schedule_field = record_pam_config.get_typed_field('schedule', 'defaultRotationSchedule') + if schedule_field and isinstance(schedule_field.value, list) and len(schedule_field.value) > 0: + if isinstance(schedule_field.value[0], dict): + record_schedule_data = [schedule_field.value[0]] + + if pwd_complexity_rule_list is None: + if current_record_rotation: + pwd_complexity_rule_list_encrypted = current_record_rotation.pwd_complexity or b'' + else: + pwd_complexity_rule_list_encrypted = b'' + else: + if len(pwd_complexity_rule_list) > 0: + pwd_complexity_rule_list_encrypted = router_utils.encrypt_pwd_complexity(pwd_complexity_rule_list, + target_record.record_key) + else: + pwd_complexity_rule_list_encrypted = b'' + + record_resource_uid = target_iam_aad_config_uid + if record_resource_uid is None: + if current_record_rotation: + record_resource_uid = current_record_rotation.resource_uid + if record_resource_uid is None: + resource_field = record_pam_config.get_typed_field('pamResources') + if resource_field and isinstance(resource_field.value, list) and len(resource_field.value) > 0: + resources = resource_field.value[0] + if isinstance(resources, dict): + resource_uids = resources.get('resourceRef') + if isinstance(resource_uids, list) and len(resource_uids) > 0: + if len(resource_uids) == 1: + record_resource_uid = resource_uids[0] + else: + skipped_records.append([target_record.record_uid, target_record.title, + f'PAM Configuration: {len(resource_uids)} admin resources', + 'Specify both configuration UID and resource UID [--config, --resource]']) + return + + disabled = False + + if kwargs.get('enable'): + _dag.set_resource_allowed(target_iam_aad_config_uid, rotation=True, + is_config=bool(target_iam_aad_config_uid)) + elif kwargs.get('disable'): + _dag.set_resource_allowed(target_iam_aad_config_uid, rotation=False, + is_config=bool(target_iam_aad_config_uid)) + disabled = True + + schedule = 'On-Demand' + if isinstance(record_schedule_data, list) and len(record_schedule_data) > 0: + if isinstance(record_schedule_data[0], dict): + schedule = record_schedule_data[0].get('type') + complexity = '' + if pwd_complexity_rule_list_encrypted: + try: + decrypted_complexity = crypto.decrypt_aes_v2(pwd_complexity_rule_list_encrypted, + target_record.record_key) + c = json.loads(decrypted_complexity.decode()) + complexity = f"{c.get('length', 0)}," \ + f"{c.get('caps', 0)}," \ + f"{c.get('lowercase', 0)}," \ + f"{c.get('digits', 0)}," \ + f"{c.get('special', 0)}," \ + f"{c.get('specialChars', PAM_DEFAULT_SPECIAL_CHAR)}" + except: + pass + valid_records.append( + [target_record.record_uid, target_record.title, not disabled, record_config_uid, record_resource_uid, + schedule, + complexity]) + + + rq = router_pb2.RouterRecordRotationRequest() + if current_record_rotation: + rq.revision = current_record_rotation.revision + rq.recordUid = utils.base64_url_decode(target_record.record_uid) + rq.configurationUid = utils.base64_url_decode(record_config_uid) + rq.resourceUid = b'' + rq.noop = False + rq.schedule = json.dumps(record_schedule_data) if record_schedule_data else '' + rq.pwdComplexity = pwd_complexity_rule_list_encrypted + rq.disabled = disabled + r_requests.append(rq) + + def config_user(_dag, target_record, target_resource_uid, target_config_uid=None, silent=None): + current_record_rotation = context.get_record_rotation(target_record.record_uid) + schedule_only = kwargs.get('schedule_only') + + if schedule_only: + if kwargs.get('folder_name') and ( + not current_record_rotation or current_record_rotation.disabled): + skipped_records.append([target_record.record_uid, target_record.title, + 'Rotation not enabled', 'Skipped']) + return + if not current_record_rotation: + skipped_records.append([target_record.record_uid, target_record.title, + 'No rotation info', 'Skipped']) + return + + record_config_uid = current_record_rotation.configuration_uid + record_pam_config = pam_configurations.get(record_config_uid, pam_config) + record_schedule_data = schedule_data + if record_schedule_data is None: + try: + cs = current_record_rotation.schedule + record_schedule_data = json.loads(cs) if cs else [] + except: + record_schedule_data = [] + pwd_complexity_rule_list_encrypted = current_record_rotation.pwd_complexity or b'' + record_resource_uid = current_record_rotation.resource_uid + + if record_resource_uid == record_config_uid: + record_resource_uid = None + disabled = current_record_rotation.disabled + + schedule = 'On-Demand' + if isinstance(record_schedule_data, list) and len(record_schedule_data) > 0: + if isinstance(record_schedule_data[0], dict): + schedule = record_schedule_data[0].get('type') + complexity = '' + if pwd_complexity_rule_list_encrypted: + try: + decrypted_complexity = crypto.decrypt_aes_v2(pwd_complexity_rule_list_encrypted, + target_record.record_key) + c = json.loads(decrypted_complexity.decode()) + complexity = f"{c.get('length', 0)},{c.get('caps', 0)},{c.get('lowercase', 0)},{c.get('digits', 0)},{c.get('special', 0)},{c.get('specialChars', PAM_DEFAULT_SPECIAL_CHAR)}" + except Exception: + pass + + valid_records.append([ + target_record.record_uid, target_record.title, not disabled, record_config_uid, + record_resource_uid, schedule, complexity]) + + noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE' + if target_record and not noop_rotation: + noop_field = target_record.get_typed_field('text', 'NOOP') + if (noop_field and noop_field.value and + isinstance(noop_field.value, list) and + str(noop_field.value[0]).upper() == 'TRUE'): + noop_rotation = True + + rq = router_pb2.RouterRecordRotationRequest() + rq.revision = current_record_rotation.revision + rq.recordUid = utils.base64_url_decode(target_record.record_uid) + rq.configurationUid = utils.base64_url_decode(record_config_uid) + rq.resourceUid = utils.base64_url_decode(record_resource_uid) if record_resource_uid else b'' + rq.schedule = json.dumps(record_schedule_data) if record_schedule_data else '' + rq.pwdComplexity = pwd_complexity_rule_list_encrypted + rq.disabled = disabled + if noop_rotation: + rq.noop = True + rq.resourceUid = b'' + r_requests.append(rq) + return + + noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE' + if target_record and not noop_rotation: + noop_field = target_record.get_typed_field('text', 'NOOP') + if (noop_field and noop_field.value and + isinstance(noop_field.value, list) and + str(noop_field.value[0]).upper() == 'TRUE'): + noop_rotation = True + if _dag and _dag.linking_dag: + admin_record_uids = _dag.get_all_admins() + if folder_name and target_record.record_uid in admin_record_uids: + skipped_records.append([target_record.record_uid, target_record.title, 'Admin Credential', + 'This record is used as Admin credentials on a PAM Configuration. Skipped']) + return + + if isinstance(target_resource_uid, str) and len(target_resource_uid) > 0: + _dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, target_resource_uid) + if not _dag or not _dag.linking_dag.has_graph: + if target_config_uid and target_resource_uid: + config_resource(_dag, target_record, target_config_uid, silent=silent) + if not _dag or not _dag.linking_dag.has_graph: + raise base.CommandError(f'Resource "{target_resource_uid}" is not associated ' + f'with any configuration. ' + f'pam rotation edit -rs {target_resource_uid} ' + f'--config CONFIG') + + if not _dag.check_if_resource_has_admin(target_resource_uid): + raise base.CommandError(f'PAM Resource "{target_resource_uid}'" does not have " + "admin credentials. Please link an admin credential to this resource. " + f"pam rotation edit -rs {target_resource_uid} " + f"--admin-user ADMIN") + current_record_rotation = context.get_record_rotation(target_record.record_uid) + + if not _dag or not _dag.linking_dag.has_graph: + _dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, target_resource_uid) + if not _dag.linking_dag.has_graph: + raise base.CommandError(f'Resource "{target_resource_uid}" is not associated ' + f'with any configuration. ' + f'pam rotation edit -rs {target_resource_uid} ' + f'--config CONFIG') + + if noop_rotation: + target_resource_uid = target_record.record_uid + record_resource_uid = None + else: + if not target_resource_uid: + resource_uids = _dag.get_all_owners(target_record.record_uid) + if len(resource_uids) > 1: + if folder_name: + skipped_records.append([ + target_record.record_uid, + target_record.title, + 'Multiple Resources', + f'Record is associated with {len(resource_uids)} resources. Use --record with --resource to configure individually.' + ]) + return + else: + raise base.CommandError(f'Record "{target_record.record_uid}" is ' + f'associated with multiple resources so you must supply ' + f'"--resource/-rs RESOURCE".') + elif len(resource_uids) == 0: + raise base.CommandError( + f'Record "{target_record.record_uid}" is not associated with' + f' any resource. Please use "pam rotation user ' + f'{target_record.record_uid} --resource RESOURCE" to associate ' + f'it.') + target_resource_uid = resource_uids[0] + + if not _dag.resource_belongs_to_config(target_resource_uid): + if target_resource_uid != _dag.record.record_uid: + raise base.CommandError( + f'Resource "{target_resource_uid}" is not associated with the ' + f'configuration of the user "{target_record.record_uid}". To associated the resources ' + f'to this config run "pam rotation resource {target_resource_uid} ' + f'--config {_dag.record.record_uid}"') + + if not _dag.user_belongs_to_resource(target_record.record_uid, target_resource_uid): + old_resource_uid = _dag.get_resource_uid(target_record.record_uid) + if old_resource_uid is not None and old_resource_uid != target_resource_uid: + logger.info( + f'User "{target_record.record_uid}" is associated with another resource: ' + f'{old_resource_uid}. ' + f'Now moving it to {target_resource_uid} and it will no longer be rotated on {old_resource_uid}.' + ) + _dag.link_user_to_resource(target_record.record_uid, old_resource_uid, belongs_to=False) + _dag.link_user_to_resource(target_record.record_uid, target_resource_uid, belongs_to=True) + + record_config_uid = _dag.record.record_uid + record_pam_config = pam_config + if not record_config_uid: + if current_record_rotation: + record_config_uid = current_record_rotation.configuration_uid + pc = vault.vault_data.load_record(record_config_uid) + if pc is None: + skipped_records.append( + [target_record.record_uid, target_record.title, 'PAM Configuration was deleted', + 'Specify a configuration UID parameter [--config]']) + return + if not isinstance(pc, vault_record.TypedRecord) or pc.version != 6: + skipped_records.append( + [target_record.record_uid, target_record.title, 'PAM Configuration is invalid', + 'Specify a configuration UID parameter [--config]']) + return + record_pam_config = pc + else: + skipped_records.append( + [target_record.record_uid, target_record.title, 'No current PAM Configuration', + 'Specify a configuration UID parameter [--config]']) + return + + record_schedule_data = schedule_data + if record_schedule_data is None: + if current_record_rotation: + try: + current_schedule = current_record_rotation.schedule + if current_schedule: + record_schedule_data = json.loads(current_schedule) + except: + pass + elif record_pam_config: + schedule_field = record_pam_config.get_typed_field('schedule', 'defaultRotationSchedule') + if schedule_field and isinstance(schedule_field.value, list) and len(schedule_field.value) > 0: + if isinstance(schedule_field.value[0], dict): + record_schedule_data = [schedule_field.value[0]] + else: + record_schedule_data = [] + + if pwd_complexity_rule_list is None: + if current_record_rotation: + pwd_complexity_rule_list_encrypted = current_record_rotation.pwd_complexity or b'' + else: + pwd_complexity_rule_list_encrypted = b'' + else: + if len(pwd_complexity_rule_list) > 0: + pwd_complexity_rule_list_encrypted = router_utils.encrypt_pwd_complexity(pwd_complexity_rule_list, + target_record.record_key) + else: + pwd_complexity_rule_list_encrypted = b'' + + if not noop_rotation: + record_resource_uid = target_resource_uid + + if record_resource_uid == _dag.record.record_uid: + record_resource_uid = None + if record_resource_uid is None: + if current_record_rotation: + record_resource_uid = current_record_rotation.resource_uid + if record_resource_uid == record_config_uid: + record_resource_uid = None + if record_resource_uid is None: + resource_field = record_pam_config.get_typed_field('pamResources') + if resource_field and isinstance(resource_field.value, list) and len(resource_field.value) > 0: + resources = resource_field.value[0] + if isinstance(resources, dict): + resource_uids = resources.get('resourceRef') + if isinstance(resource_uids, list) and len(resource_uids) > 0: + if len(resource_uids) == 1: + record_resource_uid = resource_uids[0] + else: + skipped_records.append([target_record.record_uid, target_record.title, + f'PAM Configuration: {len(resource_uids)} admin resources', + 'Specify both configuration UID and resource UID [--config, --resource]']) + return + + disabled = False + + if kwargs.get('enable'): + _dag.set_resource_allowed(target_resource_uid, rotation=True) + elif kwargs.get('disable'): + _dag.set_resource_allowed(target_resource_uid, rotation=False) + disabled = True + + schedule = 'On-Demand' + if isinstance(record_schedule_data, list) and len(record_schedule_data) > 0: + if isinstance(record_schedule_data[0], dict): + schedule = record_schedule_data[0].get('type') + complexity = '' + if pwd_complexity_rule_list_encrypted: + try: + decrypted_complexity = crypto.decrypt_aes_v2(pwd_complexity_rule_list_encrypted, + target_record.record_key) + c = json.loads(decrypted_complexity.decode()) + complexity = f"{c.get('length', 0)}," \ + f"{c.get('caps', 0)}," \ + f"{c.get('lowercase', 0)}," \ + f"{c.get('digits', 0)}," \ + f"{c.get('special', 0)}," \ + f"{c.get('specialChars', PAM_DEFAULT_SPECIAL_CHAR)}" + except: + pass + valid_records.append( + [target_record.record_uid, target_record.title, not disabled, record_config_uid, record_resource_uid, + schedule, + complexity]) + + rq = router_pb2.RouterRecordRotationRequest() + if current_record_rotation: + rq.revision = current_record_rotation.revision + rq.recordUid = utils.base64_url_decode(target_record.record_uid) + rq.configurationUid = utils.base64_url_decode(record_config_uid) + rq.resourceUid = utils.base64_url_decode(record_resource_uid) if record_resource_uid else b'' + rq.schedule = json.dumps(record_schedule_data) if record_schedule_data else '' + rq.pwdComplexity = pwd_complexity_rule_list_encrypted + rq.disabled = disabled + if noop_rotation: + rq.noop = True + rq.resourceUid = b'' + r_requests.append(rq) + + record_uids = set() + + folder_uids = set() + record_pattern = '' + record_name = kwargs.get('record_name') + + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(vault) + if record_name: + if record_name in vault.vault_data._records: + record_uids.add(record_name) + else: + rs = folder_utils.try_resolve_path(context, record_name) + if rs is not None: + folder, record_title = rs + if record_title: + record_pattern = record_title + if isinstance(folder, vault_types.Folder): + folder_uids.add(folder.folder_uid) + elif isinstance(folder, list): + for f in folder: + if isinstance(f, vault_types.Folder): + folder_uids.add(f.folder_uid) + else: + logger.warning('Record \"%s\" not found. Skipping.', record_name) + + folder_name = kwargs.get('folder_name') + if folder_name: + if folder_name in vault.vault_data._folders: + folder_uids.add(folder_name) + else: + rs = folder_utils.try_resolve_path(context, folder_name) + if rs is not None: + folder, record_title = rs + if not record_title: + + def add_folders(folder: vault_types.Folder): + folder_uids.add(folder.folder_uid or '') + + if isinstance(folder, vault_types.Folder): + folder = [folder] + if isinstance(folder, list): + for f in folder: + vault_utils.traverse_folder_tree(vault.vault_data, f, add_folders) + else: + logger.warning('Folder \"%s\" not found. Skipping.', folder_name) + + if record_name and folder_name: + raise base.CommandError('Cannot use both --record and --folder at the same time.') + + if folder_uids: + regex = re.compile(fnmatch.translate(record_pattern), re.IGNORECASE).match if record_pattern else None + for folder_uid in folder_uids: + folder_records = vault.vault_data.get_folder(folder_uid).records + if not folder_records: + continue + if record_pattern and record_pattern in folder_records: + record_uids.add(record_pattern) + else: + for record_uid in folder_records: + if record_uid not in record_uids: + r = vault.vault_data.load_record(record_uid) + if r: + if regex and not regex(r.title): + continue + record_uids.add(record_uid) + + pam_records = [] + valid_record_types = ['pamDatabase', 'pamDirectory', 'pamMachine', 'pamUser', 'pamRemoteBrowser'] + for record_uid in record_uids: + record = vault.vault_data.load_record(record_uid) + if record and isinstance(record, vault_record.TypedRecord) and record.record_type in valid_record_types: + pam_records.append(record) + + if len(pam_records) == 0: + rts = ', '.join(valid_record_types) + raise base.CommandError(f'No PAM record is found. Valid PAM record types: {rts}') + else: + if not kwargs.get('silent'): + logger.info('Selected %d PAM record(s) for rotation', len(pam_records)) + + pam_configurations = {} + for x in vault.vault_data.find_records( + criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6): + loaded = vault.vault_data.load_record(x.record_uid) + if loaded and isinstance(loaded, vault_record.TypedRecord): + pam_configurations[x.record_uid] = loaded + + config_uid = kwargs.get('config') + cfg_rec = vault.vault_data.load_record(kwargs.get('config', None)) + if cfg_rec and cfg_rec.version == 6 and cfg_rec.record_uid in pam_configurations: + config_uid = cfg_rec.record_uid + + pam_config = None + if config_uid: + if config_uid in pam_configurations: + pam_config = pam_configurations[config_uid] + else: + raise base.CommandError(f'Record uid {config_uid} is not a PAM Configuration record.') + + schedule_config = kwargs.get('schedule_config') is True + schedule_data = parse_schedule_data(kwargs) + + pwd_complexity = kwargs.get("pwd_complexity") + pwd_complexity_rule_list = None + if pwd_complexity is not None: + if pwd_complexity: + pwd_complexity_list = [s.strip() for s in pwd_complexity.split(',', maxsplit=5)] + if len(pwd_complexity_list) < 5 or not all(n.isnumeric() for n in pwd_complexity_list[:5]): + raise base.CommandError('Invalid rules to generate password. ''Format is "length, ' + 'upper, lower, digits, symbols". Ex: 32,5,5,5,5[,SPECIAL CHARS]') + + special_chars = PAM_DEFAULT_SPECIAL_CHAR + if len(pwd_complexity_list) == 6: + special_chars = "" + for char in PAM_DEFAULT_SPECIAL_CHAR: + if char in pwd_complexity_list[5]: + special_chars += char + + pwd_complexity_rule_list = { + 'length': int(pwd_complexity_list[0]), + 'caps': int(pwd_complexity_list[1]), + 'lowercase': int(pwd_complexity_list[2]), + 'digits': int(pwd_complexity_list[3]), + 'special': int(pwd_complexity_list[4]), + 'specialChars': special_chars + } + else: + pwd_complexity_rule_list = {} + + resource_uid = kwargs.get('resource') + res_rec = vault.vault_data.load_record(kwargs.get('resource', None)) + if res_rec and isinstance(res_rec, vault_record.TypedRecord): + resource_uid = res_rec.record_uid + + skipped_header = ['record_uid', 'record_title', 'problem', 'description'] + skipped_records = [] + valid_header = ['record_uid', 'record_title', 'enabled', 'configuration_uid', 'resource_uid', 'schedule', + 'complexity'] + valid_records = [] + + r_requests = [] + + for _record in pam_records: + tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, _record.record_uid) + if _record.record_type in ['pamMachine', 'pamDatabase', 'pamDirectory', 'pamRemoteBrowser']: + config_resource(tmp_dag, _record, config_uid, silent=kwargs.get('silent')) + elif _record.record_type == 'pamUser': + iam_aad_config_uid = kwargs.get('iam_aad_config_uid') + rotation_profile = kwargs.get('rotation_profile') + + if iam_aad_config_uid and iam_aad_config_uid not in pam_configurations: + raise base.CommandError(f'Record uid {iam_aad_config_uid} is not a PAM Configuration record.') + + if resource_uid and iam_aad_config_uid: + raise base.CommandError('Cannot use both --resource and --iam-aad-config_uid at once.' + ' --resource is used to configure users found on a resource.' + ' --iam-aad-config-uid is used to configure AWS IAM or Azure AD users') + + if rotation_profile: + if rotation_profile == 'iam_user': + effective_config_uid = iam_aad_config_uid or config_uid + if not effective_config_uid: + current_rotation = context.get_record_rotation(_record.record_uid) + if current_rotation: + effective_config_uid = current_rotation.configuration_uid + if not effective_config_uid: + raise base.CommandError('IAM user rotation requires a PAM Configuration. ' + 'Use --config or --iam-aad-config to specify one.') + if effective_config_uid not in pam_configurations: + raise base.CommandError( + f'Record uid {effective_config_uid} is not a PAM Configuration record.') + config_iam_aad_user(tmp_dag, _record, effective_config_uid) + + elif rotation_profile == 'scripts_only': + kwargs['noop'] = 'TRUE' + config_user(tmp_dag, _record, resource_uid, config_uid, silent=kwargs.get('silent')) + + elif rotation_profile == 'general': + if not resource_uid: + raise base.CommandError('General rotation profile requires --resource to be specified.') + config_user(tmp_dag, _record, resource_uid, config_uid, silent=kwargs.get('silent')) + + elif iam_aad_config_uid: + config_iam_aad_user(tmp_dag, _record, iam_aad_config_uid) + + else: + config_user(tmp_dag, _record, resource_uid, config_uid, silent=kwargs.get('silent')) + + force = kwargs.get('force') is True + + if len(skipped_records) > 0: + skipped_header = [report_utils.field_to_title(x) for x in skipped_header] + report_utils.dump_report_data(skipped_records, skipped_header, title='The following record(s) were skipped') + + if len(r_requests) > 0 and not force: + answer = prompt_utils.user_choice('\nDo you want to cancel password rotation?', 'Yn', 'Y') + if answer.lower().startswith('y'): + return + + if len(r_requests) > 0: + valid_header = [report_utils.field_to_title(x) for x in valid_header] + if not kwargs.get('silent'): + report_utils.dump_report_data(valid_records, valid_header, title='The following record(s) will be updated') + if not force: + answer = prompt_utils.user_choice('\nDo you want to update password rotation?', 'Yn', 'Y') + if answer.lower().startswith('n'): + return + + for rq in r_requests: + record_uid = utils.base64_url_encode(rq.recordUid) + try: + router_utils.router_set_record_rotation_information(vault, rq, transmission_key, encrypted_transmission_key, + encrypted_session_token) + except KeeperApiError as kae: + logger.warning('Record "%s": Set rotation error "%s": %s', + record_uid, kae.result_code, kae.message) + vault.sync_data = True + context.refresh_record_rotations() + + +class PAMRouterGetRotationInfo(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='dr-router-get-rotation-info-parser') + parser.add_argument('--record-uid', '-r', required=True, dest='record_uid', action='store', + help='Record UID to rotate') + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs): + + record_uid = kwargs.get('record_uid') + record_uid_bytes = utils.base64_url_decode(record_uid) + + vault = context.vault + + rri = record_utils.record_rotation_get(vault, record_uid_bytes) + rri_status_name = router_pb2.RouterRotationStatus.Name(rri.status) + if rri_status_name == 'RRS_ONLINE': + + logger.info(f'Rotation Status: Ready to rotate ({rri_status_name})') + configuration_uid = utils.base64_url_encode(rri.configurationUid) + logger.info(f'PAM Config UID: {configuration_uid}') + logger.info(f'Node ID: {rri.nodeId}') + + logger.info( + f"Gateway Name where the rotation will be performed: {(rri.controllerName if rri.controllerName else '-')}") + logger.info( + f"Gateway Uid: {(utils.base64_url_encode(rri.controllerUid) if rri.controllerUid else '-')}") + + def is_resource_ok(resource_id, vault, configuration_uid): + if resource_id not in vault.vault_data._records: + return False + + configuration = vault.vault_data.load_record(configuration_uid) + if not isinstance(configuration, vault_record.TypedRecord): + return False + + field = configuration.get_typed_field('pamResources') + if not (field and isinstance(field.value, list) and len(field.value) == 1): + return False + + rv = field.value[0] + if not isinstance(rv, dict): + return False + + resources = rv.get('resourceRef') + return isinstance(resources, list) and resource_id in resources + + if rri.resourceUid: + resource_id = utils.base64_url_encode(rri.resourceUid) + resource_ok = is_resource_ok(resource_id, vault, configuration_uid) + logger.info(f"Admin Resource Uid: {resource_id if resource_ok else 'FAIL'}") + + if rri.pwdComplexity: + logger.info(f"Password Complexity: {rri.pwdComplexity}") + try: + record = vault.vault_data._records[record_uid] + if record: + complexity = crypto.decrypt_aes_v2(utils.base64_url_decode(rri.pwdComplexity), + record.record_key) + c = json.loads(complexity.decode()) + logger.info(f"Password Complexity Data: " + f"Length: {c.get('length')}; Lowercase: {c.get('lowercase')}; " + f"Uppercase: {c.get('caps')}; " + f"Digits: {c.get('digits')}; " + f"Symbols: {c.get('special')}; " + f"Symbols Chars: {c.get('specialChars')}") + except: + pass + else: + logger.info(f"Password Complexity: [not set]") + + logger.info(f"Is Rotation Disabled: {rri.disabled}") + + rq = pam_pb2.PAMGenericUidsRequest() + schedules_proto = router_utils.router_get_rotation_schedules(context, rq) + if schedules_proto: + schedules = list(schedules_proto.schedules) + for s in schedules: + if s.recordUid == record_uid_bytes: + if s.noSchedule is True: + logger.info(f"Schedule Type: Manual Rotation") + else: + if s.scheduleData: + schedule_arr = s.scheduleData.replace('RotateActionJob|', '').split('.') + if len(schedule_arr) == 4: + schedule_str = f'{schedule_arr[0]} on {schedule_arr[1]} at {schedule_arr[2]} UTC with interval count of {schedule_arr[3]}' + elif len(schedule_arr) == 3: + schedule_str = f'{schedule_arr[0]} at {schedule_arr[1]} UTC with interval count of {schedule_arr[2]}' + else: + schedule_str = s.scheduleData + logger.info(f"Schedule: {schedule_str}") + break + + logger.info(f"\nCommand to manually rotate: pam action rotate -r {record_uid}") + else: + logger.info(f'Rotation Status: Not ready to rotate ({rri_status_name})') + + +class PAMRouterScriptCommand(base.GroupCommand): + def __init__(self): + super().__init__('PAM Router Script') + self.register_command(PAMScriptListCommand(), 'list', 'l') + self.register_command(PAMScriptAddCommand(), 'add', 'a') + self.register_command(PAMScriptEditCommand(), 'edit', 'e') + self.register_command(PAMScriptDeleteCommand(), 'delete', 'd') + self.default_verb = 'list' + + +class PAMScriptListCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotate script view', parents=[base.report_output_parser], + description='List script fields') + parser.add_argument('pattern', nargs='?', help='Record UID, path, or search pattern') + super().__init__(parser) + + def execute(self, context: KeeperParams, **kwargs): + pattern = kwargs.get('pattern') + + vault = context.vault + + table = [] + header = ['record_uid', 'title', 'record_type', 'script_uid', 'script_name', 'records', 'command'] + for rec in vault.vault_data.find_records(criteria=pattern, record_version=3, + record_type=('pamUser', 'pamDirectory')): + record = vault.vault_data.load_record(rec.record_uid) + if not isinstance(record, vault_record.TypedRecord): + continue + for field in (x for x in record.fields if x.type == 'script'): + value = field.get_default_value(dict) + if not value: + continue + file_ref = value.get('fileRef') + if not file_ref: + continue + file_record = vault.vault_data.load_record(file_ref) + if not file_record: + continue + records = value.get('recordRef') + command = value.get('command') + table.append([record.record_uid, record.title, record.record_type, file_record.record_uid, + file_record.title, records, command]) + fmt = kwargs.get('format') + if fmt != 'json': + header = [report_utils.field_to_title(x) for x in header] + return report_utils.dump_report_data(table, header, fmt=fmt, filename=kwargs.get('output'), row_number=True) + + +class PAMScriptAddCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotate script add', description='Add script to record') + PAMScriptAddCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--script', required=True, dest='script', action='store', + help='Script file name') + parser.add_argument('--add-credential', dest='add_credential', action='append', + help='Record with rotation credential') + parser.add_argument('--script-command', dest='script_command', action='store', + help='Script command') + parser.add_argument('record', help='Record UID or Title') + + def execute(self, context: KeeperParams, **kwargs): + vault = context.vault + + record_name = kwargs.get('record') + if not record_name: + raise base.CommandError('"record" argument is required') + records = list(vault.vault_data.find_records(criteria=record_name, record_version=3, record_type=('pamUser', 'pamDirectory'))) + if len(records) == 0: + raise base.CommandError(f'Record "{record_name}" not found') + if len(records) > 1: + raise base.CommandError(f'Record "{record_name}" is not unique. Use record UID.') + record = vault.vault_data.load_record(records[0].record_uid) + if not isinstance(record, vault_record.TypedRecord): + raise base.CommandError(f'Record "{record.title}" is not a rotation record.') + + script_field = next((x for x in record.fields if x.type == 'script'), None) + if not script_field: + script_field = vault_record.TypedField.create_field(field_type='script', field_label='rotationScripts', required=False) + record.fields.append(script_field) + + file_name = kwargs.get('script') + full_name = os.path.expanduser(file_name) + if not os.path.isfile(full_name): + raise base.CommandError(f'File "{file_name}" not found.') + + facade = record_facades.FileRefRecordFacade() + facade.record = record + pre = set(facade.file_ref) + upload_task = attachment.FileUploadTask(full_name) + attachment.upload_attachments(vault, record, [upload_task]) + post = set(facade.file_ref) + df = post.difference(pre) + if len(df) == 1: + file_uid = df.pop() + facade.file_ref.remove(file_uid) + script_value = { + 'fileRef': file_uid, + 'recordRef': [], + 'command': '', + } + script_field.value.append(script_value) + record_refs = kwargs.get('add_credential') + if isinstance(record_refs, list): + for ref in record_refs: + if ref in vault.vault_data._records: + script_value['recordRef'].append(ref) + cmd = kwargs.get('script_command') + if cmd: + script_value['command'] = cmd + + record_management.update_record(vault, record) + vault.sync_data = True + + +class PAMScriptEditCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotate script edit', description='Edit script field') + PAMScriptEditCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--script', required=True, dest='script', action='store', + help='Script UID or name') + parser.add_argument('-ac', '--add-credential', dest='add_credential', action='append', + help='Add a record with rotation credential') + parser.add_argument('-rc', '--remove-credential', dest='remove_credential', action='append', + help='Remove a record with rotation credential') + parser.add_argument('--script-command', dest='script_command', action='store', + help='Script command') + parser.add_argument('record', help='Record UID or Title') + + def execute(self, context: KeeperParams, **kwargs): + vault = context.vault + + record_name = kwargs.get('record') + if not record_name: + raise base.CommandError('"record" argument is required') + + script_name = kwargs.get('script') + if not script_name: + raise base.CommandError('"script" argument is required') + + records = list(vault.vault_data.find_records(criteria=record_name, record_version=3, record_type=('pamUser', 'pamDirectory'))) + if len(records) == 0: + raise base.CommandError(f'Record "{record_name}" not found') + if len(records) > 1: + raise base.CommandError(f'Record "{record_name}" is not unique. Use record UID.') + record = vault.vault_data.load_record(records[0].record_uid) + if not isinstance(record, vault_record.TypedRecord): + raise base.CommandError(f'Record "{record.title}" is not a rotation record.') + + script_field = next((x for x in record.fields if x.type == 'script'), None) + if script_field is None: + raise base.CommandError(f'Record "{record.title}" has no rotation scripts.') + script_value = next((x for x in script_field.value if x.get('fileRef') == script_name), None) + if script_value is None: + s_name = script_name.casefold() + for x in script_field.value: + file_uid = x.get('fileRef') + file_record = vault.vault_data.load_record(file_uid) + if isinstance(file_record, vault_record.FileRecord): + if file_record.record_uid == s_name: + script_value = x + break + elif file_record.title.casefold() == s_name: + script_value = x + break + + if not isinstance(script_value, dict): + raise base.CommandError(f'Record "{record.title}" does not have script "{script_name}"') + + modified = False + refs = set() + record_refs = script_value.get('recordRef') + if isinstance(record_refs, list): + refs.update(record_refs) + remove_credential = kwargs.get('remove_credential') + if isinstance(remove_credential, list) and remove_credential: + refs.difference_update(remove_credential) + modified = True + add_credential = kwargs.get('add_credential') + if isinstance(add_credential, list) and add_credential: + refs.update(add_credential) + modified = True + if modified: + script_value['recordRef'] = list(refs) + command = kwargs.get('script_command') + if command: + script_value['command'] = command + modified = True + + if not modified: + raise base.CommandError('Nothing to do') + + record_management.update_record(vault, record) + vault.sync_data = True + + +class PAMScriptDeleteCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser(prog='pam rotate script delete', description='Delete script field') + PAMScriptDeleteCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--script', required=True, dest='script', action='store', + help='Script UID or name') + parser.add_argument('record', help='Record UID or Title') + + def execute(self, context: KeeperParams, **kwargs): + vault = context.vault + + record_name = kwargs.get('record') + if not record_name: + raise base.CommandError('"record" argument is required') + + script_name = kwargs.get('script') + if not script_name: + raise base.CommandError('"script" argument is required') + + records = list(vault.vault_data.find_records(criteria=record_name, record_version=3, record_type=('pamUser', 'pamDirectory'))) + if len(records) == 0: + raise base.CommandError(f'Record "{record_name}" not found') + if len(records) > 1: + raise base.CommandError(f'Record "{record_name}" is not unique. Use record UID.') + record = vault.vault_data.load_record(records[0].record_uid) + if not isinstance(record, vault_record.TypedRecord): + raise base.CommandError(f'Record "{record.title}" is not a rotation record.') + + script_field = next((x for x in record.fields if x.type == 'script'), None) + if script_field is None: + raise base.CommandError(f'Record "{record.title}" has no rotation scripts.') + script_value = next((x for x in script_field.value if x.get('fileRef') == script_name), None) + if script_value is None: + s_name = script_name.casefold() + for x in script_field.value: + file_uid = x.get('fileRef') + file_record = vault.vault_data.load_record(file_uid) + if isinstance(file_record, vault_record.FileRecord): + if file_record.record_uid == s_name: + script_value = x + break + elif file_record.title.casefold() == s_name: + script_value = x + break + + if not isinstance(script_value, dict): + raise base.CommandError(f'Record "{record.title}" does not have script "{script_name}"') + + script_field.value.remove(script_value) + record_management.update_record(vault, record) + vault.sync_data = True diff --git a/keepercli-package/src/keepercli/commands/pam/saas/__init__.py b/keepercli-package/src/keepercli/commands/pam/saas/__init__.py new file mode 100644 index 00000000..409aa575 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/saas/__init__.py @@ -0,0 +1,345 @@ +from __future__ import annotations +import json +from ....helpers.router_utils import router_send_action_to_gateway, get_response_payload +from ...pam.pam_dto import GatewayAction +from keepersdk.proto import pam_pb2 +from keepersdk.vault import vault_record +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk import utils +import hmac +import hashlib +import os +from pydantic import BaseModel +from typing import Optional, List, Any, TYPE_CHECKING + +from .... import api + +logger = api.get_logger() +if TYPE_CHECKING: + from ..discovery.discover import GatewayContext + from ....params import KeeperParams + from keepersdk.vault import vault_record + from keepersdk.helpers.keeper_dag.dag_vertex import DAGVertex + + +CATALOG_REPO = "Keeper-Security/discovery-and-rotation-saas-dev" + + +class GatewayActionSaasListCommandInputs: + + def __init__(self, + configuration_uid: str, + languages: Optional[List[str]] = None): + + if languages is None: + languages = ["en_US"] + + self.configurationUid = configuration_uid + self.languages = languages + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionSaasListCommand(GatewayAction): + + def __init__(self, inputs: GatewayActionSaasListCommandInputs, conversation_id=None): + super().__init__('saas-list', inputs=inputs, conversation_id=conversation_id, is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class SaasConfigEnum(BaseModel): + value: str + desc: Optional[str] = None + code: Optional[str] = None + + +class SaasConfigItem(BaseModel): + id: str + label: str + desc: str + is_secret: bool = False + desc_code: Optional[str] = None + type: Optional[str] = "text" + code: Optional[str] = None + default_value: Optional[Any] = None + enum_values: List[SaasConfigEnum] = [] + required: bool = False + + +class SaasPluginUsage(BaseModel): + record_id: str + plugin_name: str + user_uids: List[str] = [] + + +class SaasCatalog(BaseModel): + name: str + type: str = "catalog" + author: Optional[str] = None + email: Optional[str] = None + summary: Optional[str] = None + file: Optional[str] = None + file_sig: Optional[str] = None + allows_remote_management: Optional[bool] = False + readme: Optional[str] = None + fields: List[SaasConfigItem] = [] + installed: bool = False + used_by: List[SaasPluginUsage] = [] + + @property + def file_name(self): + # `file` will be either a file name or URL to a file. + # This will get the file name. + return self.file.split("/")[-1] if self.file else None + + +def get_gateway_saas_schema(context: KeeperParams, gateway_context: GatewayContext) -> Optional[List[dict]]: + """ + Get a plugins list from the Gateway. + + Using the plugins from the Gateway handles problem with versions. + """ + + if gateway_context is None: + logger.error(f"The user record does not have the set gateway") + return None + + # Get schema information from the Gateway + action_inputs = GatewayActionSaasListCommandInputs( + configuration_uid=gateway_context.configuration_uid, + ) + + conversation_id = GatewayAction.generate_conversation_id() + router_response = router_send_action_to_gateway( + context=context, + gateway_action=GatewayActionSaasListCommand( + inputs=action_inputs, + conversation_id=conversation_id), + message_type=pam_pb2.CMT_GENERAL, + is_streaming=False, + destination_gateway_uid_str=gateway_context.gateway_uid + ) + + if router_response is None: + logger.error(f"Did not get router response.") + return None + + response = router_response.get("response") + logger.debug(f"Router Response: {response}") + payload = get_response_payload(router_response) + data = payload.get("data") + if data is None: + raise Exception("The router returned a failure.") + elif data.get("success") is False: + error = data.get("error") + logger.debug(f"gateway returned: {error}") + logger.error(f"Could not get a list of SaaS plugins available on the gateway.") + return None + + return data.get("data", []) + + +def find_user_saas_configurations(context: KeeperParams, gateway_context: GatewayContext) -> dict: + """ + Find all the SaaS configuration being used by a gateway. + """ + + record_linking = RecordLink(record=gateway_context.configuration, context=context, logger=logger) + + def _walk_graph(v: DAGVertex, m: dict, pv: Optional[DAGVertex] = None): + if not v.active: + return + + # Get the record for this vertex; skip if not a pamUser. + record = context.vault.vault_data.get_record(v.uid) + if record is not None and record.record_type == "pamUser" and pv is not None: + acl = record_linking.get_acl(v.uid, pv.uid) + if acl is not None and acl.rotation_settings is not None: + config_record_uid_list = acl.rotation_settings.saas_record_uid_list + if config_record_uid_list is not None: + for config_record_uid in config_record_uid_list: + + if config_record_uid not in m: + config_record = context.vault.vault_data.load_record( + config_record_uid) + if config_record is None: + continue + + plugin_name = next((f.value for f in config_record.custom if f.label == "SaaS Type"), + None) + if plugin_name is None: + continue + + m[config_record_uid] = SaasPluginUsage( + record_id=config_record_uid, + plugin_name=plugin_name[0] + ) + + m[config_record_uid].user_uids.append(v.uid) + + for next_v in v.has_vertices(): + _walk_graph( + v=next_v, + pv=v, + m=m + ) + + usage_map = {} + _walk_graph( + v=record_linking.dag.get_root, + m=usage_map) + + return usage_map + + +def get_plugins_map(context: KeeperParams, gateway_context: GatewayContext) -> Optional[dict[str, SaasCatalog]]: + """ + Get a map of all the available plugins. + Then the Gateway is checked for custom plugin; plugins outside control. + The result is a dictionary, with the plugin name as the key. + """ + + plugin_map = {} + gateway_plugins = get_gateway_saas_schema(context, gateway_context) + if gateway_plugins is None: + return None + + # Add the Gateway plugins to map; all these plugins are installed. + for plugin_dict in gateway_plugins: + plugin = SaasCatalog.model_validate(plugin_dict) # type: SaasCatalog + plugin.installed = True + plugin_map[plugin.name] = plugin + + # #### CATALOG PLUGINS + + # Get the latest release of the catalog.json + api_url = f"https://api.github.com/repos/{CATALOG_REPO}/releases/latest" + res = utils.ssl_aware_get(api_url) + if res.ok is False: + logger.info("") + logger.error(f"Could not get plugin catalog from GitHub.") + return None + release_data = res.json() + + # Find the latest release URL + assets = release_data.get("assets", []) + asset = assets[0] + download_url = asset["browser_download_url"] + logger.debug(f"download {asset['name']} from {download_url}") + + # Download the latest the catalog.yml + res = utils.ssl_aware_get(download_url) + if res.ok is False: + logger.info("") + logger.error(f"Could not download the plugin catalog from GitHub.") + return None + + # Get a mapping of all the plugins being used by the plugin name. + # The group usage by plugin name; we can have multiple configuration for the same plugin + # This return dictionary of config record UID to SaasPluginUsage + plugin_usage = find_user_saas_configurations(context, gateway_context) + plugin_usage_map = {} + for config_record_uid in plugin_usage: # type: str + plugin_name = plugin_usage[config_record_uid].plugin_name + if plugin_name not in plugin_usage_map: + plugin_usage_map[plugin_name] = [] + plugin_usage_map[plugin_name].append(plugin_usage[config_record_uid]) + + for plugin_dict in json.loads(res.content): # type: dict + if plugin_dict.get("type") == "builtin": + continue + + plugin = SaasCatalog.model_validate(plugin_dict) # type: SaasCatalog + if plugin.name in plugin_map: + logger.debug(f"found duplicate plugin {plugin.name}; using plugin from gateway.") + continue + plugin_map[plugin.name] = plugin + + return plugin_map + + +def make_script_signature(plugin_code_bytes: bytes) -> str: + + this_is_not_a_secret = b"NOT_IMPORTANT" + return hmac.new(this_is_not_a_secret, plugin_code_bytes, hashlib.sha256).hexdigest() + + +def get_field_input(field, current_value: Optional[str] = None): + + logger.debug(field.model_dump_json()) + + logger.info(f"{field.label}") + logger.info(f"Description: {field.desc}") + if field.required is True: + logger.warning(f"Field is required.") + if field.type == "multiline": + logger.info(f"Enter a file path to load value from file.") + + while True: + prompt = "Enter value" + extra_text = [] + valid_values = [] + if len(field.enum_values) > 0: + valid_values = [str(x.value) for x in field.enum_values] + extra_text.append(f"Allowed values: " + + f", ".join(valid_values)) + if current_value is not None: + extra_text.append(f"Enter for current value '{current_value}'") + if field.default_value is not None: + extra_text.append(f"Enter for default value '{field.default_value}'") + if len(extra_text) > 0: + prompt += f" (" + "; ".join(extra_text) + ")" + prompt += " > " + value = input(prompt) + if value == "": + if current_value is not None: + value = current_value + elif field.default_value is not None: + value = field.default_value + elif os.path.exists(value): + with open(value, "r") as fh: + value = fh.read() + fh.close() + if len(valid_values) > 0 and value not in valid_values: + logger.error(f"{value} is not a valid value.") + continue + if value is not None: + break + if field.required is False: + break + + logger.error(f"This field is required.") + + return [value] + + +def get_record_field_value(record: vault_record.TypedRecord, label: str) -> Optional[str]: + + field = next((f for f in record.custom if f.label == label), None) + if field is None or field.value is None or len(field.value) == 0 or field.value[0] is None or field.value[0] == "": + return None + return field.value[0] + + +def set_record_field_value(record: vault_record.TypedRecord, label: str, value: str, field_type: Optional[str] = "text"): + + if value is not None and isinstance(value, list): + value = value[0] + + field = next((f for f in record.custom if f.label == label), None) + if field is None or field.value is None or len(field.value) == 0 or field.value[0] is None or field.value[0] == "": + if value is not None: + record.custom.append( + vault_record.TypedField.create_field( + field_type=field_type, + field_label=label, + required=False + ) + ) + elif value is not None: + field.value = [value] + else: + field.value = [] diff --git a/keepercli-package/src/keepercli/commands/pam/saas/saas_commands.py b/keepercli-package/src/keepercli/commands/pam/saas/saas_commands.py new file mode 100644 index 00000000..504268a6 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/saas/saas_commands.py @@ -0,0 +1,1089 @@ + +import argparse +import json +import logging +import os +import traceback +from typing import List, Optional +from tempfile import TemporaryDirectory + +from keepersdk.helpers.keeper_dag import dag_utils + +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from ..pam_dto import GatewayAction +from ....params import KeeperParams +from .... import api +from . import ( + SaasCatalog, + get_plugins_map, + get_field_input, + make_script_signature, + get_record_field_value, + set_record_field_value, +) + +from keepersdk.vault import vault_record, vault_extensions, attachment, record_management +from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY +from keepersdk.helpers.keeper_dag.record_link import RecordLink +from keepersdk.helpers.keeper_dag.dag_types import UserAclRotationSettings +from keepersdk import crypto, utils +from keepersdk.proto import record_pb2 +from keepersdk.errors import KeeperApiError + +logger = api.get_logger() + + +class RecordNotConfigException(Exception): + pass + + +class GatewayActionSaasConfigCommandInputs: + + def __init__(self, + configuration_uid: str, + plugin_code: str, + gateway_context: GatewayContext, + languages: Optional[List[str]] = None, + ): + + if languages is None: + languages = ["en_US"] + + self.configurationUid = configuration_uid + self.pluginCodeEnv = gateway_context.encrypt_str(plugin_code) + self.languages = languages + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class GatewayActionSaasListCommand(GatewayAction): + + def __init__(self, inputs: GatewayActionSaasConfigCommandInputs, conversation_id=None): + super().__init__('saas-list', inputs=inputs, conversation_id=conversation_id, is_scheduled=True) + + def toJSON(self): + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class PAMActionSaasConfigCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-saas-config') + PAMActionSaasConfigCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--list', '-l', required=False, dest='do_list', action='store_true', + help='List available SaaS rotations.') + parser.add_argument('--plugin', '-p', required=False, dest='plugin', action='store', + help='Plugin name') + parser.add_argument('--info', required=False, dest='do_info', action='store_true', + help='Get information about a plugin or plugins being used.') + parser.add_argument('--create', required=False, dest='do_create', action='store_true', + help='Create a SaaS Plugin config record.') + parser.add_argument('--update-config-uid', '-u', required=False, dest='do_update', action='store', + help='Update an existing SaaS configuration.') + parser.add_argument('--shared-folder-uid', '-s', required=False, dest='shared_folder_uid', + action='store', help='Shared folder to store SaaS configuration.') + + @staticmethod + def _show_list(plugins: dict[str, SaasCatalog]): + + sorted_catalog = {} + if plugins: + sorted_catalog = dict(sorted(plugins.items(), key=lambda i: i[1].name)) + + sort_results = { + "custom": {"title": "Custom", "using": [], "not_using": []}, + "catalog": {"title": "Catalog", "using": [], "not_using": []}, + "builtin": {"title": "Builtin", "using": [], "not_using": []}, + } + + logger.info("") + logger.info(f"Available SaaS Plugins") + for _, plugin in sorted_catalog.items(): + plugin_type = plugin.type + status = "using" if len(plugin.used_by) is True else "not_using" + sort_results[plugin_type][status].append(plugin) + + for plugin_type in ["custom", "catalog", "builtin"]: + for status in ["not_using", "using"]: + title = sort_results[plugin_type]["title"] + for plugin in sort_results[plugin_type][status]: + summary = plugin.summary or "No description available" + name = plugin.name + desc = f" ({title}" + if status == "using": + desc += f", Using" + desc += f")" + row = f" * {name}{desc} - {summary}" + logger.info(row) + + @staticmethod + def _show_plugin_info(plugin: SaasCatalog): + logger.info("") + logger.info(f"{plugin.name}") + logger.info(f" Type: {plugin.type}") + if plugin.author and plugin.email: + logger.info(f" Author: {plugin.author} ({plugin.email})") + elif plugin.author: + logger.info(f" Author: {plugin.author}") + logger.info(f" Summary: {plugin.summary or 'No description available'}") + if plugin.readme: + logger.info(f" Documents: {plugin.readme}") + logger.info(f" Fields") + req_field = [] + opt_field = [] + for field in plugin.fields: + if field.required: + req_field.append(f" * Required: {field.label} - " + f"{field.desc}") + else: + opt_field.append(f" * Optional: {field.label} - {field.desc}") + for item in req_field: + logger.info(item) + for item in opt_field: + logger.info(item) + logger.info("") + + @staticmethod + def _create_config(context: KeeperParams, + plugin: SaasCatalog, + shared_folder_uid: str, + plugin_code_bytes: Optional[bytes] = None): + + custom_fields = [ + vault_record.TypedField.create_field( + field_type="text", + field_label="SaaS Type", + required=True + ), + vault_record.TypedField.create_field( + field_type="text", + field_label="Active", + required=False + ) + ] + + for is_required in [True, False]: + for item in plugin.fields: + if item.required is is_required: + logger.info("") + value = get_field_input(item) + if value is not None: + field_type = item.type + if field_type in ["url", "int", "number", "bool", "enum"]: + field_type = "text" + + record_field = vault_record.TypedField.create_field(field_type=field_type, field_label=item.label, required=True) + + custom_fields.append(record_field) + + logger.info("") + while True: + title = input("Title for the SaaS configuration record> ") + if title != "": + break + logger.error(f"Require a record title.") + + record = vault_record.TypedRecord() + record.type_name = "login" + record.record_uid = utils.generate_uid() + record.record_key = utils.generate_aes_key() + record.title = title + + for item in custom_fields: + record.custom.append(item) + + vault = context.vault + folder = vault.vault_data.get_folder(shared_folder_uid) + folder_key = None # type: Optional[bytes] + if folder.folder_type == 'shared_folder_folder': + shared_folder_uid = folder.folder_scope_uid + elif folder.folder_type == 'shared_folder': + shared_folder_uid = folder.folder_uid + else: + shared_folder_uid = None + if shared_folder_uid and shared_folder_uid in vault.vault_data._shared_folders: + shared_folder = vault.vault_data.get_folder(shared_folder_uid) + folder_key = shared_folder.folder_key + + add_record = record_pb2.RecordAdd() + add_record.record_uid = utils.base64_url_decode(record.record_uid) + add_record.record_key = crypto.encrypt_aes_v2(record.record_key, vault.keeper_auth.auth_context.data_key) + add_record.client_modified_time = utils.current_milli_time() + add_record.folder_type = record_pb2.user_folder + if folder: + add_record.folder_uid = utils.base64_url_decode(folder.folder_uid) + if folder.folder_type == 'shared_folder': + add_record.folder_type = record_pb2.shared_folder + elif folder.folder_type == 'shared_folder_folder': + add_record.folder_type = record_pb2.shared_folder_folder + if folder_key: + add_record.folder_key = crypto.encrypt_aes_v2(record.record_key, folder_key) + + record_type = vault.vault_data.get_record_type_by_name(record.record_type) + data = vault_extensions.extract_typed_record_data(record, record_type) + json_data = vault_extensions.get_padded_json_bytes(data) + add_record.data = crypto.encrypt_aes_v2(json_data, record.record_key) + + if vault.keeper_auth.auth_context.enterprise_ec_public_key: + audit_data = vault_extensions.extract_audit_data(record) + if audit_data: + add_record.audit.version = 0 + add_record.audit.data = crypto.encrypt_ec( + json.dumps(audit_data).encode('utf-8'), vault.keeper_auth.auth_context.enterprise_ec_public_key) + + rq = record_pb2.RecordsAddRequest() + rq.records.append(add_record) + rs = vault.keeper_auth.execute_auth_rest('vault/records_add', rq, response_type=record_pb2.RecordsModifyResponse) + record_rs = next((x for x in rs.records if utils.base64_url_encode(x.record_uid) == record.record_uid), None) + if record_rs: + if record_rs.status != record_pb2.RS_SUCCESS: + raise KeeperApiError(record_rs.status.__str__(), rs.message) + record.revision = rs.revision + + vault.sync_down() + + # If this is not a built-in or custom script, we need to attach it to the config record. + if plugin_code_bytes is not None and plugin.file_name: + + with TemporaryDirectory() as temp_dir: + vault.sync_down() + + existing_record = vault.vault_data.load_record(record.record_uid) + if existing_record is None: + logger.error(f"Could not load the config record {record.record_uid} to attach script.") + return + + temp_file = os.path.join(temp_dir, plugin.file_name) + with open(temp_file, "wb") as fh: + fh.write(plugin_code_bytes) + fh.close() + task = attachment.FileUploadTask(temp_file) + task.title = f"{plugin.name} Script" + task.mime_type = "text/x-python" + + if plugin.file_sig: + script_signature = make_script_signature(plugin_code_bytes) + if script_signature != plugin.file_sig: + raise ValueError("The plugin signature in catalog does not match what was downloaded.") + + attachment.upload_attachments(vault, existing_record, [task]) + + record.fields = [ + vault_record.TypedField.create_field( + field_type="fileRef", + field_label="rotationScripts", + required=False) + ] + + record_management.update_record(vault, existing_record) + context.vault.sync_down() + + logger.info("") + logger.info(f"Created SaaS configuration record with UID of {record.record_uid}") + logger.info("") + logger.info("Assign this configuration to a user using the following command.") + logger.info(f" pam action saas set -c {record.record_uid} -u ") + logger.info(f" See pam action saas set --help for more information.") + + def execute(self, context: KeeperParams, **kwargs): + + do_list = kwargs.get("do_list", False) + do_info = kwargs.get("do_info", False) + do_create = kwargs.get("do_create", False) + do_update = kwargs.get("do_update", False) + shared_folder_uid = kwargs.get("shared_folder_uid") + + use_plugin = kwargs.get("plugin") + gateway = kwargs.get("gateway") + configuration_uid = kwargs.get('configuration_uid') + + vault = context.vault + gateway_context = GatewayContext.from_gateway(vault=vault, gateway=gateway, configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return None + + plugins = get_plugins_map(context=context, gateway_context=gateway_context) + + if do_list: + self._show_list(plugins) + elif use_plugin is not None: + + if use_plugin not in plugins: + logger.error(f"Cannot find '{use_plugin}' in the catalog.") + return None + + plugin = plugins[use_plugin] + + if do_info: + self._show_plugin_info(plugin=plugin) + + elif do_create: + + shared_folders = gateway_context.get_shared_folders(vault) + if shared_folder_uid is None: + if len(shared_folders) == 1: + shared_folder_uid = shared_folders[0].get("uid") + else: + logger.error(f"Multiple shared folders found. Please use '-s' to select a shared folder.") + if next((x for x in shared_folders if x.get("uid") == shared_folder_uid), None) is None: + logger.error(f"The shared folder is not part of the gateway application.") + return None + + # For catalog plugins, we need to download the python file from GitHub. + plugin_code_bytes = None + if plugin.type == "catalog" and plugin.file: + res = utils.ssl_aware_get(plugin.file) + if res.ok is False: + logger.error(f"Could not download the script from GitHub.") + return None + plugin_code_bytes = res.content + + self._create_config( + context=context, + plugin=plugin, + shared_folder_uid=shared_folder_uid, + plugin_code_bytes=plugin_code_bytes) + elif do_update: + pass + else: + self.get_parser().print_help() + else: + if do_update: + pass + else: + self.get_parser().print_help() + + +class PAMActionSaasSetCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam action saas set') + PAMActionSaasSetCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User record') + parser.add_argument('--config-record-uid', '-c', required=True, dest='config_record_uid', + action='store', help='The UID of the record that has SaaS configuration') + parser.add_argument('--resource-uid', '-r', required=False, dest='resource_uid', action='store', + help='The UID of the Resource record, if needed.') + + def execute(self, context: KeeperParams, **kwargs): + + user_uid = kwargs.get("user_uid") + resource_uid = kwargs.get("resource_uid") + config_record_uid = kwargs.get("config_record_uid") + + logger.info("") + + vault = context.vault + + user_record = vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + # Make sure this user is a PAM User. + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User.") + return + + record_rotation = context.get_record_rotation(user_record.record_uid) + if record_rotation is not None: + configuration_uid = record_rotation.configuration_uid + else: + logger.error(f"The user record does not have any rotation settings.") + return + + if configuration_uid is None: + logger.error(f"The user record does not have the configuration record set in the rotation settings.") + return + + gateway_context = GatewayContext.from_configuration_uid(vault=vault, configuration_uid=configuration_uid) + + if gateway_context is None: + logger.error(f"The user record does not have the set gateway") + return + + plugins = get_plugins_map(context=context, gateway_context=gateway_context) + if plugins is None: + return + + # Check to see if the config record exists. + config_record = vault.vault_data.get_record(config_record_uid) + if config_record is None: + logger.error(f"The SaaS configuration record does not exists.") + return + + # Make sure this config is a Login record. + if config_record.record_type not in ["login", "saasConfiguration"]: + logger.error(f"The SaaS configuration record is not a SaaS configuration record: " + f"{config_record.record_type}") + return + + config_record = vault.vault_data.load_record(config_record_uid) + + plugin_name_field = next((x for x in config_record.custom if x.label == "SaaS Type"), None) + if plugin_name_field is None: + logger.error(f"The SaaS configuration record is missing the custom field label 'SaaS Type'") + return + + plugin_name = None + if plugin_name_field.value is not None and len(plugin_name_field.value) > 0: + plugin_name = plugin_name_field.value[0] + + if plugin_name is None: + logger.error(f"The SaaS configuration record's custom field label 'SaaS Type' does not have a value.") + return + + if plugin_name not in plugins: + logger.error(f"The SaaS configuration record's custom field label 'SaaS Type' is not supported by the " + "gateway or the value is not correct.") + return + + plugin = plugins[plugin_name] + + # Make sure the SaaS configuration record has correct custom fields. + missing_fields = [] + for field in plugin.fields: + if field.required is True and field.default_value is None: + found = next((x for x in config_record.custom if x.label == field.label), None) + if not found: + missing_fields.append(field.label.strip()) + + if len(missing_fields) > 0: + logger.error(f"The SaaS configuration record is missing the following required custom fields: " + f'{", ".join(missing_fields)}') + return + + parent_uid = gateway_context.configuration_uid + + # Allow a resource record to be used. + if resource_uid is not None: + resource_record = vault.vault_data.load_record(resource_uid) + if resource_record is None: + logger.error(f"The resource record does not exists.") + return + + # Make sure this user is a PAM User. + if user_record.record_type in [PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY]: + logger.error(f"The resource record does not have the correct record type.") + return + + parent_uid = resource_uid + + record_link = RecordLink(record=gateway_context.configuration, context=context, fail_on_corrupt=False) + acl = record_link.get_acl(user_uid, parent_uid) + if acl is None: + if resource_uid is not None: + logger.error(f"There is no relationship between the user and the resource record.") + else: + logger.error(f"There is no relationship between the user and the configuration record.") + return + + if acl.rotation_settings is None: + acl.rotation_settings = UserAclRotationSettings() + + if resource_uid is not None and acl.rotation_settings.noop is True: + logger.error(f"The rotation is flagged as No Operation, however you passed in a resource record. " + f"This combination is not allowed.") + return + + # If there is a resource record, it is not NOOP. + # If there is NO resource record, it is NOOP. + # However, if this is an IAM User, don't set the NOOP + if acl.is_iam_user is False: + acl.rotation_settings.noop = resource_uid is None + + if config_record_uid in acl.rotation_settings.saas_record_uid_list: + logger.error(f"The SaaS configuration record is already being used for this user.") + return + + acl.rotation_settings.saas_record_uid_list = [config_record_uid] + + record_link.belongs_to(user_uid, parent_uid, acl=acl) + record_link.save() + + logger.info(f"Setting {plugin_name} rotation for the user record.") + logger.info("") + + +class PAMActionSaasRemoveCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-saas-remove') + PAMActionSaasRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User record') + parser.add_argument('--resource-uid', '-r', required=False, dest='resource_uid', action='store', + help='The UID of the Resource record, if needed.') + + def execute(self, context: KeeperParams, **kwargs): + + user_uid = kwargs.get("user_uid") # type: str + resource_uid = kwargs.get("resource_uid") # type: str + + logger.info("") + vault = context.vault + + user_record = vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + # Make sure this user is a PAM User. + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User.") + return + + record_rotation = context.get_record_rotation(user_record.record_uid) + if record_rotation is not None: + configuration_uid = record_rotation.configuration_uid + else: + logger.error(f"The user record does not have any rotation settings.") + return + + if configuration_uid is None: + logger.error(f"The user record does not have the configuration record set in the rotation settings.") + return + + gateway_context = GatewayContext.from_configuration_uid(vault=vault, configuration_uid=configuration_uid) + + if gateway_context is None: + logger.error(f"The user record does not have the set gateway") + return + + # Don't check config record + parent_uid = gateway_context.configuration_uid + + # Allow a resource record to be used. + if resource_uid is not None: + resource_record = vault.vault_data.get_record(resource_uid) + if resource_record is None: + logger.error(f"The resource record does not exists.") + return + + # Make sure this user is a PAM User. + if user_record.record_type in [PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY]: + logger.error(f"The resource record does not have the correct record type.") + return + + parent_uid = resource_uid + + record_link = RecordLink(record=gateway_context.configuration, context=context, fail_on_corrupt=False) + acl = record_link.get_acl(user_uid, parent_uid) + if acl is None: + if resource_uid is not None: + logger.error(f"There is no relationship between the user and the resource record.") + else: + logger.error(f"There is no relationship between the user and the configuration record.") + return + + if acl.rotation_settings is None: + acl.rotation_settings = UserAclRotationSettings() + + if resource_uid is not None and acl.rotation_settings.noop is True: + logger.error(f"The rotation is flagged as No Operation, however you passed in a resource record. " + f"This combination is not allowed.") + return + + # An empty array removes the SaaS config. + acl.rotation_settings.saas_record_uid_list = [] + + record_link.belongs_to(user_uid, parent_uid, acl) + record_link.save() + + logger.info(f"Removing SaaS service rotation from the user record.") + + +class PAMActionSaasUserCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-saas-user') + PAMActionSaasUserCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--user-record-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User record') + + def execute(self, context: KeeperParams, **kwargs): + + user_uid = kwargs.get("user_uid") + + logger.info("") + vault = context.vault + + user_record = vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + # Make sure this user is a PAM User. + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User.") + return + + record_rotation = context.get_record_rotation(user_record.record_uid) + if record_rotation is not None: + configuration_uid = record_rotation.configuration_uid + else: + logger.error(f"The user record does not have any rotation settings.") + return + + if configuration_uid is None: + logger.error(f"The user record does not have the configuration record set in the rotation settings.") + return + + gateway_context = GatewayContext.from_configuration_uid(vault=vault, configuration_uid=configuration_uid) + + if gateway_context is None: + logger.error(f"The user record does not have the set gateway") + return + + plugins = get_plugins_map(context, gateway_context) + + record_link = RecordLink(record=gateway_context.configuration, context=context, fail_on_corrupt=False) + user_vertex = record_link.get_record_link(user_uid) + if user_vertex is None: + logger.error(f"Cannot find the user in the record link graph.") + return + + logger.info(f"User: {user_record.title}") + + missing_configs = [] + + for parent_vertex in user_vertex.belongs_to_vertices(): + + parent_record = vault.vault_data.get_record(parent_vertex.uid) + if parent_record is None: + logger.error(f"* Parent record UID {parent_vertex.uid} does not exists.") + logger.error(f" The record may have been deleted, however the relationship still exists.") + logger.info("") + continue + + logger.info(f" * {parent_record.title}, {parent_record.record_type}") + logger.info("") + + acl = record_link.get_acl(user_uid, parent_vertex.uid) + if acl is not None and acl.rotation_settings is not None: + saas_record_uid_list = acl.rotation_settings.saas_record_uid_list + if saas_record_uid_list is None or len(saas_record_uid_list) == 0: + logger.error(f" The user does not have any SaaS service rotations.") + return + + for config_record_uid in saas_record_uid_list: + config_record = vault.vault_data.get_record(config_record_uid) + if config_record is None: + logger.error(f" * Record UID {config_record_uid} not longer exists.") + continue + logger.info(f" {config_record.title}") + + plugin_name = "" + saas_type_field = next((x for x in config_record.custom if x.label == "SaaS Type"), None) + if (saas_type_field is not None and saas_type_field.value is not None + and len(saas_type_field.value) > 0): + plugin_name = saas_type_field.value[0] + + plugin = plugins.get(plugin_name) + + if plugin is None: + plugin_name += " (Not Supported)" + + rotation_active = "Active" + rotation_active_field = next((x for x in config_record.custom if x.label == "Active"), + None) + + if (rotation_active_field is not None and rotation_active_field.value is not None + and len(rotation_active_field.value) > 0): + is_active = dag_utils.value_to_boolean(rotation_active_field.value[0]) + if is_active is False: + rotation_active = "Inactive" + + logger.info(f" SaaS Type: {plugin_name}") + logger.info(f" Config Record UID: {config_record.record_uid}") + logger.info(f" Active: {rotation_active}") + + if plugin is not None: + + for field in plugin.fields: + value = next((x.value for x in config_record.custom if x.label == field.label), None) + if value is not None: + if len(value) > 0: + value = value[0] + else: + value = None + if value is None: + if field.default_value is not None: + value = f"{field.default_value} (Default)" + else: + value = "Not Set" + logger.info(f" {field.label}: {value}") + logger.info("") + + +class PAMActionSaasUpdateCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-saas-update') + PAMActionSaasUpdateCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name of UID.') + parser.add_argument('--configuration-uid', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--all', '-a', required=False, dest='do_all', action='store_true', + help='Update all configurations.') + parser.add_argument('--config-record-uid', '-c', required=False, dest='config_uid', action='store', + help='Update a specific configuration.') + parser.add_argument('--dry-run', required=False, dest='do_dry_run', action='store_true', + help='Dry run. Do not save any changes.') + + @staticmethod + def get_field_values(record: vault_record.TypedRecord, field_type: str) -> List[str]: + return next( + (f.value + for f in record.fields + if f.type == field_type), + None + ) + + @classmethod + def _get_file_refs(cls, record: vault_record.TypedRecord) -> List[str]: + return list(next((x.value for x in record.fields if x.type == "fileRef"), [])) + + @classmethod + def _update_script(cls, context: KeeperParams, config_record: vault_record.TypedRecord, plugin: SaasCatalog): + + if plugin.type != "catalog": + raise ValueError("Cannot download script for non-catalog plugin.") + + if not plugin.file: + raise ValueError("Plugin does not have a file URL.") + + if not plugin.file_name: + raise ValueError("Plugin does not have a file name.") + + logger.info(" * downloading updated plugin script") + res = utils.ssl_aware_get(plugin.file) + if res.ok is False: + raise ValueError("Could download updated script from GitHub") + plugin_code_bytes = res.content + + new_script_sig = make_script_signature(plugin_code_bytes=plugin_code_bytes) + + if plugin.file_sig: + logger.debug(f"downloaded {new_script_sig} vs catalog {plugin.file_sig}") + if new_script_sig != plugin.file_sig: + raise ValueError("The plugin signature in catalog does not match what was downloaded.") + + with TemporaryDirectory() as temp_dir: + temp_file = os.path.join(temp_dir, plugin.file_name) + with open(temp_file, "wb") as fh: + fh.write(plugin_code_bytes) + fh.close() + + task = attachment.FileUploadTask(temp_file) + task.title = f"{plugin.name} Script" + task.mime_type = "text/x-python" + + existing_file_refs = cls._get_file_refs(config_record) + logger.debug(f"existing file ref: {existing_file_refs}") + + attachment.upload_attachments(context.vault, config_record, [task]) + + new_file_refs = cls._get_file_refs(config_record) + logger.debug(f"new file ref: {new_file_refs}") + + if existing_file_refs is not None: + logger.debug("existing file ref exists") + for existing_file_ref in existing_file_refs: # type: str + logger.debug(f" * {existing_file_ref}") + if existing_file_ref in new_file_refs: + new_file_refs.remove(existing_file_ref) + else: + logger.debug("no existing file ref, use new file ref") + + logger.debug(f"save file ref: {new_file_refs}") + + config_record.fields = [ + vault_record.TypedField.create_field( + field_type="fileRef", + field_label="rotationScripts", + required=False + ) + ] + + record_management.update_record(context.vault, config_record) + context.sync_data = True + + logger.info(f" * the plugin script is now up-to-date.") + + @classmethod + def _missing_fields(cls, config_record: vault_record.TypedRecord, plugin: SaasCatalog) -> List[str]: + + # Make the record into a map by the field label + records_field_map = {} + for field in config_record.custom: + records_field_map[field.label] = field + + missing_fields = [] + for field in plugin.fields: + if not field.required or field.default_value is not None: + continue + record_field = records_field_map.get(field.label) + if (record_field is None + or record_field.value is None + or len(record_field.value) == 0 + or record_field.value[0] is None + or record_field.value[0] == ""): + missing_fields.append(field.label) + return missing_fields + + @classmethod + def _update_config(cls, + context: KeeperParams, + plugins: dict[str, SaasCatalog], + config_record: vault_record.TypedRecord, + dry_run: bool = False) -> Optional[SaasCatalog]: + + plugin_field = next((x for x in config_record.custom if x.label == "SaaS Type"), None) + if plugin_field is None or len(plugin_field.value) == 0: + logger.debug("record is not a SaaS Configuration record") + raise RecordNotConfigException() + plugin_name = plugin_field.value[0] + logger.debug(f"plugin name is {plugin_name}") + + plugin = plugins.get(plugin_name) + if plugin is not None and plugin.type == "catalog": + + missing_fields = cls._missing_fields(config_record=config_record, plugin=plugin) + + logger.info(f"{config_record.title} ({config_record.record_uid}) - {plugin_name}") + logger.debug(f"plugin is {plugin_name} for config {config_record.title}") + attachments = list(attachment.prepare_attachment_download(context.vault, config_record.record_uid)) + + # If there is no script, attach script to record. + if len(attachments) == 0: + logger.info(" * the record does not contain a plugin script.") + logger.debug(" * configuration did not have script, add current script.") + + if not dry_run: + cls._update_script( + context=context, + config_record=config_record, + plugin=plugin, + ) + else: + logger.info(f" * not updating script due to dry run.") + + if len(missing_fields) == 0: + logger.info(f" * the configuration record fields are up-to-date.") + else: + logger.error(f" * the configuration record's required field(s) are missing or blank: " + f"{', '.join(missing_fields)}") + logger.info("") + return plugin + + logger.debug(f"found {len(attachments)} attached script(s).") + + if len(attachments) > 1: + raise ValueError("Found multiple scripts. Only one script is allowed per SaaS Configuration record.") + + for atta in attachments: + with TemporaryDirectory() as temp_dir: + if not plugin.file_name: + logger.debug("plugin does not have a file name, using default") + temp_file = str(os.path.join(temp_dir, f"{plugin.name}_script.py")) + else: + temp_file = str(os.path.join(temp_dir, plugin.file_name)) + logger.debug(f"download to {temp_file}") + + log_level = logger.getEffectiveLevel() + try: + logger.setLevel(logging.WARNING) + atta.download_to_file(temp_file) + finally: + logger.setLevel(log_level) + + with open(temp_file, "rb") as fh: + plugin_code_bytes = fh.read() + fh.close() + + attach_file_sig = make_script_signature(plugin_code_bytes=plugin_code_bytes) + + if plugin.file_sig: + logger.debug(f"attached {attach_file_sig} vs catalog {plugin.file_sig}") + sig_matches = attach_file_sig == plugin.file_sig + else: + logger.debug("plugin does not have a file signature, skipping verification") + sig_matches = True + + if not sig_matches: + logger.error(f" * the plugin script have changed.") + logger.debug("the script has changed, update") + + if not dry_run: + cls._update_script( + context=context, + config_record=config_record, + plugin=plugin, + ) + else: + logger.info(f" * not updating script due to dry run.") + else: + logger.info(f" * the plugin script is up-to-date.") + + if len(missing_fields) == 0: + logger.info(f" * the configuration record fields are up-to-date.") + else: + logger.error(f" * the configuration record's required field(s) are missing or blank: " + f"{', '.join(missing_fields)}") + + # If the record type is login, migrate to saasConfiguration + if config_record.record_type == "login": + logger.info(f" * migrate record type to SaaS Configuration.") + config_record.type_name = "saasConfiguration" + record_management.update_record(context.vault, config_record) + + logger.info("") + + logger.debug("plugin doesn't used attached scripts, or bad SaaS type in config record.") + return plugin + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") # type: str + do_all = kwargs.get("do_all", False) # type: bool + config_record_uid = kwargs.get("config_uid") # type: str + do_dry_run = kwargs.get("do_dry_run", False) # type: bool + + configuration_uid = kwargs.get('configuration_uid') # type Optional[str] + vault = context.vault + + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=configuration_uid) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + logger.info("") + + if do_dry_run: + logger.info(f"Dry run enabled. No changes will be saved.") + logger.info("") + + plugins = get_plugins_map( + context=context, + gateway_context=gateway_context + ) + + if do_all: + logger.debug("search vault for login record types") + for record in list(vault.vault_data.find_records(criteria=None, record_type=["login", "saasConfiguration"], record_version=None)): + logger.debug("--------------------------------------------------------------------------------------") + config_record = vault.vault_data.load_record(record.record_uid) + + logger.debug(f"checking record {record.record_uid}, {record.title}") + try: + self._update_config( + context=context, + plugins=plugins, + config_record=config_record, + dry_run=do_dry_run + ) + except RecordNotConfigException: + pass + except Exception as err: + logger.error(f" *{err}") + logger.debug(traceback.format_exc()) + logger.debug(f"ERROR (no fatal): {err}") + + context.sync_data = True + + elif config_record_uid is not None: + config_record = vault.vault_data.load_record(config_record_uid) + if config_record is None: + logger.error(f"Cannot find a record for UID {config_record_uid}.") + return + + try: + plugin = self._update_config( + context=context, + plugins=plugins, + config_record=config_record, + dry_run=do_dry_run + ) + if plugin is not None: + missing_fields = self._missing_fields(config_record=config_record, plugin=plugin) + + if len(missing_fields) > 0: + vault.sync_down() + config_record = vault.vault_data.get_record(config_record_uid) + + # If the record type is login, migrate to saasConfiguration + if config_record.record_type == "login": + logger.debug("migrating from login to saasConfiguration record type") + config_record.type_name = "saasConfiguration" + + config_record = vault.vault_data.load_record(config_record_uid) + + for required in [True, False]: + for field in plugin.fields: + if field.required is required: + current_value = get_record_field_value( + record=config_record, + label=field.label + ) + logger.info("") + value = get_field_input(field, current_value=current_value) + if value is not None: + set_record_field_value( + record=config_record, + label=field.label, + value=value + ) + + if not do_dry_run: + record_management.update_record(vault, config_record) + logger.info("") + logger.info(f"The SaaS configuration record has been updated.") + logger.info("") + else: + logger.info("") + logger.info(f"The SaaS configuration record was not saved due to dry run.") + logger.info("") + + vault.sync_down() + + except Exception as err: + logger.error("") + logger.debug(traceback.format_exc()) + logger.error(f"{err}.") + return + else: + logger.error("") + logger.error(f"Requires either the --all or --config-record-uid parameters.") + logger.info("") + self._parser.print_help() diff --git a/keepercli-package/src/keepercli/commands/pam/service/__init__.py b/keepercli-package/src/keepercli/commands/pam/service/__init__.py new file mode 100644 index 00000000..d41a4a70 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/service/__init__.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from keepersdk.helpers.keeper_dag.dag_utils import value_to_boolean +from keepersdk.vault import vault_online +from ....api import get_logger +import os +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from keepersdk.helpers.keeper_dag.connection import ConnectionBase + + +def get_connection(vault: vault_online.VaultOnline) -> ConnectionBase: + if value_to_boolean(os.environ.get("USE_LOCAL_DAG", False)) is False: + from keepersdk.helpers.keeper_dag.connection.commander import Connection as CommanderConnection + return CommanderConnection(vault=vault, logger=get_logger()) + else: + from keepersdk.helpers.keeper_dag.connection.local import Connection as LocalConnection + return LocalConnection() \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/pam/service/service_commands.py b/keepercli-package/src/keepercli/commands/pam/service/service_commands.py new file mode 100644 index 00000000..e68fecc0 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/pam/service/service_commands.py @@ -0,0 +1,354 @@ +import argparse +from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase +from ....params import KeeperParams +from .... import api +from ....__init__ import __version__ +from ....commands import base + +from keepersdk.helpers.keeper_dag.user_service import UserService +from keepersdk.helpers.keeper_dag.dag_types import EdgeType, ServiceAcl, RefType +from keepersdk.helpers.keeper_dag.constants import PAM_MACHINE, PAM_USER +from keepersdk.helpers.keeper_dag.record_link import RecordLink + + +logger = api.get_logger() + + +class PAMActionServiceListCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-service-list') + PAMActionServiceListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + vault = context.vault + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + user_service = UserService(record=gateway_context.configuration, context=context, fail_on_corrupt=False, + agent=f"Cmdr/{__version__}") + + service_map = {} + for resource_vertex in user_service.dag.get_root.has_vertices(edge_type=EdgeType.LINK): + resource_record = vault.vault_data.get_record(resource_vertex.uid) + if resource_record is None or resource_record.record_type != PAM_MACHINE: + continue + user_vertices = user_service.get_user_vertices(resource_vertex.uid) + if len(user_vertices) > 0: + for user_vertex in user_vertices: + user_record = vault.vault_data.load_record(user_vertex.uid) + if user_record is None: + continue + acl = user_service.get_acl(resource_record.record_uid, user_record.record_uid) + if acl is None or (acl.is_service is False and acl.is_task is False): + continue + if user_record.record_uid not in service_map: + service_map[user_record.record_uid] = { + "title": user_record.title, + "machines": [] + } + text = f"{resource_record.title} ({resource_record.record_uid}) :" + comma = "" + if acl.is_service: + text += f" Services" + comma = "," + if acl.is_task: + text += f"{comma} Scheduled Tasks" + if acl.is_iis_pool: + text += f"{comma} IIS Pools" + service_map[user_record.record_uid]["machines"].append(text) + + logger.info("") + printed_something = False + logger.info("User Mapping") + for user_uid in service_map: + user = service_map[user_uid] + printed_something = True + logger.info(f" {user['title']} ({user_uid})") + for machine in user["machines"]: + logger.info(f" * {machine}") + logger.info("") + if not printed_something: + logger.error(f"There are no service mappings.") + + +class PAMActionServiceAddCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-service-add') + PAMActionServiceAddCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--machine-uid', '-m', required=True, dest='machine_uid', action='store', + help='The UID of the Windows Machine record') + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User record') + parser.add_argument('--type', '-t', required=True, choices=['service', 'task', 'iis'], dest='type', + action='store', help='Relationship to add [service, task, iis]') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + machine_uid = kwargs.get("machine_uid") + user_uid = kwargs.get("user_uid") + rel_type = kwargs.get("type") + + logger.info("") + vault = context.vault + + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + if gateway_context is None: + logger.error(f"Cannot get gateway information. Gateway may not be up.") + return + + user_service = UserService(record=gateway_context.configuration, context=context, fail_on_corrupt=False, + agent=f"Cmdr/{__version__}") + record_link = RecordLink(record=gateway_context.configuration, context=context, fail_on_corrupt=False, + agent=f"Cmdr/{__version__}") + + ############### + machine_record = vault.vault_data.get_record(machine_uid) + if machine_record is None: + logger.error(f"The machine record does not exists.") + return + + if machine_record.record_type != PAM_MACHINE: + logger.error(f"The machine record is not a PAM Machine.") + return + + # Make sure this machine is linked to the configuration record. + machine_rl = record_link.get_record_link(machine_record.record_uid) + if machine_rl is None: + logger.error(f"The machine record does not exists in the graph.") + return + + # Edges from provider and machine might be wrong. + # Should be a LINK edge, could be an ACL edge. + if (machine_rl.get_edge(record_link.dag.get_root, edge_type=EdgeType.LINK) is None and + machine_rl.get_edge(record_link.dag.get_root, edge_type=EdgeType.ACL) is None): + logger.error(f"The machine record does not belong to this gateway.") + return + + ############### + + user_record = vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User.") + return + + record_rotation = context.get_record_rotation(user_record.record_uid) + if record_rotation is not None: + controller_uid = record_rotation.configuration_uid + if controller_uid is None or controller_uid != gateway_context.configuration_uid: + logger.error(f"The user record does not belong to this gateway. Cannot use this user.") + return + else: + logger.error(f"The user record does not have any rotation settings.") + return + + ######## + + # Make sure it is a Windows machine. + # Linux and Mac do not use passwords in services and cron jobs; no need to link. + machine_record = vault.vault_data.load_record(machine_uid) + os_field = next((x for x in machine_record.fields if x.label == "operatingSystem"), None) + if os_field is None: + logger.error(f"Cannot find the operating system field in this record.") + return + os_type = None + if len(os_field.value) > 0: + os_type = os_field.value[0] + if os_type is None: + logger.error(f"The operating system field of the machine record is blank.") + return + if os_type != "windows": + logger.error(f"The operating system is not Windows. " + "PAM can only rotate the services and scheduled task password on Windows.") + return + + machine_vertex = user_service.get_record_link(machine_record.record_uid) + if machine_vertex is None: + machine_vertex = user_service.dag.add_vertex( + uid=machine_record.record_uid, + name=machine_record.title, + vertex_type=RefType.PAM_MACHINE) + + user_vertex = user_service.get_record_link(user_record.record_uid) + if user_vertex is None: + user_vertex = user_service.dag.add_vertex( + uid=user_record.record_uid, + name=user_record.title, + vertex_type=RefType.PAM_USER) + + # Get the existing service ACL and set the proper attribute. + acl = user_service.get_acl(machine_vertex.uid, user_vertex.uid) + if acl is None: + acl = ServiceAcl() + if rel_type == "service": + acl.is_service = True + elif rel_type == "task": + acl.is_task = True + else: + acl.is_iis_pool = True + + if not user_service.dag.get_root.has(machine_vertex): + user_service.belongs_to(gateway_context.configuration_uid, machine_vertex.uid) + + user_service.belongs_to(machine_vertex.uid, user_vertex.uid, acl=acl) + + user_service.save() + + if rel_type == "service": + logger.info( + f"Success: Services running on this machine, using this user, will be updated and restarted after " + "password rotation." + ) + elif rel_type == "task": + logger.info( + f"Success: Scheduled tasks running on this machine, using this user, will be updated after " + "password rotation." + ) + else: + logger.info( + f"Success: IIS pools running on this machine, using this user, will be updated after " + "password rotation." + ) + + +class PAMActionServiceRemoveCommand(PAMGatewayActionDiscoverCommandBase): + + def __init__(self): + parser = argparse.ArgumentParser(prog='pam-action-service-remove') + PAMActionServiceRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store', + help='Gateway name or UID') + parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', + action='store', help='PAM configuration UID, if gateway has multiple.') + parser.add_argument('--machine-uid', '-m', required=True, dest='machine_uid', action='store', + help='The UID of the Windows Machine record') + parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', + help='The UID of the User record') + parser.add_argument('--type', '-t', required=True, choices=['service', 'task', 'iis'], dest='type', + action='store', help='Relationship to remove [service, task, iis]') + + def execute(self, context: KeeperParams, **kwargs): + + gateway = kwargs.get("gateway") + machine_uid = kwargs.get("machine_uid") + user_uid = kwargs.get("user_uid") + rel_type = kwargs.get("type") + + logger.info("") + if not context.vault: + raise base.CommandError("Vault not found. Login to initialize the vault.") + vault = context.vault + + gateway_context = GatewayContext.from_gateway(vault=vault, + gateway=gateway, + configuration_uid=kwargs.get('configuration_uid')) + if gateway_context is None: + logger.error(f"Could not find the gateway configuration for {gateway}.") + return + + if gateway_context is None: + logger.error(f"Cannot get gateway information. Gateway may not be up.") + return + + user_service = UserService(record=gateway_context.configuration, context=context, fail_on_corrupt=False, + agent=f"Cmdr/{__version__}") + + machine_record = vault.vault_data.get_record(machine_uid) + if machine_record is None: + logger.error(f"The machine record does not exists.") + return + + if machine_record.record_type != PAM_MACHINE: + logger.error(f"The machine record is not a PAM Machine.") + return + + user_record = vault.vault_data.get_record(user_uid) + if user_record is None: + logger.error(f"The user record does not exists.") + return + + if user_record.record_type != PAM_USER: + logger.error(f"The user record is not a PAM User.") + return + + machine_vertex = user_service.get_record_link(machine_record.record_uid) + if machine_vertex is None: + logger.error(f"The machine does not exist in the mapping.") + return + + user_vertex = user_service.get_record_link(user_record.record_uid) + if user_vertex is None: + logger.error(f"The user does not exist in the mapping.") + return + + acl = user_service.get_acl(machine_vertex.uid, user_vertex.uid) + if acl is None: + logger.error(f"The user did not control any services, scheduled tasks, or IIS pools on the machine.") + return + + if rel_type == "service": + acl.is_service = False + elif rel_type == "task": + acl.is_task = False + else: + acl.is_iis_pool = False + + if not user_service.dag.get_root.has(machine_vertex): + user_service.belongs_to(gateway_context.configuration_uid, machine_vertex.uid) + + user_service.belongs_to(machine_vertex.uid, user_vertex.uid, acl=acl) + user_service.save() + + if rel_type == "service": + logger.info( + f"Success: Services running on this machine will no longer have their password changed when this " + "user's password is rotated." + ) + elif rel_type == "task": + logger.info( + f"Success: Scheduled tasks running on this machine will no longer have their password changed " + "when this user's password is rotated." + ) + else: + logger.info( + f"Success: IIP pools running on this machine will no longer have their password changed " + "when this user's password is rotated." + ) diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index 25e2f41e..f7a6536f 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -70,7 +70,7 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): '-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' + '--email', '-e', 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' @@ -385,7 +385,8 @@ def add_client( first_access_expire_duration_ms=first_access_expire_duration_ms, access_expire_in_ms=access_expire_in_ms, master_key=master_key, - server=server + server=server, + client_type=GENERAL ) tokens.append(token_data['token_info']) diff --git a/keepercli-package/src/keepercli/commands/shares.py b/keepercli-package/src/keepercli/commands/shares.py index 1ec67b41..223f337e 100644 --- a/keepercli-package/src/keepercli/commands/shares.py +++ b/keepercli-package/src/keepercli/commands/shares.py @@ -118,7 +118,6 @@ 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() @@ -209,7 +208,6 @@ def get_contact(user, contacts): return None - class ShareFolderCommand(base.ArgparseCommand): def __init__(self): self.parser = argparse.ArgumentParser( diff --git a/keepercli-package/src/keepercli/helpers/email_utils.py b/keepercli-package/src/keepercli/helpers/email_utils.py new file mode 100644 index 00000000..1674f762 --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/email_utils.py @@ -0,0 +1,1364 @@ +import sys +from abc import ABC, abstractmethod +import os +from typing import Any, Dict, List, Optional +from dataclasses import dataclass, field +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import smtplib +import ssl +import logging + +import keepersdk +from keepersdk.vault import vault_online, vault_record, vault_extensions, record_management + +from .. import api + +logger = api.get_logger() + + +def get_installation_method(): + """ + Detect Keeper Commander installation method. + + Returns: + str: 'binary' (PyInstaller frozen), 'pip' (installed via pip), or 'source' (development) + """ + + # Check if running as PyInstaller binary + if getattr(sys, 'frozen', False): + return 'binary' + + # Check if installed via pip + location = keepersdk.__file__ + + if 'site-packages' in location or 'dist-packages' in location: + return 'pip' + + # Running from source + return 'source' + + +def check_provider_dependencies(provider: str) -> tuple: + """ + Check if dependencies for a provider are available. + + Args: + provider: Provider name (smtp, ses, sendgrid, gmail-oauth, microsoft-oauth) + + Returns: + tuple: (dependencies_available: bool, error_message: str) + """ + if provider == 'smtp': + # SMTP uses standard library, always works + return (True, '') + + installation_method = get_installation_method() + + # Binary installations only support SMTP + if installation_method == 'binary': + return ( + False, + f'{provider} is not available in the binary installation.\n' + f'\n' + f'To use this provider, you must switch to the PyPI version:\n' + f' 1. Uninstall the binary version\n' + f' 2. Install via pip with email dependencies:\n' + f' pip install keepercommander[email]\n' + f'\n' + f'The binary version only supports SMTP for email functionality.' + ) + + # Check for required packages on pip/source installations + if provider == 'ses': + try: + import boto3 + return (True, '') + except ImportError: + return ( + False, + 'AWS SES requires additional dependencies.\n' + 'Install with:\n' + ' pip install keepercommander[email-ses]\n' + ' # or install all email providers:\n' + ' pip install keepercommander[email]' + ) + + elif provider == 'sendgrid': + try: + import sendgrid + return (True, '') + except ImportError: + return ( + False, + 'SendGrid requires additional dependencies.\n' + 'Install with:\n' + ' pip install keepercommander[email-sendgrid]\n' + ' # or install all email providers:\n' + ' pip install keepercommander[email]' + ) + + elif provider == 'gmail-oauth': + try: + import google.auth + import googleapiclient + return (True, '') + except ImportError: + return ( + False, + 'Gmail OAuth requires additional dependencies.\n' + 'Install with:\n' + ' pip install keepercommander[email-gmail-oauth]\n' + ' # or install all email providers:\n' + ' pip install keepercommander[email]' + ) + + elif provider == 'microsoft-oauth': + try: + import msal + return (True, '') + except ImportError: + return ( + False, + 'Microsoft OAuth requires additional dependencies.\n' + 'Install with:\n' + ' pip install keepercommander[email-microsoft-oauth]\n' + ' # or install all email providers:\n' + ' pip install keepercommander[email]' + ) + + return (True, '') + + +@dataclass +class EmailConfig: + """ + Email configuration for sending emails via various providers. + + Stored as Keeper records (login type) with encrypted credentials. + See ADR Decision 1 in ONBOARDING_FEATURE_IMPLEMENTATION_PLAN.md + + Supported providers: + - smtp: Standard SMTP with username/password + - ses: AWS Simple Email Service + - sendgrid: SendGrid API + - gmail-oauth: Gmail with OAuth 2.0 (uses Gmail API) + - microsoft-oauth: Microsoft 365/Outlook with OAuth 2.0 (uses Graph API) + """ + record_uid: str + name: str + provider: str # 'smtp', 'ses', 'sendgrid', 'gmail-oauth', 'microsoft-oauth' + from_address: str + from_name: str = "Keeper Commander" + + # SMTP-specific + smtp_host: Optional[str] = None + smtp_port: int = 587 + smtp_username: Optional[str] = None + smtp_password: Optional[str] = None + smtp_use_tls: bool = True + smtp_use_ssl: bool = False + + # AWS SES-specific + aws_region: Optional[str] = None + aws_access_key: Optional[str] = None + aws_secret_key: Optional[str] = None + + # SendGrid-specific + sendgrid_api_key: Optional[str] = None + + # OAuth-specific (gmail-oauth, microsoft-oauth) + oauth_client_id: Optional[str] = None + oauth_client_secret: Optional[str] = None + oauth_access_token: Optional[str] = None + oauth_refresh_token: Optional[str] = None + oauth_token_expiry: Optional[str] = None # ISO 8601 format + oauth_scopes: Optional[List[str]] = None + oauth_tenant_id: Optional[str] = None # For Microsoft 365 (can be 'common', 'organizations', or specific tenant ID) + + # Additional metadata + custom_fields: Dict[str, Any] = field(default_factory=dict) + + # Internal flag to track OAuth token updates (not stored in Keeper) + _oauth_tokens_updated: bool = field(default=False, init=False) + + def validate(self) -> List[str]: + """ + Validate email configuration completeness. + + Returns: + List of validation error messages (empty if valid) + """ + errors = [] + + if not self.provider: + errors.append("Provider is required") + + if not self.from_address: + errors.append("From address is required") + + if self.provider == 'smtp': + if not self.smtp_host: + errors.append("SMTP host is required") + if not self.smtp_username: + errors.append("SMTP username is required") + if not self.smtp_password: + errors.append("SMTP password is required") + + elif self.provider == 'ses': + if not self.aws_region: + errors.append("AWS region is required for SES") + if not self.aws_access_key: + errors.append("AWS access key is required for SES") + if not self.aws_secret_key: + errors.append("AWS secret key is required for SES") + + elif self.provider == 'sendgrid': + if not self.sendgrid_api_key: + errors.append("SendGrid API key is required") + + elif self.provider in ('gmail-oauth', 'microsoft-oauth'): + # OAuth providers require either interactive auth OR manual token entry + if not self.oauth_client_id: + errors.append(f"OAuth client ID is required for {self.provider}") + if not self.oauth_client_secret: + errors.append(f"OAuth client secret is required for {self.provider}") + + # If no access token, we'll do interactive OAuth flow + # If access token provided, we should also have refresh token + if self.oauth_access_token and not self.oauth_refresh_token: + errors.append("OAuth refresh token is required when access token is provided") + + # Microsoft requires tenant ID + if self.provider == 'microsoft-oauth' and not self.oauth_tenant_id: + errors.append("OAuth tenant ID is required for Microsoft (use 'common' for multi-tenant)") + + else: + errors.append(f"Unknown provider: {self.provider}") + + return errors + + def is_oauth_provider(self) -> bool: + """Check if this config uses OAuth authentication.""" + return self.provider in ('gmail-oauth', 'microsoft-oauth') + + def tokens_need_refresh(self) -> bool: + """ + Check if OAuth tokens need to be refreshed. + + Returns: + True if tokens are expired or will expire soon (within 5 minutes) + """ + if not self.is_oauth_provider(): + return False + + if not self.oauth_token_expiry: + # No expiry set, assume tokens are valid + return False + + try: + from datetime import datetime, timedelta, timezone + expiry = datetime.fromisoformat(self.oauth_token_expiry.replace('Z', '+00:00')) + now = datetime.now(timezone.utc) + # Refresh if expired or expiring within 5 minutes + return expiry <= now + timedelta(minutes=5) + except Exception: + # If we can't parse the expiry, assume we need to refresh + return True + + +class EmailProvider(ABC): + """ + Abstract base class for email providers. + + Each provider implements send() method for their specific API/protocol. + """ + + def __init__(self, config: EmailConfig): + self.config = config + validation_errors = config.validate() + if validation_errors: + raise ValueError(f"Invalid email configuration: {', '.join(validation_errors)}") + + @abstractmethod + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via provider. + + Args: + to: Recipient email address + subject: Email subject + body: Email body (plain text or HTML) + html: True if body is HTML + + Returns: + True if sent successfully, False otherwise + + Raises: + Exception: If send fails with unrecoverable error + """ + pass + + @abstractmethod + def test_connection(self) -> bool: + """ + Test connection to email provider. + + Returns: + True if connection successful, False otherwise + """ + pass + + +class SMTPEmailProvider(EmailProvider): + """ + SMTP email provider implementation. + + Supports standard SMTP with TLS/SSL for Gmail, Office 365, and other SMTP servers. + """ + + def __init__(self, config: EmailConfig): + super().__init__(config) + self._connection = None + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via SMTP. + + Args: + to: Recipient email address + subject: Email subject + body: Email body + html: True if body is HTML + + Returns: + True if sent successfully + + Raises: + smtplib.SMTPException: If SMTP operation fails + """ + try: + # Create message + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = f"{self.config.from_name} <{self.config.from_address}>" + msg['To'] = to + + # Attach body + if html: + part = MIMEText(body, 'html') + else: + part = MIMEText(body, 'plain') + msg.attach(part) + + # Connect and send + if self.config.smtp_use_ssl: + # Use SMTP_SSL for port 465 + context = ssl.create_default_context() + with smtplib.SMTP_SSL( + self.config.smtp_host, + self.config.smtp_port, + context=context + ) as server: + server.login(self.config.smtp_username, self.config.smtp_password) + server.send_message(msg) + else: + # Use SMTP with STARTTLS for port 587 + with smtplib.SMTP( + self.config.smtp_host, + self.config.smtp_port, + timeout=30 + ) as server: + server.ehlo() + if self.config.smtp_use_tls: + context = ssl.create_default_context() + server.starttls(context=context) + server.ehlo() + server.login(self.config.smtp_username, self.config.smtp_password) + server.send_message(msg) + + logging.info(f"[EMAIL] SMTP email sent to {to} via {self.config.smtp_host}") + return True + + except smtplib.SMTPAuthenticationError as e: + logging.error(f"[EMAIL] SMTP authentication failed: {e}") + raise + except smtplib.SMTPException as e: + logging.error(f"[EMAIL] SMTP error: {e}") + raise + except Exception as e: + logging.error(f"[EMAIL] Unexpected error sending email: {e}") + raise + + def test_connection(self) -> bool: + """ + Test SMTP connection and authentication. + + Returns: + True if connection successful + """ + try: + if self.config.smtp_use_ssl: + context = ssl.create_default_context() + with smtplib.SMTP_SSL( + self.config.smtp_host, + self.config.smtp_port, + context=context, + timeout=10 + ) as server: + server.login(self.config.smtp_username, self.config.smtp_password) + else: + with smtplib.SMTP( + self.config.smtp_host, + self.config.smtp_port, + timeout=10 + ) as server: + server.ehlo() + if self.config.smtp_use_tls: + context = ssl.create_default_context() + server.starttls(context=context) + server.ehlo() + server.login(self.config.smtp_username, self.config.smtp_password) + + logging.info(f"[EMAIL] SMTP connection test successful: {self.config.smtp_host}") + return True + + except Exception as e: + logging.error(f"[EMAIL] SMTP connection test failed: {e}") + return False + + +class SendGridEmailProvider(EmailProvider): + """ + SendGrid email provider implementation. + + Uses SendGrid HTTP API for sending emails. + """ + + def __init__(self, config: EmailConfig): + super().__init__(config) + # Import here to avoid dependency if not using SendGrid + try: + from sendgrid import SendGridAPIClient + from sendgrid.helpers.mail import Mail + self.SendGridAPIClient = SendGridAPIClient + self.Mail = Mail + except ImportError: + _, error_message = check_provider_dependencies('sendgrid') + raise ImportError(error_message) + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via SendGrid API. + + Args: + to: Recipient email address + subject: Email subject + body: Email body + html: True if body is HTML + + Returns: + True if sent successfully + """ + try: + message = self.Mail( + from_email=(self.config.from_address, self.config.from_name), + to_emails=to, + subject=subject, + html_content=body if html else None, + plain_text_content=body if not html else None + ) + + sg = self.SendGridAPIClient(self.config.sendgrid_api_key) + response = sg.send(message) + + if response.status_code in (200, 201, 202): + logging.info(f"[EMAIL] SendGrid email sent to {to}") + return True + else: + logging.error(f"[EMAIL] SendGrid returned status {response.status_code}") + return False + + except Exception as e: + logging.error(f"[EMAIL] SendGrid error: {e}") + raise + + def test_connection(self) -> bool: + """ + Test SendGrid API connection. + + Returns: + True if API key is valid + """ + try: + # SendGrid doesn't have a dedicated test endpoint + # We can verify the API key format and try to initialize the client + sg = self.SendGridAPIClient(self.config.sendgrid_api_key) + + # If we get here without exception, API key format is valid + # Note: This doesn't guarantee the key is active, but it's the best we can do + logging.info("[EMAIL] SendGrid API client initialized successfully") + return True + + except Exception as e: + logging.error(f"[EMAIL] SendGrid connection test failed: {e}") + return False + + +class SESEmailProvider(EmailProvider): + """ + AWS SES email provider implementation. + + Uses boto3 to send emails via Amazon Simple Email Service. + """ + + def __init__(self, config: EmailConfig): + super().__init__(config) + # Import here to avoid dependency if not using SES + try: + import boto3 + from botocore.exceptions import ClientError + self.boto3 = boto3 + self.ClientError = ClientError + except ImportError: + _, error_message = check_provider_dependencies('ses') + raise ImportError(error_message) + + # Initialize SES client + self.ses_client = self.boto3.client( + 'ses', + region_name=self.config.aws_region, + aws_access_key_id=self.config.aws_access_key, + aws_secret_access_key=self.config.aws_secret_key + ) + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via AWS SES. + + Args: + to: Recipient email address + subject: Email subject + body: Email body + html: True if body is HTML + + Returns: + True if sent successfully + """ + try: + if html: + body_part = {'Html': {'Charset': 'UTF-8', 'Data': body}} + else: + body_part = {'Text': {'Charset': 'UTF-8', 'Data': body}} + + response = self.ses_client.send_email( + Source=f"{self.config.from_name} <{self.config.from_address}>", + Destination={'ToAddresses': [to]}, + Message={ + 'Subject': {'Charset': 'UTF-8', 'Data': subject}, + 'Body': body_part + } + ) + + message_id = response.get('MessageId') + logging.info(f"[EMAIL] SES email sent to {to}, MessageId: {message_id}") + return True + + except self.ClientError as e: + error_code = e.response['Error']['Code'] + error_message = e.response['Error']['Message'] + + if error_code == 'MessageRejected': + logging.error(f"[EMAIL] SES rejected message: {error_message}") + elif error_code == 'ConfigurationSetDoesNotExist': + logging.error(f"[EMAIL] SES configuration error: {error_message}") + else: + logging.error(f"[EMAIL] SES error ({error_code}): {error_message}") + + raise + + except Exception as e: + logging.error(f"[EMAIL] SES unexpected error: {e}") + raise + + def test_connection(self) -> bool: + """ + Test AWS SES connection and verify email address. + + Returns: + True if connection successful and sender email verified + """ + try: + # Check if sender email is verified in SES + response = self.ses_client.get_identity_verification_attributes( + Identities=[self.config.from_address] + ) + + attributes = response.get('VerificationAttributes', {}) + status = attributes.get(self.config.from_address, {}).get('VerificationStatus') + + if status == 'Success': + logging.info(f"[EMAIL] SES connection test successful, {self.config.from_address} is verified") + return True + else: + logging.warning( + f"[EMAIL] SES email {self.config.from_address} not verified. " + f"Status: {status}. Emails may fail to send." + ) + return False + + except self.ClientError as e: + logging.error(f"[EMAIL] SES connection test failed: {e}") + return False + + except Exception as e: + logging.error(f"[EMAIL] SES unexpected error: {e}") + return False + + +class GmailOAuthProvider(EmailProvider): + """ + Gmail OAuth email provider implementation. + + Uses Gmail API with OAuth 2.0 authentication instead of SMTP. + Automatically refreshes expired tokens. + """ + + def __init__(self, config: EmailConfig): + """ + Initialize Gmail OAuth provider. + + Args: + config: EmailConfig with OAuth credentials + """ + super().__init__(config) + + # Import Gmail API dependencies + try: + from google.auth.transport.requests import Request + from google.oauth2.credentials import Credentials + from googleapiclient.discovery import build + + self.Request = Request + self.Credentials = Credentials + self.build = build + except ImportError as e: + _, error_message = check_provider_dependencies('gmail-oauth') + raise ImportError(error_message) from e + + self.credentials = self._load_credentials() + self.service = None + + def _load_credentials(self): + """Load OAuth credentials from config.""" + from datetime import datetime + + if not self.config.oauth_access_token: + raise ValueError("Gmail OAuth access token is required") + + # Parse token expiry + token_expiry = None + if self.config.oauth_token_expiry: + try: + # Parse as timezone-aware datetime + expiry_aware = datetime.fromisoformat( + self.config.oauth_token_expiry.replace('Z', '+00:00') + ) + # Convert to naive UTC datetime (Google's library expects naive datetimes) + token_expiry = expiry_aware.replace(tzinfo=None) + except Exception: + pass + + # Create credentials object + creds = self.Credentials( + token=self.config.oauth_access_token, + refresh_token=self.config.oauth_refresh_token, + token_uri='https://oauth2.googleapis.com/token', + client_id=self.config.oauth_client_id, + client_secret=self.config.oauth_client_secret, + scopes=['https://www.googleapis.com/auth/gmail.send'] + ) + + # Set expiry if available (as naive UTC datetime) + if token_expiry: + creds.expiry = token_expiry + + return creds + + def _refresh_if_expired(self): + """Refresh tokens if expired.""" + if self.credentials.expired or not self.credentials.valid: + logging.info("[EMAIL] Gmail OAuth tokens expired, refreshing...") + self.credentials.refresh(self.Request()) + + # Update config with new tokens + self.config.oauth_access_token = self.credentials.token + if self.credentials.refresh_token: + self.config.oauth_refresh_token = self.credentials.refresh_token + if self.credentials.expiry: + self.config.oauth_token_expiry = self.credentials.expiry.isoformat() + + # Mark tokens as updated so caller can persist them + self.config._oauth_tokens_updated = True + + logging.info("[EMAIL] Gmail OAuth tokens refreshed successfully") + + def _get_service(self): + """Get or create Gmail API service.""" + if not self.service: + self._refresh_if_expired() + self.service = self.build('gmail', 'v1', credentials=self.credentials) + return self.service + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via Gmail API. + + Args: + to: Recipient email address + subject: Email subject + body: Email body (HTML or plain text) + html: True if body is HTML + + Returns: + True if sent successfully + """ + try: + import base64 + from email.mime.text import MIMEText + + # Create message + message = MIMEText(body, 'html' if html else 'plain') + message['to'] = to + message['from'] = f"{self.config.from_name} <{self.config.from_address}>" + message['subject'] = subject + + # Encode message + raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8') + + # Send via Gmail API + service = self._get_service() + service.users().messages().send( + userId='me', + body={'raw': raw_message} + ).execute() + + logging.info(f"[EMAIL] Gmail OAuth email sent to {to}") + return True + + except Exception as e: + logging.error(f"[EMAIL] Gmail OAuth error: {e}") + raise + + def test_connection(self) -> bool: + """ + Test Gmail OAuth connection. + + Returns: + True if credentials are valid + """ + try: + # Refresh tokens if needed + self._refresh_if_expired() + + # Verify credentials are valid by checking they're not expired + # Note: gmail.send scope doesn't allow reading profile/labels + # so we just verify the credentials object is valid + if self.credentials and self.credentials.valid: + logging.info(f"[EMAIL] Gmail OAuth connection successful: {self.config.from_address}") + return True + else: + logging.error("[EMAIL] Gmail OAuth credentials are invalid") + return False + + except Exception as e: + logging.error(f"[EMAIL] Gmail OAuth connection test failed: {e}") + return False + + +class MicrosoftOAuthProvider(EmailProvider): + """ + Microsoft OAuth email provider implementation. + + Uses Microsoft Graph API with OAuth 2.0 authentication. + Supports Microsoft 365, Outlook.com, and organizational accounts. + """ + + def __init__(self, config: EmailConfig): + """ + Initialize Microsoft OAuth provider. + + Args: + config: EmailConfig with OAuth credentials and tenant_id + """ + super().__init__(config) + + # Import msal dependency + try: + import msal + self.msal = msal + except ImportError as e: + _, error_message = check_provider_dependencies('microsoft-oauth') + raise ImportError(error_message) from e + + if not self.config.oauth_tenant_id: + raise ValueError("Microsoft OAuth requires tenant_id (use 'common' for multi-tenant)") + + # Build MSAL confidential client app + self.app = self._build_msal_app() + self.token_cache = {} + + def _build_msal_app(self): + """Build MSAL confidential client application.""" + authority = f"https://login.microsoftonline.com/{self.config.oauth_tenant_id}" + + return self.msal.ConfidentialClientApplication( + client_id=self.config.oauth_client_id, + client_credential=self.config.oauth_client_secret, + authority=authority + ) + + def _get_access_token(self) -> str: + """ + Get valid access token, refreshing if necessary. + + Returns: + Valid access token + """ + # Check if we have cached token and it's still valid + if self.config.oauth_access_token and not self.config.tokens_need_refresh(): + return self.config.oauth_access_token + + # Need to refresh token + if not self.config.oauth_refresh_token: + raise ValueError("OAuth refresh token is required to refresh access token") + + logging.info("[EMAIL] Microsoft OAuth tokens expired, refreshing...") + + # Acquire token by refresh token + result = self.app.acquire_token_by_refresh_token( + refresh_token=self.config.oauth_refresh_token, + scopes=['https://graph.microsoft.com/Mail.Send'] + ) + + if 'access_token' in result: + # Update config with new tokens + self.config.oauth_access_token = result['access_token'] + + # Update refresh token if new one provided + if 'refresh_token' in result: + self.config.oauth_refresh_token = result['refresh_token'] + + # Calculate and update expiry + if 'expires_in' in result: + from datetime import datetime, timedelta, timezone + expiry = datetime.now(timezone.utc) + timedelta(seconds=result['expires_in']) + self.config.oauth_token_expiry = expiry.isoformat() + + # Mark tokens as updated so caller can persist them + self.config._oauth_tokens_updated = True + + logging.info("[EMAIL] Microsoft OAuth tokens refreshed successfully") + return result['access_token'] + else: + error = result.get('error', 'Unknown error') + error_desc = result.get('error_description', '') + raise Exception(f"Failed to refresh Microsoft OAuth token: {error} - {error_desc}") + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via Microsoft Graph API. + + Args: + to: Recipient email address + subject: Email subject + body: Email body (HTML or plain text) + html: True if body is HTML + + Returns: + True if sent successfully + """ + try: + import requests + + # Get valid access token + access_token = self._get_access_token() + + # Build Graph API request + url = 'https://graph.microsoft.com/v1.0/me/sendMail' + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + + # Construct email message + message = { + 'message': { + 'subject': subject, + 'body': { + 'contentType': 'HTML' if html else 'Text', + 'content': body + }, + 'toRecipients': [ + { + 'emailAddress': { + 'address': to + } + } + ], + 'from': { + 'emailAddress': { + 'name': self.config.from_name, + 'address': self.config.from_address + } + } + }, + 'saveToSentItems': 'true' + } + + # Send request + response = requests.post(url, headers=headers, json=message) + + if response.status_code == 202: + logging.info(f"[EMAIL] Microsoft Graph email sent to {to}") + return True + else: + logging.error(f"[EMAIL] Microsoft Graph returned status {response.status_code}: {response.text}") + return False + + except Exception as e: + logging.error(f"[EMAIL] Microsoft OAuth error: {e}") + raise + + def test_connection(self) -> bool: + """ + Test Microsoft OAuth connection. + + Returns: + True if credentials are valid + """ + try: + import requests + + # Get valid access token + access_token = self._get_access_token() + + # Try to get user profile to verify connection + url = 'https://graph.microsoft.com/v1.0/me' + headers = { + 'Authorization': f'Bearer {access_token}' + } + + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + email = data.get('mail') or data.get('userPrincipalName') + logging.info(f"[EMAIL] Microsoft OAuth connection successful: {email}") + return True + else: + logging.error(f"[EMAIL] Microsoft OAuth connection test failed: {response.status_code}") + return False + + except Exception as e: + logging.error(f"[EMAIL] Microsoft OAuth connection test failed: {e}") + return False + + +class EmailSender: + """ + Main email sender class that routes to appropriate provider. + + Usage: + config = EmailConfig(...) + sender = EmailSender(config) + sender.send(to='user@example.com', subject='Test', body='Hello', html=True) + """ + + def __init__(self, config: EmailConfig): + """ + Initialize email sender with configuration. + + Args: + config: EmailConfig object + + Raises: + ValueError: If provider is unknown or config invalid + """ + self.config = config + + # Check provider compatibility with current installation + dependencies_available, error_message = check_provider_dependencies(config.provider) + if not dependencies_available: + raise ValueError(error_message) + + # Create provider instance + provider_map = { + 'smtp': SMTPEmailProvider, + 'sendgrid': SendGridEmailProvider, + 'ses': SESEmailProvider, + 'gmail-oauth': GmailOAuthProvider, + 'microsoft-oauth': MicrosoftOAuthProvider, + } + + provider_class = provider_map.get(config.provider.lower()) + if not provider_class: + raise ValueError( + f"Unknown email provider: {config.provider}. " + f"Supported: {', '.join(provider_map.keys())}" + ) + + self.provider = provider_class(config) + + def send(self, to: str, subject: str, body: str, html: bool = False) -> bool: + """ + Send email via configured provider. + + Args: + to: Recipient email address + subject: Email subject + body: Email body + html: True if body is HTML + + Returns: + True if sent successfully + + Raises: + Exception: If send fails + """ + logging.info(f"[EMAIL] Sending email to {to} via {self.config.provider}") + return self.provider.send(to, subject, body, html) + + def test_connection(self) -> bool: + """ + Test connection to email provider. + + Returns: + True if connection successful + """ + return self.provider.test_connection() + + + +def find_email_config_record(vault: vault_online.VaultOnline, name: str) -> Optional[str]: + """ + Find email config record by name. + + Args: + vault: VaultOnline session + name: Name of email configuration + + Returns: + Record UID if found, None otherwise + """ + for record_uid in vault.vault_data._records: + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.TypedRecord): + continue + if record.record_type != 'login': + continue + + try: + record_type = vault.vault_data.get_record_type_by_name(record.record_type) + record_dict = vault_extensions.extract_typed_record_data(record, record_type) + custom_fields = record_dict.get('custom', []) + for field in custom_fields: + if field.get('type') == 'text' and field.get('label') == '__email_config__': + if record.title == name: + return record_uid + except: + continue + + return None + + +def load_email_config_from_record(vault: vault_online.VaultOnline, record_uid: str) -> EmailConfig: + """ + Load EmailConfig from a Keeper record. + + Args: + vault: VaultOnline session + record_uid: Record UID + + Returns: + EmailConfig object + + Raises: + CommandError: If record not found or invalid + """ + if record_uid not in vault.vault_data.records: + raise ValueError(f'Record {record_uid} not found') + + 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') + + record_type = vault.vault_data.get_record_type_by_name(record.record_type) + record_dict = vault_extensions.extract_typed_record_data(record, record_type) + + fields = record_dict.get('fields', []) + login = None + password = None + + for field in fields: + if field.get('type') == 'login': + values = field.get('value', []) + if values: + login = values[0] + elif field.get('type') == 'password': + values = field.get('value', []) + if values: + password = values[0] + + custom_fields = record_dict.get('custom', []) + provider_data = {} + + for field in custom_fields: + label = field.get('label', '') + if label.startswith('__email_'): + continue + + values = field.get('value', []) + if values: + provider_data[label] = values[0] + + provider = provider_data.get('provider', 'smtp') + + config = EmailConfig( + record_uid=record_uid, + name=record.title, + provider=provider, + from_address=provider_data.get('from_address', ''), + from_name=provider_data.get('from_name', 'Keeper Commander') + ) + + if provider == 'smtp': + config.smtp_host = provider_data.get('smtp_host') + config.smtp_port = int(provider_data.get('smtp_port', 587)) + config.smtp_username = login or provider_data.get('smtp_username') + config.smtp_password = password or provider_data.get('smtp_password') + config.smtp_use_tls = provider_data.get('smtp_use_tls', 'true').lower() == 'true' + config.smtp_use_ssl = provider_data.get('smtp_use_ssl', 'false').lower() == 'true' + + elif provider == 'ses': + config.aws_region = provider_data.get('aws_region') + config.aws_access_key = login or provider_data.get('aws_access_key') + config.aws_secret_key = password or provider_data.get('aws_secret_key') + + elif provider == 'sendgrid': + config.sendgrid_api_key = password or provider_data.get('sendgrid_api_key') + + elif provider in ('gmail-oauth', 'microsoft-oauth'): + + config.oauth_access_token = login + config.oauth_refresh_token = password + + config.oauth_client_id = provider_data.get('oauth_client_id') + config.oauth_client_secret = provider_data.get('oauth_client_secret') + config.oauth_token_expiry = provider_data.get('oauth_token_expiry') + + if provider == 'microsoft-oauth': + config.oauth_tenant_id = provider_data.get('oauth_tenant_id', 'common') + + return config + + +def build_onboarding_email( + share_url: str, + custom_message: str, + record_title: str, + expiration: Optional[str] = None +) -> str: + """ + Build HTML email for onboarding with one-time share link. + + Args: + share_url: One-time share URL + custom_message: Custom message from administrator + record_title: Title of the record being shared + expiration: Human-readable expiration time (e.g., "24 hours", "1 day") + + Returns: + HTML email body + + Raises: + FileNotFoundError: If email template not found + """ + template = load_email_template('onboarding.html') + + expiration_text = ( + f"This link will expire in {expiration}" + if expiration + else "This link will expire after first use" + ) + + html = template.format( + custom_message=custom_message, + share_url=share_url, + record_title=record_title, + expiration_text=expiration_text + ) + + return html + + +def load_email_template(template_name: str = 'onboarding.html') -> str: + """ + Load email template from resources directory. + + Args: + template_name: Name of template file (default: onboarding.html) + + Returns: + Template content as string + + Raises: + FileNotFoundError: If template file doesn't exist + """ + module_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(module_dir, 'resources', 'email_templates', template_name) + + if not os.path.exists(template_path): + raise FileNotFoundError(f"Email template not found: {template_path}") + + with open(template_path, 'r', encoding='utf-8') as f: + return f.read() + + +def validate_email_provider_dependencies(provider: str) -> tuple[bool, Optional[str]]: + """ + Validate that required dependencies for an email provider are installed. + + This function checks if dependencies are available WITHOUT creating the provider instance, + allowing early validation before performing operations like password rotation. + + Args: + provider: Email provider name ('smtp', 'sendgrid', 'ses', 'gmail-oauth', 'microsoft-oauth') + + Returns: + Tuple of (is_valid, error_message): + - is_valid: True if dependencies are available, False otherwise + - error_message: None if valid, otherwise contains install instructions + + Examples: + >>> valid, error = validate_email_provider_dependencies('gmail-oauth') + >>> if not valid: + ... print(error) + Gmail OAuth requires google-api-python-client and related libraries. + Install with: pip install keepercommander[email-gmail-oauth] + """ + provider = provider.lower() + + # SMTP uses Python built-ins, no extra dependencies needed + if provider == 'smtp': + return True, None + + # SendGrid + if provider == 'sendgrid': + try: + import sendgrid + return True, None + except ImportError: + return False, ( + "SendGrid email provider requires the 'sendgrid' library.\n" + "Install with: pip install keepercommander[email-sendgrid]\n" + "Or install manually: pip install sendgrid>=6.10.0" + ) + + # AWS SES + if provider == 'ses': + try: + import boto3 + return True, None + except ImportError: + return False, ( + "AWS SES email provider requires the 'boto3' library.\n" + "Install with: pip install keepercommander[email-ses]\n" + "Or install manually: pip install boto3>=1.26.0" + ) + + if provider == 'gmail-oauth': + try: + import google.auth + import google.auth.transport.requests + import google.oauth2.credentials + import googleapiclient.discovery + return True, None + except ImportError: + return False, ( + "Gmail OAuth email provider requires Google API libraries.\n" + "Install with: pip install keepercommander[email-gmail-oauth]\n" + "Or install manually: pip install google-api-python-client google-auth google-auth-oauthlib google-auth-httplib2" + ) + + # Microsoft OAuth + if provider == 'microsoft-oauth': + try: + import msal + return True, None + except ImportError: + return False, ( + "Microsoft OAuth email provider requires the 'msal' library.\n" + "Install with: pip install keepercommander[email-microsoft-oauth]\n" + "Or install manually: pip install msal>=1.20.0" + ) + + # Unknown provider + return False, ( + f"Unknown email provider: {provider}\n" + f"Supported providers: smtp, sendgrid, ses, gmail-oauth, microsoft-oauth" + ) + + +def update_oauth_tokens_in_record(vault: vault_online.VaultOnline, record_uid: str, + access_token: str, refresh_token: str, + token_expiry: str) -> None: + """ + Update OAuth tokens in email config record after automatic refresh. + + This function is called by OAuth email providers (GmailOAuthProvider, + MicrosoftOAuthProvider) after they automatically refresh expired tokens. + It updates the Keeper record to persist the new tokens. + + Args: + vault: VaultOnline session + record_uid: UID of the email config record to update + access_token: New OAuth access token + refresh_token: New OAuth refresh token (may be same as old) + token_expiry: New token expiry in ISO 8601 format + + Raises: + CommandError: If record not found or update fails + """ + if record_uid not in vault.vault_data.records: + vault.sync_down(force=True) + + if record_uid not in vault.vault_data.records: + raise ValueError(f'Email configuration record not found: {record_uid}') + + record = vault.vault_data.load_record(record_uid) + if not isinstance(record, vault_record.TypedRecord): + raise ValueError(f'Record is not a TypedRecord: {record_uid}') + + for field in record.fields: + if field.type == 'login': + field.value = [access_token] + elif field.type == 'password': + field.value = [refresh_token] + + expiry_field_found = False + for field in record.custom: + if field.label == 'oauth_token_expiry': + field.value = [token_expiry] + expiry_field_found = True + break + + if not expiry_field_found: + record.custom.append(vault_record.TypedField.create_field(field_type='text', field_label='oauth_token_expiry', required=False)) + + record_management.update_record(vault, record) + + vault.sync_down(force=True) + + logging.debug(f'[EMAIL-CONFIG] Updated OAuth tokens for record: {record_uid}') + diff --git a/keepercli-package/src/keepercli/helpers/gateway_utils.py b/keepercli-package/src/keepercli/helpers/gateway_utils.py index be6194a9..c2a2e0df 100644 --- a/keepercli-package/src/keepercli/helpers/gateway_utils.py +++ b/keepercli-package/src/keepercli/helpers/gateway_utils.py @@ -1,8 +1,9 @@ from time import time from typing import List +from keepersdk import utils from keepersdk.vault import vault_online -from keepersdk.proto import pam_pb2 +from keepersdk.proto import pam_pb2, enterprise_pb2 from keepersdk.vault import ksm_management @@ -69,7 +70,8 @@ def create_gateway( first_access_expire_duration_ms=first_access_expire_duration_ms, access_expire_in_ms=None, master_key=master_key, - server=vault.keeper_auth.keeper_endpoint.server + server=vault.keeper_auth.keeper_endpoint.server, + client_type=enterprise_pb2.DISCOVERY_AND_ROTATION_CONTROLLER ) return _extract_one_time_token(one_time_token_dict) @@ -109,3 +111,23 @@ def set_gateway_max_instances(vault: vault_online.VaultOnline, gateway_uid: byte rest_endpoint=REST_ENDPOINT_SET_MAX_INSTANCE_COUNT, request=rq ) + + +def find_connected_gateways(all_controllers, identifier): + + found_connected_controller_uid_bytes = next((c for c in all_controllers if (utils.base64_url_encode(c) == identifier)), None) + + if found_connected_controller_uid_bytes: + return found_connected_controller_uid_bytes + else: + return None + + +def edit_gateway(vault: vault_online.VaultOnline, gateway_uid, gateway_name, node_id): + rq = pam_pb2.PAMController() + rq.controllerUid = gateway_uid + rq.controllerName = gateway_name + rq.nodeId = node_id + vault.keeper_auth.execute_auth_rest( + rest_endpoint='pam/modify_controller', + request=rq) diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index fb5e7bd5..02b8ba72 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -3,12 +3,15 @@ import fnmatch import hashlib import hmac +import json import re from typing import Iterator, List, Optional from urllib import parse -from keepersdk import utils +from keepersdk import crypto, utils from keepersdk.proto.enterprise_pb2 import GetSharingAdminsRequest, GetSharingAdminsResponse +from keepersdk.proto.router_pb2 import RouterRotationInfo +from keepersdk.proto.pam_pb2 import PAMGenericUidRequest from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils from ..commands.base import CommandError @@ -144,3 +147,34 @@ def resolve_record(context: KeeperParams, name: str) -> str: return uid if record_uid is None: raise CommandError(f'Record not found: {name}') + + +def record_rotation_get(vault: vault_online.VaultOnline, record_uid_bytes: bytes) -> RouterRotationInfo: + + rq = PAMGenericUidRequest() + rq.uid = record_uid_bytes + + rotation_info_rs = vault.keeper_auth.execute_auth_rest(rest_endpoint='pam/get_rotation_info', request=rq, response_type=RouterRotationInfo) + + return rotation_info_rs + + +def pam_configurations_get_all(vault: vault_online.VaultOnline): + all_records = vault.vault_data.records() + all_v6_records = [rec for rec in list(all_records) if rec.version == 6] + + return all_v6_records + + +def pam_decrypt_configuration_data(pam_config_v6_record): + data = pam_config_v6_record.get('data') + record_key_unencrypted = pam_config_v6_record.get('record_key_unencrypted') + data_unencrypted_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(data), + record_key_unencrypted + ) + + data_unencrypted_json_str = utils.base64_url_encode(data_unencrypted_bytes) + data_unencrypted_dict = json.loads(data_unencrypted_json_str) + + return data_unencrypted_dict diff --git a/keepercli-package/src/keepercli/helpers/router_utils.py b/keepercli-package/src/keepercli/helpers/router_utils.py index 873e78ce..8dc4df3c 100644 --- a/keepercli-package/src/keepercli/helpers/router_utils.py +++ b/keepercli-package/src/keepercli/helpers/router_utils.py @@ -1,12 +1,23 @@ +from datetime import datetime +import json import logging +import os import google +import requests from typing import Optional -from keepersdk.proto import pam_pb2 +from keepersdk import utils, crypto +from keepersdk.authentication import endpoint +from keepersdk.proto import pam_pb2, router_pb2 from keepersdk.vault import vault_online +from keepersdk.errors import KeeperApiError +from ..helpers import gateway_utils +from ..commands.pam.pam_dto import GatewayAction +from ..params import KeeperParams API_PATH_GET_CONTROLLERS = "get_controllers" +VERIFY_SSL = bool(os.environ.get("VERIFY_SSL", "TRUE") == "TRUE") def router_get_connected_gateways(vault: vault_online.VaultOnline) -> Optional[pam_pb2.PAMOnlineControllers]: @@ -27,3 +38,459 @@ def router_get_connected_gateways(vault: vault_online.VaultOnline) -> Optional[p return pam_online_controllers return None + + +def router_send_action_to_gateway(context: KeeperParams, gateway_action: GatewayAction, message_type, is_streaming, + destination_gateway_uid_str=None, gateway_timeout=15000, transmission_key=None, + encrypted_transmission_key=None, encrypted_session_token=None): + + krouter_host = context.auth.keeper_endpoint.get_router_server() + + try: + router_enterprise_controllers_connected = \ + [x.controllerUid for x in router_get_connected_gateways(context.vault).controllers] + + except requests.exceptions.ConnectionError as errc: + logging.info(f"Looks like router is down. Router URL [{krouter_host}]") + return + except Exception as e: + raise e + + if destination_gateway_uid_str: + destination_gateway_uid_bytes = utils.base64_url_decode(destination_gateway_uid_str) + + if destination_gateway_uid_bytes not in router_enterprise_controllers_connected: + logging.warning(f"\tThis Gateway currently is not online.") + return + else: + if not router_enterprise_controllers_connected or len(router_enterprise_controllers_connected) == 0: + logging.warning(f"\tNo running or connected Gateways in your enterprise. " + f"Start the Gateway before sending any action to it.") + return + elif len(router_enterprise_controllers_connected) == 1: + destination_gateway_uid_bytes = router_enterprise_controllers_connected[0] + destination_gateway_uid_str = utils.base64_url_encode(destination_gateway_uid_bytes) + else: + + if not gateway_action.gateway_destination: + logging.warning(f"There are more than one Gateways running in your enterprise. " + f"Only 'pam action rotate' is able to know " + f"which Gateway should receive a request. Any other commands should have a Gateway specified. " + f"See help for the command you are trying to use. To find connected gateways run action " + f"'pam gateway list' and provide Gateway UID or Gateway Name.") + + return + + destination_gateway_uid_bytes = gateway_utils.find_connected_gateways(router_enterprise_controllers_connected, gateway_action.gateway_destination) + destination_gateway_uid_str = utils.base64_url_encode(destination_gateway_uid_bytes) + + msg_id = gateway_action.conversationId if gateway_action.conversationId else GatewayAction.generate_conversation_id('true') + + rq = router_pb2.RouterControllerMessage() + rq.messageUid = utils.base64_url_decode(msg_id) if isinstance(msg_id, str) else msg_id + rq.controllerUid = destination_gateway_uid_bytes + rq.messageType = message_type + rq.streamResponse = is_streaming + rq.payload = gateway_action.toJSON().encode('utf-8') + rq.timeout = gateway_timeout + + if not transmission_key: + transmission_key = utils.generate_aes_key() + + response = router_send_message_to_gateway( + context=context, + transmission_key=transmission_key, + rq_proto=rq, + encrypted_transmission_key=encrypted_transmission_key, + encrypted_session_token=encrypted_session_token) + + rs_body = response.content + + if type(rs_body) == bytes: + router_response = router_pb2.RouterResponse() + router_response.ParseFromString(rs_body) + + rrc = router_pb2.RouterResponseCode.Name(router_response.responseCode) + if router_response.responseCode == router_pb2.RRC_OK: + logging.debug("Good response...") + + elif router_response.responseCode == router_pb2.RRC_BAD_STATE: + raise Exception(router_response.errorMessage + ' response code: ' + rrc) + + elif router_response.responseCode == router_pb2.RRC_TIMEOUT: + + raise Exception(router_response.errorMessage + ' response code: ' + rrc) + + elif router_response.responseCode == router_pb2.RRC_CONTROLLER_DOWN: + raise Exception(router_response.errorMessage + ' response code: ' + rrc) + + else: + raise Exception(router_response.errorMessage + ' response code: ' + rrc) + + + payload_encrypted = router_response.encryptedPayload + if payload_encrypted: + + payload_decrypted = crypto.decrypt_aes_v2(payload_encrypted, transmission_key) + + controller_response = pam_pb2.ControllerResponse() + controller_response.ParseFromString(payload_decrypted) + + gateway_response_payload = json.loads(controller_response.payload) + else: + gateway_response_payload = {} + + return { + 'response': gateway_response_payload + } + + +def router_send_message_to_gateway(context: KeeperParams, transmission_key, rq_proto, + encrypted_transmission_key=None, encrypted_session_token=None): + + krouter_host = context.auth.keeper_endpoint.get_router_server() + + if not encrypted_transmission_key: + server_public_key = endpoint.SERVER_PUBLIC_KEYS[context.auth.keeper_endpoint.server_key_id] + + if context.auth.keeper_endpoint.server_key_id < 7: + encrypted_transmission_key = crypto.encrypt_rsa(transmission_key, server_public_key) + else: + encrypted_transmission_key = crypto.encrypt_ec(transmission_key, server_public_key) + + encrypted_payload = b'' + + if rq_proto: + encrypted_payload = crypto.encrypt_aes_v2(rq_proto.SerializeToString(), transmission_key) + if not encrypted_session_token: + encrypted_session_token = crypto.encrypt_aes_v2(context.auth.auth_context.session_token, transmission_key) + + rs = requests.post( + krouter_host+"/api/user/send_controller_message", + verify=VERIFY_SSL, + + headers={ + 'TransmissionKey': utils.base64_url_encode(encrypted_transmission_key), + 'Authorization': f'KeeperUser {utils.base64_url_encode(encrypted_session_token)}', + }, + data=encrypted_payload if rq_proto else None + ) + + if rs.status_code >= 300: + raise Exception(str(rs.status_code) + ': error: ' + rs.reason + ', message: ' + rs.text) + + return rs + + +def print_router_response(router_response, response_type, original_conversation_id=None, is_verbose=False, gateway_uid=''): + if not router_response: + return + + router_response_response = router_response.get('response') + router_response_response_payload_str = router_response_response.get('payload') + router_response_response_payload_dict = json.loads(router_response_response_payload_str) + + if router_response_response_payload_dict.get('warnings'): + for w in router_response_response_payload_dict.get('warnings'): + if w: + logging.warning(f'{w}') + + if original_conversation_id: + + gateway_response_conversation_id = router_response_response_payload_dict.get('conversation_id', None) + oid = (utils.base64_url_decode(original_conversation_id) + if isinstance(original_conversation_id, str) + else original_conversation_id) + gid = (utils.base64_url_decode(gateway_response_conversation_id) + if isinstance(gateway_response_conversation_id, str) + else gateway_response_conversation_id) + + if oid != gid: + logging.error(f"Message ID that was sent to the server [{original_conversation_id}] and the conversation id " + f"received back [{gateway_response_conversation_id}] are different. That probably means that " + f"the gateway sent a wrong response that was not associated with the request.") + + if not (router_response_response_payload_dict.get('is_ok') or router_response_response_payload_dict.get('isOk')): + logging.error(f"{json.dumps(router_response_response_payload_dict, indent=4)}") + return + + if router_response_response_payload_dict.get('isScheduled') or router_response_response_payload_dict.get('is_scheduled'): + conversation_id = router_response_response_payload_dict.get('conversation_id') + + gwinfo = f" --gateway={gateway_uid}" if gateway_uid else "" + logging.info(f"Scheduled action id: {conversation_id}") + logging.info(f"The action has been scheduled, use command 'pam action job-info {conversation_id}{gwinfo}' to get status of the scheduled action") + return + + elif response_type == 'job_info': + job_info = router_response_response_payload_dict.get('data') + exec_response_value = job_info.get('execResponseValue') + exec_response_value_msg = exec_response_value.get('message') if exec_response_value else None + exec_response_value_logs = exec_response_value.get('execLog') if exec_response_value else None + exec_duration = job_info.get('executionDuration') + exec_status = job_info.get('status') + exec_exception = job_info.get('execException') + + logging.info(f'Execution Details\n-------------------------') + + logging.info(f'\tStatus : {job_info.get("reason") if job_info.get("reason") else exec_status}') + + if exec_duration: + logging.info(f'\tDuration : {exec_duration}') + + if exec_response_value_msg: + logging.info(f'\tResponse Message : {exec_response_value_msg}') + + if exec_response_value_logs: + logging.info(f'\tPost-execution scripts logs:') + for el in exec_response_value_logs: + logging.info(f'\t\tscript: {el.get("name")}') + logging.info(f'\t\treturn code: {el.get("return_code")}') + if el.get("stdout"): + logging.info(f'\t\tstdout:\n---\n{el.get("stdout")}\n---') + if el.get("stderr"): + logging.info(f'\t\tstderr:\n---\n{el.get("stderr")}\n---') + logging.info(f'\n') + + if exec_exception: + logging.info(f'\tExecution Exception : {exec_exception}') + + elif response_type == 'gateway_info': + + gateway_info = router_response_response_payload_dict.get('data') + + logging.info(f'\nGateway Details') + gateway_config = gateway_info.get('gateway-config', {}) + version_info = gateway_config.get('version', {}) + if version_info.get("current"): + logging.info(f'\tVersion : {version_info.get("current")}') + + started_time = gateway_config.get("connection_info", {}).get("started") + try: + if started_time: + started_dt = datetime.fromtimestamp(float(started_time)) + local_tz = datetime.now().astimezone().tzinfo + started_str = f"{started_dt.strftime('%Y-%m-%d %H:%M:%S')} {local_tz}" + logging.info(f'\tStarted Time : {started_str}') + except (ValueError, TypeError): + pass + + if gateway_config.get("ws_log_file"): + logging.info(f'\tLogs Location : {gateway_config.get("ws_log_file")}') + + machine_env = gateway_info.get('machine', {}).get('environment', {}) + if machine_env and machine_env.get('provider'): + logging.info(f'\nEnvironment Details') + logging.info(f'\tProvider : {machine_env.get("provider")}') + if machine_env.get('provider') != 'Local/Other': + if machine_env.get('account_id'): + logging.info(f'\tAccount : {machine_env.get("account_id")}') + if machine_env.get('region'): + logging.info(f'\tRegion : {machine_env.get("region")}') + if machine_env.get('instance_type'): + logging.info(f'\tInstance Type : {machine_env.get("instance_type")}') + + machine = gateway_info.get('machine', {}) + logging.info(f'\nMachine Details') + + if machine.get("hostname"): + logging.info(f'\tHostname : {machine.get("hostname")}') + if machine.get("ip_address_local") and machine.get("ip_address_local") != "unknown": + logging.info(f'\tIP (Local) : {machine.get("ip_address_local")}') + if machine.get("ip_address_external"): + logging.info(f'\tIP (External) : {machine.get("ip_address_external")}') + + os_info = [] + if machine.get("system"): os_info.append(machine.get("system")) + if machine.get("release"): os_info.append(machine.get("release")) + if os_info: + logging.info(f'\tOperating System : {" ".join(os_info)}') + + memory = machine.get('memory', {}) + if memory.get('free_gb') is not None and memory.get('total_gb') is not None: + logging.info(f'\tMemory : {memory.get("free_gb")}GB free / {memory.get("total_gb")}GB total') + + installed_packages = { + pkg.split('==')[0]: pkg.split('==')[1] + for pkg in machine.get('installed-python-packages', []) + } + + core_packages = [ + ('KDNRM', installed_packages.get('kdnrm')), + ('Keeper GraphSync', installed_packages.get('keeper-dag')), + ('Discovery Common', installed_packages.get('discovery-common')) + ] + + if any(version for _, version in core_packages): + logging.info(f'\nCore Components') + for name, version in core_packages: + if version: + logging.info(f'\t{name:<16} : {version}') + + # KSM Details + logging.info(f'\nKSM Application Details') + ksm_app = gateway_info.get('ksm', {}).get('app', {}) + + if ksm_app.get("title"): + logging.info(f'\tTitle : {ksm_app.get("title")}') + if ksm_app.get("records-count") is not None: + logging.info(f'\tRecords Count : {ksm_app.get("records-count")}') + if ksm_app.get("folders-count") is not None: + logging.info(f'\tFolders Count : {ksm_app.get("folders-count")}') + if ksm_app.get("expires-on"): + logging.info(f'\tExpires On : {ksm_app.get("expires-on")}') + logging.info(f'\tWarnings : {ksm_app.get("warnings") or "None"}') + + # Router Details + logging.info(f'\nRouter Connection') + router_conn = gateway_info.get('router', {}).get('connection', {}) + if router_conn.get("base-url"): + logging.info(f'\tURL : {router_conn.get("base-url")}') + router_status = router_conn.get("status", "UNKNOWN").lower() + logging.info(f'\tStatus : {router_status}') + + # PAM Configurations + logging.info(f'\nPAM Configurations Accessible to this Gateway') + pam_configs = gateway_info.get('pam_configurations', []) + if pam_configs: + for idx, config in enumerate(pam_configs, 1): + logging.info(f'\t{idx}. {config}') + else: + logging.info(f'\tNo PAM Configurations found') + + # Additional details for verbose mode + if is_verbose: + logging.info(f'\nAdditional Details') + if machine.get("working-dir"): + logging.info(f'\tWorking Directory : {machine.get("working-dir")}') + if machine.get("package-dir"): + logging.info(f'\tPackage Directory: {machine.get("package-dir")}') + if machine.get("executable"): + logging.info(f'\tPython Executable: {machine.get("executable")}') + + if machine.get('installed-python-packages'): + logging.info(f'\nInstalled Python Packages') + for package in sorted(machine.get('installed-python-packages', [])): + logging.info(f'\t{package}') + + +def get_response_payload(router_response): + + router_response_response = router_response.get('response') + router_response_response_payload_str = router_response_response.get('payload') + router_response_response_payload_dict = json.loads(router_response_response_payload_str) + + return router_response_response_payload_dict + +def router_get_rotation_schedules(vault: vault_online, proto_request): + return _post_request_to_router(vault, 'get_rotation_schedules', rq_proto=proto_request, rs_type=pam_pb2.PAMRotationSchedulesResponse) + +def _post_request_to_router(vault: vault_online.VaultOnline, path, rq_proto=None, rs_type=None, method='post', + raw_without_status_check_response=False, query_params=None, transmission_key=None, + encrypted_transmission_key=None, encrypted_session_token=None): + krouter_host = vault.keeper_auth.keeper_endpoint.get_router_server() + path = '/api/user/' + path + + if not transmission_key: + transmission_key = utils.generate_aes_key() + if not encrypted_transmission_key: + server_public_key = endpoint.SERVER_PUBLIC_KEYS[vault.keeper_auth.keeper_endpoint.server_key_id] + + if vault.keeper_auth.keeper_endpoint.server_key_id < 7: + encrypted_transmission_key = crypto.encrypt_rsa(transmission_key, server_public_key) + else: + encrypted_transmission_key = crypto.encrypt_ec(transmission_key, server_public_key) + + encrypted_payload = b'' + + if rq_proto: + if logging.getLogger().level <= logging.DEBUG: + js = google.protobuf.json_format.MessageToJson(rq_proto) + logging.debug('>>> [GW RQ] %s: %s', path, js) + encrypted_payload = crypto.encrypt_aes_v2(rq_proto.SerializeToString(), transmission_key) + + if not encrypted_session_token: + encrypted_session_token = crypto.encrypt_aes_v2(vault.keeper_auth.auth_context.session_token, transmission_key) + + try: + rs = requests.request(method, + krouter_host + path, + params=query_params, + verify=VERIFY_SSL, + headers={ + 'TransmissionKey': utils.base64_url_encode(encrypted_transmission_key), + 'Authorization': f'KeeperUser {utils.base64_url_encode(encrypted_session_token)}' + }, + data=encrypted_payload if rq_proto else None + ) + except ConnectionError as e: + raise KeeperApiError('-1', f"KRouter is not reachable on '{krouter_host}'. Error: ${e}") + except Exception as ex: + raise ex + + content_type = rs.headers.get('Content-Type') or '' + + if raw_without_status_check_response: + return rs + + if rs.status_code < 400: + if content_type == 'application/json': + return rs.json() + + rs_body = rs.content + if isinstance(rs_body, bytes): + router_response = router_pb2.RouterResponse() + router_response.ParseFromString(rs_body) + + rrc = router_pb2.RouterResponseCode.Name(router_response.responseCode) + if router_response.responseCode != router_pb2.RRC_OK: + raise Exception(router_response.errorMessage + ' Response code: ' + rrc) + + if router_response.encryptedPayload: + payload_encrypted = router_response.encryptedPayload + payload_decrypted = crypto.decrypt_aes_v2(payload_encrypted, transmission_key) + else: + payload_decrypted = None + + if rs_type: + if payload_decrypted: + rs_proto = rs_type() + rs_proto.ParseFromString(payload_decrypted) + if logging.getLogger().level <= logging.DEBUG: + js = google.protobuf.json_format.MessageToJson(rs_proto) + logging.debug('>>> [GW RS] %s: %s', 'get_rotation_schedules', js) + return rs_proto + else: + return None + + return payload_decrypted + + return rs_body + else: + raise KeeperApiError(rs.status_code.__str__(), rs.text) + + +def router_set_record_rotation_information(vault: vault_online.VaultOnline, proto_request, transmission_key=None, + encrypted_transmission_key=None, encrypted_session_token=None): + rs = _post_request_to_router(vault, 'set_record_rotation', proto_request, transmission_key=transmission_key, + encrypted_transmission_key=encrypted_transmission_key, + encrypted_session_token=encrypted_session_token) + + return rs + + +def router_configure_resource(vault: vault_online.VaultOnline, proto_request, transmission_key=None, + encrypted_transmission_key=None, encrypted_session_token=None): + rs = _post_request_to_router(vault, 'configure_resource', proto_request, transmission_key=transmission_key, + encrypted_transmission_key=encrypted_transmission_key, + encrypted_session_token=encrypted_session_token) + + return rs + + +def encrypt_pwd_complexity(rule_list_dict, record_key_unencrypted): + rule_list_json = json.dumps(rule_list_dict) + rule_list_json_bytes = rule_list_json.encode('UTF-8') + rule_list_json_encrypted = crypto.encrypt_aes_v2(rule_list_json_bytes, record_key_unencrypted) + + return rule_list_json_encrypted \ No newline at end of file diff --git a/keepercli-package/src/keepercli/params.py b/keepercli-package/src/keepercli/params.py index 421e9dc0..bde54e08 100644 --- a/keepercli-package/src/keepercli/params.py +++ b/keepercli-package/src/keepercli/params.py @@ -4,10 +4,12 @@ import threading from typing import Dict, Optional, Any, Type +from keepersdk import utils as keepersdk_utils from keepersdk.authentication import configuration, endpoint, keeper_auth from keepersdk.enterprise import sqlite_enterprise_storage, enterprise_types, enterprise_loader from keepersdk.vault import vault_online, sqlite_storage from keepersdk.plugins.pedm import admin_plugin +from keepersdk.plugins.pam import pam_plugin, pam_types class KeeperConfig(configuration.IConfigurationStorage): @@ -162,6 +164,8 @@ def __init__(self, keeper_config: KeeperConfig): self._pedm_plugin: Optional[admin_plugin.PedmPlugin] = None + self._pam_plugin: Optional[pam_plugin.PamPlugin] = None + @property def keeper_config(self) -> KeeperConfig: return self._keeper_config @@ -181,6 +185,9 @@ def clear_session(self) -> None: self._pedm_plugin.close() self._pedm_plugin = None + if self._pam_plugin: + self._pam_plugin = None + if self._enterprise_loader: self._enterprise_loader = None @@ -236,9 +243,36 @@ def pedm_plugin(self) -> admin_plugin.PedmPlugin: self._pedm_plugin.sync_down() return self._pedm_plugin + @property + def pam_plugin(self) -> pam_plugin.PamPlugin: + assert self._enterprise_loader is not None + if not self._pam_plugin: + self._pam_plugin = pam_plugin.PamPlugin(self._enterprise_loader) + return self._pam_plugin + def vault_down(self): if self._vault: self._vault.sync_down() + self.refresh_record_rotations() + + def refresh_record_rotations(self) -> None: + """Reload vault ``recordRotations`` into :attr:`pam_plugin` after a vault sync (enterprise admins only).""" + if not self._auth or not self._auth.auth_context.is_enterprise_admin: + return + if self._enterprise_loader is None: + return + try: + self.pam_plugin.sync_record_rotations_from_vault() + except Exception as e: + keepersdk_utils.get_logger().warning('refresh_record_rotations failed: %s', e) + + def get_record_rotation(self, record_uid: str) -> Optional[pam_types.PamRecordRotationInfo]: + """Rotation metadata for ``record_uid`` from the last vault / PAM rotation sync (or ``None``).""" + if not self._auth or not self._auth.auth_context.is_enterprise_admin: + return None + if self._enterprise_loader is None: + return None + return self.pam_plugin.record_rotations.get_entity(record_uid) def enterprise_down(self): if self._auth and self._enterprise_loader: diff --git a/keepersdk-package/requirements.txt b/keepersdk-package/requirements.txt index c767bfdf..cba5d495 100644 --- a/keepersdk-package/requirements.txt +++ b/keepersdk-package/requirements.txt @@ -5,3 +5,4 @@ protobuf>=5.28.3 websockets>=13.1 fido2>=2.0.0; python_version>='3.10' email-validator>=2.0.0 +pydantic>=2.6.4; python_version>='3.8' diff --git a/keepersdk-package/src/keepersdk/helpers/config_utils.py b/keepersdk-package/src/keepersdk/helpers/config_utils.py new file mode 100644 index 00000000..e1bb9281 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/config_utils.py @@ -0,0 +1,39 @@ +from ..proto import pam_pb2 +from ..vault import vault_extensions, vault_online, vault_record +from .. import utils, crypto + +def pam_configuration_create_record_v6(vault: vault_online.VaultOnline, record: vault_record.TypedRecord, folder_uid: str): + if not record.record_uid: + record.record_uid = utils.generate_uid() + + record_key = vault.vault_data.get_record_key(record.record_uid) + if not record_key: + record_key = utils.generate_aes_key() + + schema = vault.vault_data.get_record_type_by_name(record.record_type) + record_data = vault_extensions.extract_typed_record_data(record, schema) + json_data = vault_extensions.get_padded_json_bytes(record_data) + + car = pam_pb2.ConfigurationAddRequest() + car.configurationUid = utils.base64_url_decode(record.record_uid) + car.recordKey = crypto.encrypt_aes_v2(record_key, vault.keeper_auth.auth_context.data_key) + car.data = crypto.encrypt_aes_v2(json_data, record_key) + + vault.keeper_auth.execute_auth_rest('pam/add_configuration_record', car) + + +def configuration_controller_get(vault: vault_online.VaultOnline, config_uid_bytes: bytes): + """ + Get the Controller UID that has access to the configuration UID + Retrieves a keeper.pam_controller record, from given configuration_uid provided in request. + controller_uid is the UID of the user who has access to the configuration url_safe_str_to_bytes(config_uid) + """ + rq = pam_pb2.PAMGenericUidRequest() + rq.uid = config_uid_bytes + + config_info_rs = vault.keeper_auth.execute_auth_rest('pam/get_configuration_controller', rq, response_type=pam_pb2.PAMController) + + if config_info_rs: + return config_info_rs + else: + return None diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/__init__.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__init__.py new file mode 100644 index 00000000..84f1672d --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__init__.py @@ -0,0 +1,302 @@ + +import csv +from enum import Enum +import logging +import sys +import time +import os +from pydantic import BaseModel +from typing import Any, Dict, Optional, Tuple, Union + +from . import dag_utils, dag_crypto, exceptions +from .dag_types import SyncQuery +from .__version__ import __version__ as dag_version + +from ...proto import router_pb2, GraphSync_pb2 + +class ConnectionBase: + + ADD_DATA = "/add_data" + SYNC = "/sync" + + TIMEOUT = 30 + + def __init__(self, + is_device: bool, + logger: Optional[logging.Logger] = None, + log_transactions: Optional[bool] = None, + log_transactions_dir: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = False): + + self.is_device = is_device + + if logger is None: + logger = logging.getLogger() + self.logger = logger + + if log_transactions is not None: + self.log_transactions = dag_utils.value_to_boolean(log_transactions) + else: + self.log_transactions: bool = dag_utils.value_to_boolean(os.environ.get("GS_LOG_TRANS", False)) + + self.log_transactions_dir = os.environ.get("GS_LOG_TRANS_DIR", log_transactions_dir) + if self.log_transactions_dir is None: + self.log_transactions_dir = "." + + if self.log_transactions is True: + self.logger.info("keeper-dag transaction logging is ENABLED; " + f"write directory at {self.log_transactions_dir}") + + self.use_read_protobuf = use_read_protobuf + self.use_write_protobuf = use_write_protobuf + + self.transmission_key = None + + def close(self): + if hasattr(self, "logger"): + self.logger = None + del self.logger + + def __del__(self): + self.close() + + def log_transaction_path(self, file: str): + return os.path.join(self.log_transactions_dir, f"graph_{file}.csv") + + @staticmethod + def get_record_uid(record: object) -> str: + pass + + @staticmethod + def get_key_bytes(record: object) -> bytes: + pass + + @staticmethod + def get_encrypted_payload_data(encrypted_payload_data: bytes) -> bytes: + try: + router_response = router_pb2.RouterResponse() + router_response.ParseFromString(encrypted_payload_data) + return router_response.encryptedPayload + except Exception as err: + raise Exception(f"Could not parse router response: {err}") + + def rest_call_to_router(self, + http_method: str, + endpoint: str, + agent: str, + payload: Optional[Union[str, bytes]] = None, + retry: int = 5, + retry_wait: float = 10, + throttle_inc_factor: float = 1.5, + timeout: Optional[int] = None, + headers: Optional[Dict] = None) -> Optional[bytes]: + return b"" + + def _endpoint(self, action: str, endpoint: Optional[str] = None) -> str: + + """ + Build the endpoint on the remote site. + + This method will attempt to fix slashes. + + :param action: + :param endpoint: + :return: + """ + + if endpoint is not None and endpoint != "": + if isinstance(endpoint, Enum): + endpoint = endpoint.value + + while endpoint.startswith("/"): + endpoint = endpoint[1:] + while endpoint.endswith("/"): + endpoint = endpoint[:-1] + endpoint = "/" + endpoint + else: + endpoint = "" + + while action.startswith("/"): + action = action[1:] + while action.endswith("/"): + action = action[:-1] + action = "/" + action + + base = "/api/device" + if not self.is_device: + base = "/api/user" + + return base + endpoint + action + + def write_transaction_log(self, + agent: str, + endpoint: str, + graph_id: Optional[int] = None, + request: Optional[Any] = None, + response: Optional[Any] = None, + error: Optional[str] = None): + + if self.log_transactions is True: + + file_name = graph_id + if file_name is None: + file_name = endpoint.replace("/", "_") + + timestamp = time.time() + + if isinstance(request, BaseModel): + request = request.model_dump_json() + elif hasattr(request, "SerializeToString"): + request = request.SerializeToString() + + if isinstance(response, BaseModel): + response = request.model_dump_json() + elif hasattr(response, "SerializeToString"): + response = request.SerializeToString() + + self.logger.info(f"TRANSACTION TIMESTAMP: {timestamp}") + filename = self.log_transaction_path(str(file_name)) + self.logger.debug(f"write to {filename}") + with open(filename, mode='a', newline='') as file: + self.logger.debug("write add_data to transaction log") + writer = csv.writer(file) + writer.writerow([ + timestamp, + sys.argv[0], + endpoint, + agent, + request, + response, + error + ]) + file.close() + + def payload_and_headers(self, payload: Any) -> Tuple[Union[str, bytes], Dict]: + + headers = {} + if isinstance(payload, BaseModel): + self.logger.debug("payload is pydantic") + payload = payload.model_dump_json() + elif hasattr(payload, "SerializeToString"): + self.logger.debug("payload is protobuf") + headers = {'Content-Type': 'application/octet-stream'} + payload = dag_crypto.encrypt_aes(payload.SerializeToString(), self.transmission_key) + else: + raise Exception("Cannot determine if the model is pydantic or protobuf.") + + return payload, headers + + def sync(self, + sync_query: Union[SyncQuery, GraphSync_pb2.GraphSyncQuery], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + agent: Optional[str] = None) -> bytes: + + if agent is None: + f"keeper-dag/{dag_version}" + + endpoint = self._endpoint(ConnectionBase.SYNC, endpoint) + self.logger.debug(f"endpoint {endpoint}") + + try: + sync_query, headers = self.payload_and_headers(sync_query) + payload = self.rest_call_to_router(http_method="POST", + endpoint=endpoint, + agent=agent, + headers=headers, + payload=sync_query) + + if self.use_read_protobuf: + try: + self.logger.debug(f"decrypt payload with transmission key {dag_utils.kotlin_bytes(self.transmission_key)}") + payload = self.get_encrypted_payload_data(payload) + payload = dag_crypto.decrypt_aes(payload, self.transmission_key) + except Exception as err: + self.logger.error(f"Could not decrypt protobuf graph sync response: {type(err)}, {err}") + + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=payload, + agent=agent, + endpoint=endpoint, + error=None + ) + + return payload + + except exceptions.DAGConnectionException as err: + + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise err + except Exception as err: + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise exceptions.DAGException(f"Could not load the DAG structure: {err}") + + def debug_dump(self) -> str: + return "Connection does not allow debug dump." + + def add_data(self, + payload: Union[dag_types.DataPayload, GraphSync_pb2.GraphSyncAddDataRequest], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + use_protobuf: bool = False, + agent: Optional[str] = None): + + if agent is None: + f"keeper-dag/{dag_version}" + + endpoint = self._endpoint(ConnectionBase.ADD_DATA, endpoint) + self.logger.debug(f"endpoint {endpoint}") + + try: + payload, headers = self.payload_and_headers(payload) + self.rest_call_to_router(http_method="POST", + endpoint=endpoint, + payload=payload, + headers=headers, + agent=agent) + + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=None + ) + except exceptions.DAGConnectionException as err: + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise err + except Exception as err: + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise exceptions.DAGException(f"Could not create a new DAG structure: {err}") diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py new file mode 100644 index 00000000..12ce4098 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py @@ -0,0 +1 @@ +__version__ = '1.1.0' # pragma: no cover diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection.py new file mode 100644 index 00000000..52f5d0ec --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection.py @@ -0,0 +1,210 @@ +import logging +import os + +import time +from typing import Any, Dict, Optional, Tuple, Union + +import requests +from . import ConnectionBase, dag_utils, exceptions + +from keepersdk.vault import vault_online, vault_record +from keepersdk import crypto, utils +from keepersdk.authentication import endpoint + +class Connection(ConnectionBase): + + def __init__(self, + vault: vault_online.VaultOnline, + verify_ssl: bool = True, + is_ws: bool = False, + logger: Optional[logging.Logger] = None, + log_transactions: Optional[bool] = False, + log_transactions_dir: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = False, + **kwargs): + + super().__init__(is_device=False, + logger=logger, + log_transactions=log_transactions, + log_transactions_dir=log_transactions_dir, + use_read_protobuf=use_read_protobuf, + use_write_protobuf=use_write_protobuf) + + self.vault = vault + self.verify_ssl = dag_utils.value_to_boolean(os.environ.get("VERIFY_SSL", verify_ssl)) + self.is_ws = is_ws + + self.transmission_key = kwargs.get("transmission_key") + self.dep_encrypted_transmission_key = kwargs.get("encrypted_transmission_key") + self.dep_encrypted_session_token = kwargs.get("encrypted_session_token") + + @staticmethod + def get_record_uid(record: vault_record.KeeperRecord) -> str: + return record.record_uid + + def get_key_bytes(self, record: vault_record.KeeperRecord) -> Optional[bytes]: + if hasattr(record, "record_key_bytes"): + rk = getattr(record, "record_key_bytes", None) + if rk: + return rk + if hasattr(record, "record_key"): + rk = getattr(record, "record_key", None) + if rk: + return rk + return self.vault.vault_data.get_record_key(record.record_uid) + + @property + def hostname(self) -> str: + # The host is connect.keepersecurity.com, connect.dev.keepersecurity.com, etc. Append "connect" in front + # of host used for Commander. + configured_host = f'connect.{self.vault.keeper_auth.keeper_endpoint.server}' + + # In GovCloud environments, the router service is not under the govcloud subdomain + if 'govcloud.' in configured_host: + # "connect.govcloud.keepersecurity.com" -> "connect.keepersecurity.com" + configured_host = configured_host.replace('govcloud.', '') + + return os.environ.get("ROUTER_HOST", configured_host) + + @property + def dag_server_url(self) -> str: + + # Allow override of the URL. If not set, get the hostname from the config. + hostname = os.environ.get("KROUTER_URL", self.hostname) + if hostname.startswith('ws') or hostname.startswith('http'): + return hostname + + use_ssl = dag_utils.value_to_boolean(os.environ.get("USE_SSL", True)) + if self.is_ws: + prot_pref = 'ws' + else: + prot_pref = 'http' + if use_ssl is True: + prot_pref += "s" + + return f'{prot_pref}://{hostname}' + + # deprecated + def get_keeper_tokens(self): + self.transmission_key = utils.generate_aes_key() + server_public_key = endpoint.SERVER_PUBLIC_KEYS[self.vault.keeper_auth.keeper_endpoint.server_key_id] + + if self.vault.keeper_auth.keeper_endpoint.server_key_id < 7: + self.dep_encrypted_transmission_key = crypto.encrypt_rsa(self.transmission_key, server_public_key) + else: + self.dep_encrypted_transmission_key = crypto.encrypt_ec(self.transmission_key, server_public_key) + self.dep_encrypted_session_token = crypto.encrypt_aes_v2( + self.vault.keeper_auth.auth_context.session_token, self.transmission_key) + + def payload_and_headers(self, payload: Any) -> Tuple[Union[str, bytes], Dict]: + + # If the dep_encrypted_transmission_key, use the set value over the generated ones. + if self.dep_encrypted_transmission_key is not None: + encrypted_transmission_key = self.dep_encrypted_transmission_key + encrypted_session_token = self.dep_encrypted_session_token + + # This is what we want to use; it's different for each call. + else: + # Create a new transmission key + self.transmission_key = utils.generate_aes_key() + self.logger.debug(f"transmission key is {self.transmission_key}") + # self.params.rest_context.transmission_key = self.transmission_key + server_public_key = endpoint.SERVER_PUBLIC_KEYS[self.vault.keeper_auth.keeper_endpoint.server_key_id] + + if self.vault.keeper_auth.keeper_endpoint.server_key_id < 7: + encrypted_transmission_key = crypto.encrypt_rsa(self.transmission_key, server_public_key) + else: + encrypted_transmission_key = crypto.encrypt_ec(self.transmission_key, server_public_key) + encrypted_session_token = crypto.encrypt_aes_v2( + self.vault.keeper_auth.auth_context.session_token, self.transmission_key) + + # We need the transmission_key for protobuf sync since it returns values encrypted with the transmission_key. + if self.transmission_key is None: + raise exceptions.DAGConnectionException("The transmission key has not been set. If setting encrypted_transmission_key " + "and encrypted_session_token, also set transmission_key to 32 bytes. " + "Setting the encrypted_transmission_key and encrypted_session_token is " + "deprecated.") + + payload, headers = super().payload_and_headers(payload) + + headers["TransmissionKey"] = utils.base64_url_encode(encrypted_transmission_key) + headers["Authorization"] = f'KeeperUser {utils.base64_url_encode(encrypted_session_token)}' + + return payload, headers + + def rest_call_to_router(self, + http_method: str, + endpoint: str, + agent: str, + payload: Optional[Union[str, bytes]] = None, + retry: int = 5, + retry_wait: float = 10, + throttle_inc_factor: float = 1.5, + timeout: Optional[int] = None, + headers: Optional[Dict] = None) -> Optional[bytes]: + + if timeout is None or timeout == 0: + timeout = Connection.TIMEOUT + + if isinstance(payload, str): + payload = payload.encode() + + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + + url = self.dag_server_url + endpoint + + if headers is None: + headers = {} + + attempt = 0 + while True: + try: + attempt += 1 + self.logger.debug(f"graph web service call to {url} [{attempt}/{retry}]") + response = requests.request( + method=http_method, + url=url, + verify=self.verify_ssl, + headers={ + **headers, + 'User-Agent': agent + }, + data=payload, + timeout=timeout + ) + self.logger.debug(f"response status: {response.status_code}") + response.raise_for_status() + return response.content + + except requests.exceptions.HTTPError as http_err: + + msg = http_err.response.reason + try: + content = http_err.response.content.decode() + if content is not None and content != "": + msg = "; " + content + except (Exception,): + pass + + err_msg = f"{http_err.response.status_code}, {msg}" + + if http_err.response.status_code == 429: + attempt -= 1 + retry_wait *= throttle_inc_factor + self.logger.warning("the connection to the graph service is being throttled, " + f"increasing the delay between retry: {retry_wait} seconds.") + + except Exception as err: + err_msg = str(err) + + self.logger.info(f"call to graph web service {url} had a problem: {err_msg}") + if attempt >= retry: + self.logger.error(f"call to graph web service {url}, after {retry} " + f"attempts, failed!: {err_msg}") + raise exceptions.DAGConnectionException(f"Call to graph web service {url}, after {retry} " + f"attempts, failed!: {err_msg}") + + self.logger.info(f"will retry call after {retry_wait} seconds.") + time.sleep(retry_wait) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/__init__.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/__init__.py new file mode 100644 index 00000000..52212435 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/__init__.py @@ -0,0 +1,319 @@ +from __future__ import annotations +import logging +from ..__version__ import __version__ +from ....proto import GraphSync_pb2 as gs_pb2 +from ..exceptions import DAGException, DAGConnectionException +from ..dag_types import SyncQuery, DataPayload +from ..dag_utils import value_to_boolean, kotlin_bytes +from ..dag_crypto import encrypt_aes, decrypt_aes +import csv +import os +import time +import sys +from enum import Enum +from pydantic import BaseModel +from typing import Optional, Union, Any, Dict, Tuple, TYPE_CHECKING +if TYPE_CHECKING: + Logger = Union[logging.RootLogger, logging.Logger] + +try: + from ....proto import router_pb2 as router_pb2 +except (Exception,): + from ....proto import router_abbr_pb2 as router_pb2 + + +class ConnectionBase: + + ADD_DATA = "/add_data" + SYNC = "/sync" + + TIMEOUT = 30 + + def __init__(self, + is_device: bool, + logger: Optional[Logger] = None, + log_transactions: Optional[bool] = None, + log_transactions_dir: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = False): + + self.is_device = is_device + + if logger is None: + logger = logging.getLogger() + self.logger = logger + + if log_transactions is not None: + self.log_transactions = value_to_boolean(log_transactions) + else: + self.log_transactions: bool = value_to_boolean(os.environ.get("GS_LOG_TRANS", False)) + + self.log_transactions_dir = os.environ.get("GS_LOG_TRANS_DIR", log_transactions_dir) + if self.log_transactions_dir is None: + self.log_transactions_dir = "." + + if self.log_transactions is True: + self.logger.info("keeper-dag transaction logging is ENABLED; " + f"write directory at {self.log_transactions_dir}") + + self.use_read_protobuf = use_read_protobuf + self.use_write_protobuf = use_write_protobuf + + self.transmission_key = None + + def close(self): + if hasattr(self, "logger"): + self.logger = None + del self.logger + + def __del__(self): + self.close() + + def log_transaction_path(self, file: str): + return os.path.join(self.log_transactions_dir, f"graph_{file}.csv") + + @staticmethod + def get_record_uid(record: object) -> str: + pass + + @staticmethod + def get_key_bytes(record: object) -> bytes: + pass + + @staticmethod + def get_encrypted_payload_data(encrypted_payload_data: bytes) -> bytes: + try: + router_response = router_pb2.RouterResponse() + router_response.ParseFromString(encrypted_payload_data) + return router_response.encryptedPayload + except Exception as err: + raise Exception(f"Could not parse router response: {err}") + + @staticmethod + def get_router_host(server_hostname: str): + + if server_hostname == 'govcloud.keepersecurity.us': + configured_host = 'connect.keepersecurity.us' + else: + configured_host = f'connect.{server_hostname}' + + return os.environ.get("ROUTER_HOST", configured_host) + + def rest_call_to_router(self, + http_method: str, + endpoint: str, + agent: str, + payload: Optional[Union[str, bytes]] = None, + retry: int = 5, + retry_wait: float = 10, + throttle_inc_factor: float = 1.5, + timeout: Optional[int] = None, + headers: Optional[Dict] = None) -> Optional[bytes]: + return b"" + + def _endpoint(self, action: str, endpoint: Optional[str] = None) -> str: + """ + Build the endpoint on the remote site. + + This method will attempt to fix slashes. + + :param action: + :param endpoint: + :return: + """ + + if endpoint is not None and endpoint != "": + if isinstance(endpoint, Enum): + endpoint = endpoint.value + + while endpoint.startswith("/"): + endpoint = endpoint[1:] + while endpoint.endswith("/"): + endpoint = endpoint[:-1] + endpoint = "/" + endpoint + else: + endpoint = "" + + while action.startswith("/"): + action = action[1:] + while action.endswith("/"): + action = action[:-1] + action = "/" + action + + base = "/api/device" + if not self.is_device: + base = "/api/user" + + return base + endpoint + action + + def write_transaction_log(self, + agent: str, + endpoint: str, + graph_id: Optional[int] = None, + request: Optional[Any] = None, + response: Optional[Any] = None, + error: Optional[str] = None): + + if self.log_transactions is True: + + file_name = graph_id + if file_name is None: + file_name = endpoint.replace("/", "_") + + timestamp = time.time() + + if isinstance(request, BaseModel): + request = request.model_dump_json() + elif hasattr(request, "SerializeToString"): + request = request.SerializeToString() + + if isinstance(response, BaseModel): + response = request.model_dump_json() + elif hasattr(response, "SerializeToString"): + response = request.SerializeToString() + + self.logger.info(f"TRANSACTION TIMESTAMP: {timestamp}") + filename = self.log_transaction_path(str(file_name)) + self.logger.debug(f"write to {filename}") + with open(filename, mode='a', newline='') as file: + self.logger.debug("write add_data to transaction log") + writer = csv.writer(file) + writer.writerow([ + timestamp, + sys.argv[0], + endpoint, + agent, + request, + response, + error + ]) + file.close() + + def payload_and_headers(self, payload: Any) -> Tuple[Union[str, bytes], Dict]: + + headers = {} + if isinstance(payload, BaseModel): + self.logger.debug("payload is pydantic") + payload = payload.model_dump_json() + elif hasattr(payload, "SerializeToString"): + self.logger.debug("payload is protobuf") + headers = {'Content-Type': 'application/octet-stream'} + payload = encrypt_aes(payload.SerializeToString(), self.transmission_key) + else: + raise Exception("Cannot determine if the model is pydantic or protobuf.") + + return payload, headers + + def sync(self, + sync_query: Union[SyncQuery, gs_pb2.GraphSyncQuery], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + agent: Optional[str] = None) -> bytes: + + if agent is None: + f"keeper-dag/{__version__}" + + endpoint = self._endpoint(ConnectionBase.SYNC, endpoint) + self.logger.debug(f"endpoint {endpoint}") + + try: + sync_query, headers = self.payload_and_headers(sync_query) + payload = self.rest_call_to_router(http_method="POST", + endpoint=endpoint, + agent=agent, + headers=headers, + payload=sync_query) + + if self.use_read_protobuf: + try: + self.logger.debug(f"decrypt payload with transmission key {kotlin_bytes(self.transmission_key)}") + payload = self.get_encrypted_payload_data(payload) + payload = decrypt_aes(payload, self.transmission_key) + except Exception as err: + self.logger.error(f"Could not decrypt protobuf graph sync response: {type(err)}, {err}") + + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=payload, + agent=agent, + endpoint=endpoint, + error=None + ) + + return payload + + except DAGConnectionException as err: + + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise err + except Exception as err: + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise DAGException(f"Could not load the DAG structure: {err}") + + def debug_dump(self) -> str: + return "Connection does not allow debug dump." + + def add_data(self, + payload: Union[DataPayload, gs_pb2.GraphSyncAddDataRequest], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + use_protobuf: bool = False, + agent: Optional[str] = None): + + if agent is None: + f"keeper-dag/{__version__}" + + endpoint = self._endpoint(ConnectionBase.ADD_DATA, endpoint) + self.logger.debug(f"endpoint {endpoint}") + + try: + payload, headers = self.payload_and_headers(payload) + self.rest_call_to_router(http_method="POST", + endpoint=endpoint, + payload=payload, + headers=headers, + agent=agent) + + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=None + ) + except DAGConnectionException as err: + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise err + except Exception as err: + self.write_transaction_log( + graph_id=graph_id, + request=payload, + response=None, + agent=agent, + endpoint=endpoint, + error=str(err) + ) + raise DAGException(f"Could not create a new DAG structure: {err}") diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/commander.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/commander.py new file mode 100644 index 00000000..2c128b81 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/commander.py @@ -0,0 +1,206 @@ +from __future__ import annotations +import logging +from . import ConnectionBase +from ..exceptions import DAGConnectionException +from ....authentication import endpoint +from ..dag_utils import value_to_boolean +import os +import requests +import time + +try: + from .... import crypto, utils +except ImportError: + raise Exception("Please install the keepercommander module to use the Commander connection.") + +from typing import Optional, Union, Dict, Tuple, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from ....vault import vault_online + from ....vault.vault_record import KeeperRecord + Content = Union[str, bytes, dict] + QueryValue = Union[list, dict, str, float, int, bool] + Logger = Union[logging.RootLogger, logging.Logger] + + +class Connection(ConnectionBase): + + def __init__(self, + vault: vault_online.VaultOnline, + verify_ssl: bool = True, + is_ws: bool = False, + logger: Optional[Logger] = None, + log_transactions: Optional[bool] = False, + log_transactions_dir: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = False, + **kwargs): + + super().__init__(is_device=False, + logger=logger, + log_transactions=log_transactions, + log_transactions_dir=log_transactions_dir, + use_read_protobuf=use_read_protobuf, + use_write_protobuf=use_write_protobuf) + + self.vault = vault + self.verify_ssl = value_to_boolean(os.environ.get("VERIFY_SSL", verify_ssl)) + self.is_ws = is_ws + + self.transmission_key = kwargs.get("transmission_key") + self.dep_encrypted_transmission_key = kwargs.get("encrypted_transmission_key") + self.dep_encrypted_session_token = kwargs.get("encrypted_session_token") + + @staticmethod + def get_record_uid(record: KeeperRecord) -> str: + return record.record_uid + + def get_key_bytes(self, record: KeeperRecord) -> bytes: + if hasattr(record, "record_key_bytes"): + rk = getattr(record, "record_key_bytes", None) + if rk: + return rk + if hasattr(record, "record_key"): + rk = getattr(record, "record_key", None) + if rk: + return rk + return self.vault.vault_data.get_record_key(record.record_uid) + + @property + def hostname(self) -> str: + return self.get_router_host(self.vault.keeper_auth.keeper_endpoint.server) + + @property + def dag_server_url(self) -> str: + + hostname = os.environ.get("KROUTER_URL", self.hostname) + if hostname.startswith('ws') or hostname.startswith('http'): + return hostname + + use_ssl = value_to_boolean(os.environ.get("USE_SSL", True)) + if self.is_ws: + prot_pref = 'ws' + else: + prot_pref = 'http' + if use_ssl is True: + prot_pref += "s" + + return f'{prot_pref}://{hostname}' + + def get_keeper_tokens(self): + self.transmission_key = utils.generate_aes_key() + server_public_key = endpoint.SERVER_PUBLIC_KEYS[self.vault.keeper_auth.keeper_endpoint.server_key_id] + + if self.vault.keeper_auth.keeper_endpoint.server_key_id < 7: + self.dep_encrypted_transmission_key = crypto.encrypt_rsa(self.transmission_key, server_public_key) + else: + self.dep_encrypted_transmission_key = crypto.encrypt_ec(self.transmission_key, server_public_key) + self.dep_encrypted_session_token = crypto.encrypt_aes_v2( + self.vault.keeper_auth.auth_context.session_token, self.transmission_key) + + def payload_and_headers(self, payload: Any) -> Tuple[Union[str, bytes], Dict]: + + if self.dep_encrypted_transmission_key is not None: + encrypted_transmission_key = self.dep_encrypted_transmission_key + encrypted_session_token = self.dep_encrypted_session_token + + else: + self.transmission_key = utils.generate_aes_key() + self.logger.debug(f"transmission key is {self.transmission_key}") + server_public_key = endpoint.SERVER_PUBLIC_KEYS[self.vault.keeper_auth.keeper_endpoint.server_key_id] + + if self.vault.keeper_auth.keeper_endpoint.server_key_id < 7: + encrypted_transmission_key = crypto.encrypt_rsa(self.transmission_key, server_public_key) + else: + encrypted_transmission_key = crypto.encrypt_ec(self.transmission_key, server_public_key) + encrypted_session_token = crypto.encrypt_aes_v2( + self.vault.keeper_auth.auth_context.session_token, self.transmission_key) + + if self.transmission_key is None: + raise DAGConnectionException("The transmission key has not been set. If setting encrypted_transmission_key " + "and encrypted_session_token, also set transmission_key to 32 bytes. " + "Setting the encrypted_transmission_key and encrypted_session_token is " + "deprecated.") + + payload, headers = super().payload_and_headers(payload) + + headers["TransmissionKey"] = utils.base64_url_encode(encrypted_transmission_key) + headers["Authorization"] = f'KeeperUser {utils.base64_url_encode(encrypted_session_token)}' + + return payload, headers + + def rest_call_to_router(self, + http_method: str, + endpoint: str, + agent: str, + payload: Optional[Union[str, bytes]] = None, + retry: int = 5, + retry_wait: float = 10, + throttle_inc_factor: float = 1.5, + timeout: Optional[int] = None, + headers: Optional[Dict] = None) -> Optional[bytes]: + + if timeout is None or timeout == 0: + timeout = Connection.TIMEOUT + + if isinstance(payload, str): + payload = payload.encode() + + if not endpoint.startswith("/"): + endpoint = "/" + endpoint + + url = self.dag_server_url + endpoint + + if headers is None: + headers = {} + + attempt = 0 + while True: + try: + attempt += 1 + self.logger.debug(f"graph web service call to {url} [{attempt}/{retry}]") + response = requests.request( + method=http_method, + url=url, + verify=self.verify_ssl, + headers={ + **headers, + 'User-Agent': agent + }, + data=payload, + timeout=timeout + ) + self.logger.debug(f"response status: {response.status_code}") + response.raise_for_status() + return response.content + + except requests.exceptions.HTTPError as http_err: + + msg = http_err.response.reason + try: + content = http_err.response.content.decode() + if content is not None and content != "": + msg = "; " + content + except (Exception,): + pass + + err_msg = f"{http_err.response.status_code}, {msg}" + + if http_err.response.status_code == 429: + attempt -= 1 + retry_wait *= throttle_inc_factor + self.logger.warning("the connection to the graph service is being throttled, " + f"increasing the delay between retry: {retry_wait} seconds.") + + except Exception as err: + err_msg = str(err) + + self.logger.info(f"call to graph web service {url} had a problem: {err_msg}") + if attempt >= retry: + self.logger.error(f"call to graph web service {url}, after {retry} " + f"attempts, failed!: {err_msg}") + raise DAGConnectionException(f"Call to graph web service {url}, after {retry} " + f"attempts, failed!: {err_msg}") + + self.logger.info(f"will retry call after {retry_wait} seconds.") + time.sleep(retry_wait) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/local.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/local.py new file mode 100644 index 00000000..79ce11f3 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/connection/local.py @@ -0,0 +1,600 @@ +from . import ConnectionBase +from ..struct.protobuf import DataStruct as PbDataStruct +from ....proto import GraphSync_pb2 as gs_pb2 +from ..dag_types import DataPayload, EdgeType, SyncQuery, Ref, RefType, DAGData, SyncDataItem, SyncData +from ..dag_crypto import bytes_to_urlsafe_str, urlsafe_str_to_bytes +from ..dag_utils import value_to_boolean +from .... import utils +import json +import os +import logging +from enum import Enum +from tabulate import tabulate + +try: + import sqlite3 + from contextlib import closing +except ImportError: + raise Exception("Please install the sqlite3 module to use the Local connection.") + +from typing import Optional, Union, Any, TYPE_CHECKING +if TYPE_CHECKING: + Logger = Union[logging.RootLogger, logging.Logger] + + +class Connection(ConnectionBase): + DB_FILE = "local_dag.db" + + def __init__(self, + limit: int = 100, + db_file: Optional[str] = None, + db_dir: Optional[str] = None, + logger: Optional[Any] = None, + log_transactions: Optional[bool] = None, + log_transactions_dir: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = False): + + super().__init__(is_device=False, + logger=logger, + log_transactions=log_transactions, + log_transactions_dir=log_transactions_dir, + use_read_protobuf=use_read_protobuf, + use_write_protobuf=use_write_protobuf) + + if db_file is None: + db_file = os.environ.get("LOCAL_DAG_DB_FILE", Connection.DB_FILE) + if db_dir is None: + db_dir = os.environ.get("LOCAL_DAG_DIR", os.environ.get("HOME", os.environ.get("USERPROFILE", "./"))) + + self.allow_debug = value_to_boolean(os.environ.get("GS_CONN_DEBUG", False)) + if self.allow_debug is True: + self.debug("enabling GraphSync connection logging") + + self.db_file = os.path.join(db_dir, db_file) + self.limit = limit + + self.create_database() + + def debug(self, msg): + if self.allow_debug: + self.logger.debug(f"GraphSync LOCAL: {msg}") + + @staticmethod + def get_record_uid(record: object) -> bytes: + if hasattr(record, "record_uid"): + return getattr(record, "record_uid") + elif hasattr(record, "uid"): + return getattr(record, "uid") + raise Exception(f"Cannot find the record uid in object type: {type(record)}.") + + @staticmethod + def get_key_bytes(record: object) -> bytes: + if hasattr(record, "record_key_bytes"): + return getattr(record, "record_key_bytes") + elif hasattr(record, "record_key"): + return getattr(record, "record_key") + raise Exception("Cannot find the record key bytes in object.") + + def clear_database(self): + try: + os.unlink(self.db_file) + except (Exception,): + pass + + def create_database(self): + + self.debug("create local dag database") + + if os.path.isfile(self.db_file): + return False + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS dag_edges ( + graph_id INTEGER, + edge_id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + head CHARACTER(22) NOT NULL, + tail CHARACTER(22) NOT NULL, + data BLOB, + origin CHARACTER(22), + path TEXT, + created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_id BLOB(16) DEFAULT NULL, + creator_type INTEGER DEFAULT NULL, + creator_name TEXT DEFAULT NULL, + FOREIGN KEY(head) REFERENCES dag_vertices(vertex_id), + FOREIGN KEY(tail) REFERENCES dag_vertices(vertex_id) +) + """ + ) + + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS dag_vertices ( + vertex_id CHARACTER(22) NOT NULL, + type TEXT NOT NULL, + name TEXT, + owner_id BLOB(16) DEFAULT NULL +) + """ + ) + + cursor.execute( + """ +CREATE TABLE IF NOT EXISTS dag_streams ( + graph_id INTEGER, + sync_point INTEGER PRIMARY KEY AUTOINCREMENT, + vertex_id CHARACTER(22) NOT NULL, + edge_id INTEGER NOT NULL, + count INTEGER NOT NULL DEFAULT 0, + deletion INTEGER NOT NULL DEFAULT 0, + UNIQUE(vertex_id,edge_id), + FOREIGN KEY(vertex_id) REFERENCES dag_vertices(vertex_id), + FOREIGN KEY(edge_id) REFERENCES dag_edges(edge_id) +) + """ + ) + connection.commit() + + os.chmod(self.db_file, 0o777) + return None + + @staticmethod + def _payload_to_json(payload: Union[DataPayload, str]) -> dict: + + payload_data = "{}" + if isinstance(payload, DataPayload): + payload_data = payload.model_dump_json() + elif isinstance(payload, str): + payload_data = payload + + if not payload_data.startswith('{') and not payload_data.endswith('}'): + raise Exception(f'Invalid payload: {payload_data}') + + json.loads(payload_data) + + return json.loads(payload_data) + + def _find_stream_id(self, payload: DataPayload): + + data = Connection._payload_to_json(payload) + + self.debug("finding stream id") + + stream_id = None + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + graph_id = data.get("graphId") + + stream_ids = {} + + runs = 0 + for item in data.get("dataList"): + + item_stream_id = item.get("ref")["value"] + current_stream_id = item_stream_id + while True: + self.debug(f" check stream id {current_stream_id}") + sql = "SELECT head, edge_id FROM dag_edges WHERE tail=? AND graph_id=? AND type != ?" + res = cursor.execute(sql, (current_stream_id, graph_id, EdgeType.DATA.value)) + row = res.fetchone() + if row is None: + self.debug(f" no edge found") + if current_stream_id == item_stream_id: + current_stream_id = None + break + current_stream_id = row[0] + self.debug(f" got {current_stream_id}") + + if current_stream_id is not None: + if item_stream_id not in stream_ids: + stream_ids[current_stream_id] = 0 + stream_ids[current_stream_id] += 1 + else: + + item_stream_id = item.get("parentRef")["value"] + current_stream_id = item_stream_id + while True: + self.debug(f" check stream id {current_stream_id}") + sql = "SELECT head, edge_id FROM dag_edges WHERE tail=? AND graph_id=? AND type != ?" + res = cursor.execute(sql, (current_stream_id, graph_id, EdgeType.DATA.value)) + row = res.fetchone() + if row is None: + self.debug(f" no edge found") + if current_stream_id == item_stream_id: + current_stream_id = None + break + current_stream_id = row[0] + self.debug(f" got {current_stream_id}") + + if current_stream_id is not None: + if item_stream_id not in stream_ids: + stream_ids[current_stream_id] = 0 + stream_ids[current_stream_id] += 1 + + if runs > 3: + break + runs += 1 + + if len(stream_ids) > 0: + sorted_stream_ids = [k for k, v in sorted(stream_ids.items(), key=lambda i: i[1])] + stream_id = sorted_stream_ids.pop() + + if stream_id is None: + self.debug("stream id None, edges might be new") + + found = {} + for item in data.get("dataList"): + head_uid = item.get("parentRef")["value"] + found[head_uid] = True + for item in data.get("dataList"): + tail_uid = item.get("ref")["value"] + found.pop(tail_uid, None) + stream_ids = [uid for uid in found] + if len(stream_ids) > 0: + stream_id = stream_ids[0] + + if stream_id is None: + item = data.get("dataList")[0] + stream_id = item.get("parentRef")["value"] or item.get("ref")["value"] + + return stream_id + + @staticmethod + def _add_data_pb_to_pydantic(payload: gs_pb2.GraphSyncAddDataRequest) -> DataPayload: + + data = [] + for item in payload.data: + data.append( + DAGData( + type=PbDataStruct.PB_TO_DATA_MAP.get(item.type), + content=bytes_to_urlsafe_str(item.content), + ref=Ref( + type=PbDataStruct.PB_TO_REF_MAP.get(item.ref.type), + value=bytes_to_urlsafe_str(item.ref.value), + name=item.ref.name, + ), + parentRef=Ref( + type=PbDataStruct.PB_TO_REF_MAP.get(item.parentRef.type), + value=bytes_to_urlsafe_str(item.parentRef.value), + name=item.parentRef.name + ), + path=item.path + ) + ) + + return DataPayload( + origin=Ref( + type=PbDataStruct.PB_TO_REF_MAP.get(payload.origin.type), + value=bytes_to_urlsafe_str(payload.origin.value), + name=payload.origin.name, + ), + dataList=data + ) + + def add_data(self, + payload: Union[DataPayload, gs_pb2.GraphSyncAddDataRequest], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + use_protobuf: bool = False, + agent: Optional[str] = None): + + if isinstance(payload, gs_pb2.GraphSyncAddDataRequest): + payload = self._add_data_pb_to_pydantic(payload) + + stream_id = self._find_stream_id(payload) + self.debug(f"STREAM ID IS {stream_id}") + + endpoint = self._endpoint( + action="/add_data", + endpoint=endpoint) + self.logger.debug(f"endpoint, local test = {endpoint}") + + data = Connection._payload_to_json(payload) + + self.write_transaction_log( + graph_id=payload.graphId, + request=json.dumps(data), + response=None, + agent=agent, + endpoint=endpoint, + error=None + ) + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + origin_id = data.get("origin")["value"] + graph_id = data.get("graphId") + + saved_vertex = {} + for item in data.get("dataList"): + + tail_uid = item.get("ref")["value"] + tail_type = item.get("ref")["type"] + tail_name = item.get("ref")["name"] + + head_uid = None + head_type = None + head_name = None + if item.get("parentRef") is not None: + head_uid = item.get("parentRef")["value"] + head_type = item.get("parentRef")["type"] + head_name = item.get("parentRef")["name"] + + edge_type = item.get("type") + path = item.get("path") + + content = item.get("content") + if content is not None: + content = utils.base64_url_decode(content) + + sql = "INSERT INTO dag_edges (type, head, tail, data, origin, graph_id, path) " + sql += "VALUES (?,?,?,?,?,?,?)" + cursor.execute(sql, ( + edge_type, + head_uid, + tail_uid, + content, + origin_id, + graph_id, + path + )) + edge_id = cursor.lastrowid + + sql = "INSERT INTO dag_streams (graph_id, vertex_id, edge_id, count) VALUES (?, ?, ?, ?)" + cursor.execute(sql, ( + graph_id, + stream_id, + edge_id, + 1 + )) + + if saved_vertex.get(tail_uid) is None: + # Type is RefType enum value + sql = "INSERT INTO dag_vertices (vertex_id, type, name) VALUES (?, ?, ?)" + cursor.execute(sql, ( + tail_uid, + tail_type, + tail_name + )) + saved_vertex[tail_uid] = True + if saved_vertex.get(head_uid) is None: + # Type is RefType enum value + sql = "INSERT INTO dag_vertices (vertex_id, type, name) VALUES (?, ?, ?)" + cursor.execute(sql, ( + head_uid, + head_type, + head_name + )) + saved_vertex[head_uid] = True + + connection.commit() + + @staticmethod + def _sync_pb_to_pydantic(payload: gs_pb2.GraphSyncQuery) -> SyncQuery: + + return SyncQuery( + streamId=bytes_to_urlsafe_str(payload.streamId), + graphId=payload.syncPoint, + syncPoint=payload.syncPoint + ) + + def sync(self, + sync_query: Union[SyncQuery, gs_pb2.GraphSyncQuery], + graph_id: Optional[int] = None, + endpoint: Optional[str] = None, + agent: Optional[str] = None) -> bytes: + + is_protobuf = False + if isinstance(sync_query, gs_pb2.GraphSyncQuery): + is_protobuf = True + sync_query = self._sync_pb_to_pydantic(sync_query) + + edge_type_map = { + EdgeType.DATA.value: "data", + EdgeType.KEY.value: "key", + EdgeType.LINK.value: "link", + EdgeType.ACL.value: "acl", + EdgeType.DELETION.value: "deletion", + EdgeType.DENIAL.value: "denial", + EdgeType.UNDENIAL.value: "undenial", + } + + stream_id = sync_query.streamId + graph_id = sync_query.graphId + sync_point = sync_query.syncPoint + + endpoint = self._endpoint( + action="/sync", + endpoint=endpoint) + self.logger.debug(f"endpoint, local test = {endpoint}") + + if isinstance(sync_query.graphId, Enum): + graph_id = sync_query.graphId.value + + self.write_transaction_log( + graph_id=graph_id, + request=sync_query, + response=None, + agent=agent, + endpoint=endpoint, + error=None + ) + + has_more = False + new_sync_point = 0 + data = [] + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + self.debug(f"... loading DAG, {stream_id}, {sync_point}, {self.limit + 1}") + + args = [stream_id, sync_point, graph_id] + sql = "SELECT sync_point, edge_id FROM dag_streams WHERE vertex_id = ? AND deletion = 0 "\ + "AND sync_point > ? AND graph_id=? ORDER BY sync_point ASC LIMIT ?" + args.append(self.limit + 1) + + res = cursor.execute(sql, tuple(args)) + rows = list(res.fetchall()) + if len(rows) > self.limit: + has_more = True + rows.pop() + self.logger.debug(f"... loaded {len(rows)} edges") + for row in rows: + new_sync_point = row[0] + + args = [row[1], graph_id] + sql = "SELECT head, tail, data, path, type FROM dag_edges WHERE edge_id = ? AND graph_id=?" + res = cursor.execute(sql, tuple(args)) + edges = res.fetchone() + + parent_ref = None + if edges[1] != edges[0]: + + sql = "SELECT type FROM dag_vertices WHERE vertex_id = ?" + res = cursor.execute(sql, (edges[0],)) + head_vertex = res.fetchone() + + parent_ref = { + "type": head_vertex[0], + "value": edges[0], + "name": None + } + + sql = "SELECT type FROM dag_vertices WHERE vertex_id = ?" + res = cursor.execute(sql, (edges[1],)) + tail_vertex = res.fetchone() + + if is_protobuf: + data.append( + gs_pb2.GraphSyncDataPlus( + data=gs_pb2.GraphSyncData( + type=PbDataStruct.DATA_TO_PB_MAP.get(EdgeType.find_enum(edges[4])), + content=edges[2], + path=edges[3], + ref=gs_pb2.GraphSyncRef( + type=PbDataStruct.REF_TO_PB_MAP.get(EdgeType.find_enum(tail_vertex[0])), + value=urlsafe_str_to_bytes(edges[1]) + ), + parentRef=gs_pb2.GraphSyncRef( + type=PbDataStruct.REF_TO_PB_MAP.get(EdgeType.find_enum(parent_ref.get("type"))), + value=urlsafe_str_to_bytes(parent_ref.get("value")) + ) if parent_ref else None, + ) + ) + ) + else: + content = edges[2] + if content is not None: + content = utils.base64_url_decode(content) + + data.append( + SyncDataItem( + type=EdgeType.find_enum(edges[4]), + content=content, + path=edges[3], + deletion=False, + ref=Ref( + type=RefType.find_enum(tail_vertex[0]), + value=edges[1] + ), + parentRef=Ref( + type=RefType.find_enum(parent_ref.get("type")), + value=parent_ref.get("value") + ) if parent_ref else None, + ) + ) + + if is_protobuf: + return gs_pb2.GraphSyncResult( + streamId=urlsafe_str_to_bytes(stream_id), + syncPoint=new_sync_point, + data=data, + hasMore=has_more + ).SerializeToString() + else: + return SyncData( + syncPoint=new_sync_point, + data=data, + hasMore=has_more + ).model_dump_json().encode() + + def debug_dump(self) -> str: + + ret = "" + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + cols = ["graph_id", "edge_id", "type", "head", "tail", "data", "origin", "path", "created", + "creator_id", "creator_type", "creator_name"] + + sql = f"SELECT {','.join(cols)} FROM dag_edges ORDER BY edge_id DESC" + res = cursor.execute(sql,) + + ret += "dag_edges\n" + ret += "=========\n" + table = [] + for row in res.fetchall(): + table.append(list(row)) + + ret += tabulate(table, cols) + "\n\n" + + cols = ["e.graph_id", "e.edge_id", "v.vertex_id", "v.type", "v.name", "v.owner_id"] + + sql = f"SELECT {','.join(cols)} "\ + "FROM dag_vertices v "\ + "INNER JOIN dag_edges e ON e.tail = v.vertex_id "\ + "ORDER BY e.graph_id DESC, e.edge_id DESC" + res = cursor.execute(sql,) + + ret += "dag_vertices\n" + ret += "============\n" + table = [] + for row in res.fetchall(): + table.append(list(row)) + + ret += tabulate(table, cols) + "\n\n" + + cols = ["graph_id", "edge_id", "sync_point", "vertex_id", "count", "deletion"] + + sql = f"SELECT {','.join(cols)} FROM dag_streams ORDER BY edge_id DESC" + res = cursor.execute(sql,) + + ret += "dag_streams\n" + ret += "===========\n" + table = [] + for row in res.fetchall(): + table.append(list(row)) + + ret += tabulate(table, cols) + "\n\n" + + return ret + + def update_edge_content(self, graph_id: int, head_uid: str, tail_uid: str, content: str): + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + sql = "UPDATE dag_edges SET data=? WHERE graph_id=? AND head=? AND tail=?" + cursor.execute(sql, (content, graph_id, head_uid, tail_uid)) + + connection.commit() + + def clear(self): + + with closing(sqlite3.connect(self.db_file)) as connection: + with closing(connection.cursor()) as cursor: + + for table in ["dag_streams", "dag_edges", "dag_vertices"]: + sql = f"DELETE FROM {table}" + cursor.execute(sql, ) + + connection.commit() diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py new file mode 100644 index 00000000..3865348a --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py @@ -0,0 +1,53 @@ +# This should the relationship between Keeper Vault record +RECORD_LINK_GRAPH_ID = 0 + +# The rules +DIS_RULES_GRAPH_ID = 10 + +# The discovery job history +DIS_JOBS_GRAPH_ID = 11 + +# Discovery infrastructure +DIS_INFRA_GRAPH_ID = 12 + +# The user-to-services graph +USER_SERVICE_GRAPH_ID = 13 + +PAM_DIRECTORY = "pamDirectory" +PAM_DATABASE = "pamDatabase" +PAM_MACHINE = "pamMachine" +PAM_USER = "pamUser" +LOCAL_USER = "local" + +PAM_RESOURCES = [ + PAM_DIRECTORY, + PAM_DATABASE, + PAM_MACHINE +] + +PAM_DOMAIN_CONFIGURATION = "pamDomainConfiguration" +PAM_AZURE_CONFIGURATION = "pamAzureConfiguration" +PAM_AWS_CONFIGURATION = "pamAwsConfiguration" +PAM_NETWORK_CONFIGURATION = "pamNetworkConfiguration" +PAM_GCP_CONFIGURATION = "pamGcpConfiguration" + +PAM_CONFIGURATIONS = [ + PAM_DOMAIN_CONFIGURATION, + PAM_AZURE_CONFIGURATION, + PAM_AWS_CONFIGURATION, + PAM_NETWORK_CONFIGURATION, + PAM_GCP_CONFIGURATION +] + + +DOMAIN_USER_CONFIGS = [ + PAM_DOMAIN_CONFIGURATION, + PAM_AZURE_CONFIGURATION +] + +VERTICES_SORT_MAP = { + PAM_USER: {"order": 1, "sort": "sort_infra_name", "item": "DiscoveryUser", "key": "user"}, + PAM_DIRECTORY: {"order": 1, "sort": "sort_infra_name", "item": "DiscoveryDirectory", "key": "host_port"}, + PAM_MACHINE: {"order": 2, "sort": "sort_infra_host", "item": "DiscoveryMachine", "key": "host"}, + PAM_DATABASE: {"order": 3, "sort": "sort_infra_host", "item": "DiscoveryDatabase", "key": "host_port"}, +} diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag.py new file mode 100644 index 00000000..152a743e --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag.py @@ -0,0 +1,1289 @@ + +from enum import Enum +import importlib +import json +import logging +import os +import sys +import traceback +from typing import Any, List, Optional, Tuple, Union + +from . import dag_utils, dag_crypto +from .dag_types import EdgeType, Ref, RefType, ENDPOINT_TO_GRAPH_ID_MAP, DAGData +from .dag_vertex import DAGVertex +from .struct.protobuf import DataStruct as ProtobufDataStruct +from .struct.default import DataStruct as DefaultDataStruct +from .exceptions import (DAGPathException, DAGDataException, DAGKeyException, DAGCorruptException, + DAGVertexException, DAGConfirmException, DAGVertexAlreadyExistsException) +from .connection import ConnectionBase +from .__version__ import __version__ as dag_version +from ... import utils + +QueryValue = Union[list, dict, str, float, int, bool] + +class DAG: + + DEBUG_LEVEL = 0 + + UID_KEY_BYTES_SIZE = 16 + UID_KEY_STR_SIZE = 22 + + EDGE_LABEL = { + EdgeType.DATA: "DATA", + EdgeType.KEY: "KEY", + EdgeType.LINK: "LINK", + EdgeType.ACL: "ACL", + EdgeType.DELETION: "DELETION", + } + + def __init__(self, + conn: ConnectionBase, + record: Optional[object] = None, + key_bytes: Optional[bytes] = None, + name: Optional[str] = None, + read_endpoint: Optional[Union[str, Enum]] = None, + write_endpoint: Optional[Union[str, Enum]] = None, + graph_id: Optional[Union[int, Enum]] = None, + auto_save: bool = False, + history_level: int = 0, + logger: Optional[Any] = None, + debug_level: int = 0, + is_dev: bool = False, + vertex_type: RefType = RefType.PAM_NETWORK, + decrypt: bool = True, + fail_on_corrupt: bool = True, + data_requires_encryption: bool = False, + log_prefix: str = "GraphSync", + save_batch_count: Optional[int] = None, + agent: Optional[str] = None, + dedup_edges: bool = False): + + """ + Create a GraphSync instance. + + :param conn: Connection instance + :param record: If set, the key bytes will use the key bytes in the record. Overrides key_bytes. + :param key_bytes: If set, these key bytes will be used. + :param name: Optional name for the graph. + :param read_endpoint: Endpoint for reading from graph. Use this over `graph_id`. (i.e. graph-sync/pam ) + :param write_endpoint: Endpoint for writing to graph. Use this over `graph_id`. (i.e. graph-sync/pam ) + :param graph_id: Graph ID sets which graph to load for the graph. `endpoint` replaces this, but code is + backwards compatiable. + :param auto_save: Automatically save when modifications are performed. Default is False. + :param history_level: How much edge history to keep in memory. Default is 0, no history. + :param logger: Python logger instance to use for logging. + :param debug_level: Debug level; the higher the number will result in more debug information. + :param is_dev: Is the code running in a development environment? + :param vertex_type: The default vertex/ref type for the root vertex, if auto creating. + :param decrypt: Decrypt the graph; Default is TRUE + :param fail_on_corrupt: If unable to decrypt encrypted data, fail out. + :param data_requires_encryption: Data edges are already encrypted. Default is False. + :param log_prefix: Text prepended to the log messages. Handy if dealing with multiple graphs. + :param save_batch_count: The number of edges to save at one time. + :param agent: User Agent to send with web service requests. + :param dedup_edges: Remove modified edges if the same edge added before save. + :return: Instance of GraphSync + """ + + if logger is None: + logger = logging.getLogger() + self.logger = logger + if debug_level is None: + debug_level = int(os.environ.get("GS_DEBUG_LEVEL", os.environ.get("DAG_DEBUG_LEVEL", 0))) + + self.dedup_edge = dag_utils.value_to_boolean(os.environ.get("GS_DEDUP_EDGES", dedup_edges)) + self.dedup_edge_warning = dag_utils.value_to_boolean(os.environ.get("GS_DEDUP_EDGES_WARN", False)) + + if self.dedup_edge and auto_save: + raise Exception("Cannot run dedup_edge and auto_save at the same time. The dedup_edge feature only works " + "in bulk saves.") + + self.debug_level = debug_level + self.log_prefix = log_prefix + + if save_batch_count is None or save_batch_count <= 0: + save_batch_count = 0 + self.save_batch_count = save_batch_count + + self.vertex_type = vertex_type + + self.data_requires_encryption = data_requires_encryption + self.decrypt = decrypt + self.fail_on_corrupt = fail_on_corrupt + + gs_is_dev = os.environ.get("GS_IS_DEV", os.environ.get("DAG_IS_DEV")) + if gs_is_dev is not None: + is_dev = dag_utils.value_to_boolean(gs_is_dev) + self.is_dev = is_dev + + self.uid = None + if record is not None: + self.uid = conn.get_record_uid(record) + key_bytes = conn.get_key_bytes(record) + + self.key = key_bytes + + if key_bytes is None: + raise ValueError("Either the record or the key_bytes needs to be passed.") + + if self.uid is None: + self.uid = dag_crypto.generate_uid_str(key_bytes[:16]) + + if graph_id is None and (read_endpoint is None or write_endpoint is None): + raise ValueError("Either graph_id or read/write endpoints needs to be set.") + + if graph_id is not None: + if isinstance(read_endpoint, Enum): + graph_id = ENDPOINT_TO_GRAPH_ID_MAP.get(read_endpoint.value) + self.graph_id = graph_id + + if read_endpoint is not None: + if isinstance(read_endpoint, Enum): + read_endpoint = read_endpoint.value + if write_endpoint is not None: + if isinstance(write_endpoint, Enum): + write_endpoint = write_endpoint.value + + self.read_endpoint = read_endpoint + self.write_endpoint = write_endpoint + + if name is None: + name = f"{self.log_prefix} ROOT" + self.name = name + + self._vertices = [] + self._uid_lookup = {} + + self.origin_ref_value = dag_crypto.generate_uid_bytes(16) + self.origin_uid = dag_crypto.generate_uid_str(uid_bytes=self.origin_ref_value) + + self.auto_save = auto_save + + self._allow_auto_save = False + + self.need_save_confirm = False + + self.last_sync_point = 0 + + self.history_level = history_level + + self.corrupt_uids = [] + + self.conn = conn + + self.read_struct_obj: Union[ProtobufDataStruct, DefaultDataStruct] = ProtobufDataStruct() \ + if conn.use_read_protobuf else DefaultDataStruct() + self.write_struct_obj: Union[ProtobufDataStruct, DefaultDataStruct] = ProtobufDataStruct() \ + if conn.use_write_protobuf else DefaultDataStruct() + + self.agent = f"keeper-dag/{dag_version}" + if agent is not None: + self.agent += "; " + agent + + self.debug(f"save batch count is set to {self.save_batch_count}") + if self.is_dev is True: + self.debug("GraphSync is running in a development environment, vertex names will be included.") + self.debug(f"edge de-dup is {self.dedup_edge}", level=1) + self.debug(f"edge de-dup debug warning {self.dedup_edge_warning}", level=1) + self.debug(f"{self.log_prefix} key {self.key}", level=1) + self.debug(f"{self.log_prefix} UID {self.uid}", level=1) + self.debug(f"{self.log_prefix} UID HEX {dag_crypto.urlsafe_str_to_bytes(self.uid).hex()}", level=1) + + def __del__(self): + self.cleanup() + + def cleanup(self): + """ + Explicitly clean up the DAG and break circular references. + + This method allows users to manually trigger cleanup before the object + goes out of scope. This is useful in scenarios where you want to ensure + immediate memory release, such as: + - High-frequency DAG creation/destruction + - Long-running processes + - Memory-constrained environments + + After calling this method, the DAG object should not be used. + + Example: + dag = DAG(conn=conn, key_bytes=key) + # ... use the dag ... + dag.cleanup() # Explicitly clean up + del dag + """ + + try: + # Safely get the root vertex without creating a new one + if hasattr(self, '_vertices') and hasattr(self, 'uid') and hasattr(self, '_uid_lookup'): + if len(self._vertices) > 0 and self.uid in self._uid_lookup: + idx = self._uid_lookup[self.uid] + if idx < len(self._vertices): + root = self._vertices[idx] + if hasattr(root, 'clean_edges'): + root.clean_edges() + except (Exception,): + pass + finally: + try: + if hasattr(self, '_vertices'): + self._vertices.clear() + if hasattr(self, '_uid_lookup'): + self._uid_lookup.clear() + if hasattr(self, 'corrupt_uids'): + self.corrupt_uids.clear() + except (Exception,): + pass + + self.read_struct_obj = None + del self.read_struct_obj + self.write_struct_obj = None + del self.write_struct_obj + self.conn = None + del self.conn + + def debug(self, msg: str, level: int = 0): + """ + Debug with granularity level. + + If the debug level is greater or equal to the level on the message, the message will be displayed. + + :param msg: Text debug message + :param level: Debug level of message + :return: + """ + + if self.debug_level >= level: + + msg = f"{self.log_prefix}: {msg}" + + if self.logger is not None: + self.logger.debug(msg) + else: + logging.debug(msg) + + def debug_stacktrace(self): + exc = sys.exc_info()[0] + stack = traceback.extract_stack()[:-1] + if exc is not None: + del stack[-1] + trc = 'Traceback (most recent call last):\n' + msg = trc + ''.join(traceback.format_list(stack)) + if exc is not None: + msg += ' ' + traceback.format_exc().lstrip(trc) + self.debug(msg) + + def __str__(self): + ret = f"GraphSync {self.uid}\n" + ret += f" python instance id: {id(self)}\n" + ret += f" name: {self.name}\n" + ret += f" key: {self.key}\n" + ret += f" vertices:\n" + for v in self.all_vertices: + ret += f" * {v.uid}, Keys: {v.keychain}, Active: {v.active}\n" + for e in v.edges: + if e.edge_type == EdgeType.DATA: + ret += f" + has a DATA edge" + if e.content is not None: + ret += ", has content" + else: + ret += f" + belongs to {e.head_uid}, {DAG.EDGE_LABEL.get(e.edge_type)}, {e.content}" + ret += "\n" + + return ret + + @property + def is_corrupt(self): + return len(self.corrupt_uids) > 0 + + @property + def allow_auto_save(self) -> bool: + """ + Return the flag indicating if auto save is allowed. + :return: + """ + return self._allow_auto_save + + @allow_auto_save.setter + def allow_auto_save(self, value: bool): + """ + Set the ability to auto save. + :param value: True enables, False disables. + :return: + """ + if value: + self.debug("ability to auto save has been ENABLED", level=2) + else: + self.debug("ability to auto save has been DISABLED", level=2) + + self._allow_auto_save = value + + @property + def origin_ref(self) -> Ref: + """ + Return an instance of the origin reference for adding data + :return: + """ + return Ref( + type=RefType.DEVICE, + value=self.origin_uid, + name=self.name if self.is_dev is True else None + ) + + @property + def has_graph(self) -> bool: + """ + :return: True if there are vertices. False if no vertices. + """ + return len(self._vertices) > 0 + + @property + def vertices(self) -> List[DAGVertex]: + """ + Get all active vertices + :return: List of DAGVertex instance + """ + return [ + vertex + for vertex in self._vertices + if vertex.active is True + ] + + @property + def all_vertices(self) -> List[DAGVertex]: + """ + Get all vertices + :return: List of DAGVertex instance + """ + return self._vertices + + def get_vertex(self, key) -> Optional[DAGVertex]: + + """ + Get a single vertex. + + The key can be either a UID, path or name. + + The UID is most reliable since there can only be one per graph. + + The path is second reliable if it is set by the user. + It will find an edge with the path, the vertex that is the edge's tail is returned. + There is no unique constraint for the path. + You can have duplicates. + + The name is third, and not reliable. + The name only exists when the graph is created. + If loaded, the name will be None. + + :param key: A UID, path item, or name of a vertex. + :return: DAGVertex instance, if it exists. + """ + + if key is None: + return None + + if key in self._uid_lookup: + index = self._uid_lookup[key] + return self._vertices[index] + + vertices = self.get_vertices_by_path_value(key, inc_deleted=True) + if len(vertices) > 0: + if len(vertices) > 1: + raise DAGPathException("Cannot get vertex using the path. Found multiple vertex that use the path.") + return vertices[0] + + for vertex in vertices: + if vertex.name == key: + return vertex + + return None + + @property + def get_root(self) -> Optional[DAGVertex]: + """ + Get the root vertex + If the root vertex does not exist, it will create the vertex with a ref type of PAM_NETWORK. + :return: + """ + root = self.get_vertex(self.uid) + if root is None: + root = self.add_vertex(uid=self.uid, name=self.name, vertex_type=self.vertex_type) + return root + + def vertex_exists(self, key: str) -> bool: + """ + Check if a vertex identified by the key exists. + :param key: UID, path, or name + :return: + """ + return self.get_vertex(key) is not None + + def get_vertices_by_path_value(self, path: str, inc_deleted: bool = False) -> List[DAGVertex]: + """ + Find all vertices that have an edge that match the path + :param path: A string path value. This is a path to walk, just the value. + :param inc_deleted: Include deleted edges. + :return: List of DAGVertex + """ + results = [] + if inc_deleted: + vertices = self.all_vertices + else: + vertices = self.vertices + + for vertex in vertices: + for edge in vertex.edges: + if edge.path == path: + results.append(vertex) + return results + + def _sync(self, sync_point: int = 0) -> Tuple[List[DAGData], int]: + has_more = True + + all_data = [] + while has_more: + sync_query = self.read_struct_obj.sync_query(stream_id=self.uid, + sync_point=sync_point, + graph_id=self.graph_id) + + results = self.read_struct_obj.get_sync_result( + self.conn.sync( + sync_query=sync_query, + graph_id=self.graph_id, + endpoint=self.read_endpoint, + agent=self.agent + )) + + if results.syncPoint == 0: + return all_data, 0 + + all_data += results.data + + has_more = results.hasMore + + sync_point = results.syncPoint + + return all_data, sync_point + + def _load(self, sync_point: int = 0): + + """ + Load the DAG + + This will clear the existing graph. + It will make web services calls to get the fresh graph, which will return a list of edges. + With the list of edges, it will create vertices and connect them with the edges. + The content of the edges will remain encrypted. The 'encrypted' flag is set to True. + We need the entire graph structure before decrypting. + + We don't have to worry about keys at this point. We are just trying to get structure + and content in the right place. Nothing is decrypted here. + + :param sync_point: Where to load + """ + + self._vertices = [] # type: List[DAGVertex] + self._uid_lookup = {} # type: dict[str, int] + + self.debug("# SYNC THE GRAPH ##################################################################", level=1) + + all_data, sync_point = self._sync(sync_point=sync_point) + + self.debug(" PROCESS the non-DATA edges", level=2) + + for data in all_data: + + edge_type = EdgeType.find_enum(data.type) + if edge_type == EdgeType.DATA: + continue + + tail_uid = data.ref.value + + head_uid = None + if data.parentRef is not None: + head_uid = data.parentRef.value + + self.debug(f" * edge {edge_type}, tail {tail_uid} to head {head_uid}", level=3) + + if not self.vertex_exists(tail_uid): + self.debug(f" * tail vertex {tail_uid} does not exists. create.", level=3) + self.add_vertex( + uid=tail_uid, + name=data.ref.name, + vertex_type=RefType.find_enum(data.ref.type) + ) + + tail = self.get_vertex(tail_uid) + + if head_uid is None: + head_uid = tail_uid + + if not self.vertex_exists(head_uid): + self.debug(f" * head vertex {head_uid} does not exists. create.", level=3) + self.add_vertex( + uid=head_uid, + name=data.parentRef.name, + vertex_type=RefType.GENERAL + ) + head = self.get_vertex(head_uid) + self.debug(f" * tail {tail_uid} belongs to {head_uid}, " + f"edge type {edge_type}", level=3) + + if edge_type == EdgeType.DELETION: + tail.disconnect_from(head) + else: + content = data.content + if content is not None: + if data.content_is_base64: + content = utils.base64_url_decode(content) + + tail.belongs_to( + vertex=head, + edge_type=edge_type, + content=content, + is_encrypted=False, + path=data.path, + modified=False, + from_load=True + ) + + self.debug("", level=2) + self.debug(" PROCESS the DATA edges", level=2) + + for data in all_data: + + edge_type = EdgeType.find_enum(data.type) + if edge_type != EdgeType.DATA: + continue + + tail_uid = data.ref.value + + if not self.vertex_exists(tail_uid): + self.debug(f" * tail vertex {tail_uid} does not exists. create.", level=3) + self.add_vertex( + uid=tail_uid, + name=data.ref.name, + vertex_type=RefType.find_enum(data.ref.type) + ) + tail = self.get_vertex(tail_uid) + + content = data.content + if content is not None: + if data.content_is_base64: + content = utils.base64_url_decode(content) + + self.debug(f" * DATA edge belongs to {tail.uid}", level=3) + tail.add_data( + content=content, + is_encrypted=True, + path=data.path, + modified=False, + from_load=True, + ) + + self.debug("", level=1) + + return sync_point + + def _mark_deletion(self): + """ + Mark vertices as deleted. + + Check each vertex to see if there is any non-DELETION edge connecting to another vertex. + If there are no edges, then the vertex is flagged at deleted. + + This is done to prevent the edges from being connected to a deleted vertex. + Also, to display deleted vertex in the DOT graph. + :return: + """ + + self.debug(" CHECK dag vertices to see if they are active", level=1) + for vertex in self.all_vertices: + + self.debug(f"check vertex {vertex.uid}", level=3) + found_edge_to_another_vertex = False + for edge in vertex.edges: + # Skip the DELETION and DATA edges. + if edge.edge_type == EdgeType.DELETION or edge.edge_type == EdgeType.DATA: + continue + + # Check if this edge has a matching DELETION edge. + # If it does not, this vertex cannot be deleted. + if not edge.is_deleted: + found_edge_to_another_vertex = True + break + + # If the vertex belongs to no vertex, and it is not the root, then flag it for deletion. + if found_edge_to_another_vertex is False and vertex.uid != self.uid: + self.debug(f" * vertex is deleted", level=3) + vertex.active = False + + self.debug("", level=1) + + def _decrypt_keychain(self): + """ + Decrypt KEY/ACL edges + """ + + self.debug(" DECRYPT the dag KEY edges", level=1) + + def _get_keychain(v): + self.debug(f" * looking at {v.uid}", level=3) + + # If the vertex has a decrypted key, then return it. + if v.has_decrypted_keys is True: + self.debug(" found a decrypted keychain on vertex", level=3) + return v.keychain + + found_key_edge = False + for e in v.edges: + if e.edge_type == EdgeType.KEY: + + self.debug(f" has edge that is a key, check head vertex {e.head_uid}", level=3) + head = self.get_vertex(e.head_uid) + keychain = _get_keychain(head) + + self.debug(f" * decrypt {v.uid} with keys {keychain}", level=3) + was_able_to_decrypt = False + + for key in keychain: + try: + self.debug(f" decrypt with key {key}", level=3) + content = dag_crypto.decrypt_aes(e.content, key) + self.debug(f" content {content}", level=3) + v.add_to_keychain(content) + self.debug(f" * vertex {v.uid} keychain is {v.keychain}", level=3) + was_able_to_decrypt = True + found_key_edge = True + break + except (Exception,): + self.debug(f" !! this is not the key", level=3) + + if not was_able_to_decrypt: + e.corrupt = True + v.corrupt = True + self.corrupt_uids.append(v.uid) + if self.fail_on_corrupt: + raise DAGKeyException(f"Could not decrypt vertex {v.uid} keychain for edge path {e.path}") + return [] + + if found_key_edge: + return v.keychain + else: + self.debug(" * using record bytes", level=3) + return [self.key] + + for vertex in self.all_vertices: + if not vertex.has_key: + continue + self.debug(f"vertex {vertex.uid}, {vertex.has_key}, {vertex.has_decrypted_keys}", level=3) + vertex.keychain = _get_keychain(vertex) + self.debug(f" setting keychain to {vertex.keychain}", level=3) + + self.debug("", level=1) + + def _decrypt_data(self): + """ + Decrypt DATA edges + + This key is used to decrypt the DATA edge's content. + Walk each vertex and decrypt the DATA edge if there is a DATA edge. + """ + + self.debug(" DECRYPT the dag data", level=1) + for vertex in self.all_vertices: + if not vertex.has_data: + continue + self.debug(f"vertex {vertex.uid}, {vertex.keychain}", level=3) + + for edge in vertex.edges: + if edge.edge_type != EdgeType.DATA: + continue + + if vertex.corrupt: + self.debug(f"the key for the DATA edge is corrupt for vertex {vertex.uid}; " + "cannot decrypt data.", level=3) + continue + + if not edge.is_encrypted: + raise ValueError("The content has already been decrypted.") + + content = edge.content + + self.debug(f" * enc safe content {content}", level=3) + self.debug(f" * enc {content}, enc key {vertex.keychain}", level=3) + able_to_decrypt = False + + keychain = vertex.keychain + + for key in keychain: + try: + edge.content = dag_crypto.decrypt_aes(content, key) + able_to_decrypt = True + self.debug(f" * content {edge.content}", level=3) + break + except (Exception,): + self.debug(f" !! this is not the key", level=3) + + if not able_to_decrypt: + if self.data_requires_encryption: + self.corrupt_uids.append(vertex.uid) + raise DAGDataException(f"The data edge {vertex.uid} could not be decrypted.") + + edge.content = content + edge.needs_encryption = False + self.debug(f" * edge is not encrypted or key is incorrect.") + + edge.is_encrypted = False + + self.debug("", level=1) + + def _flag_as_not_modified(self): + """ + Flag all edges as not modified. + + :return: + """ + for vertex in self.all_vertices: + for edge in vertex.edges: + edge.modified = False + + def load(self, sync_point: int = 0) -> int: + """ + Load data from the graph. + + The first step is to recreate the structure of the graph. + The second step is mark vertex as deleted. + The third step is to decrypt the KEY/ACL/DATA edges. + Forth is to flag all edges as not modified. + + :return: The sync point of the graph stream + """ + self.allow_auto_save = False + + self.debug("== LOAD DAG ========================================================================", level=2) + sync_point = self._load(sync_point) + self.debug(f"sync point is {sync_point}") + self._mark_deletion() + if self.decrypt: + self._decrypt_keychain() + self._decrypt_data() + else: + self.logger.info("the DAG has not been decrypted, the decrypt flag was get to False") + self._flag_as_not_modified() + self.debug("====================================================================================", level=2) + + self.allow_auto_save = True + + self.last_sync_point = sync_point + + return sync_point + + def _make_delta_graph(self, duplicate_data: bool = True): + + self.debug("DELTA GRAPH", level=3) + modified_vertices = [] + for vertex in self.all_vertices: + found_modification = False + for edge in vertex.edges: + if edge.skip_on_save: + continue + if edge.modified: + found_modification = True + break + if found_modification: + modified_vertices.append(vertex) + if len(modified_vertices) == 0: + self.debug("nothing has been modified") + return + + self.debug(f"has {len(modified_vertices)} vertices", level=3) + + def _flag(v: DAGVertex): + + self.debug(f"check vertex {v.uid}", level=3) + if v.uid == self.uid: + self.debug(f" FOUND ROOT", level=3) + return True + + found_path = False + for edge_type in [EdgeType.KEY, EdgeType.ACL, EdgeType.LINK]: + seen = {} + for e in v.edges: + self.debug(f" checking {e.edge_type}, {v.uid} to {e.head_uid}", level=3) + is_deletion = None + if e.edge_type == edge_type: + self.debug(f" found {edge_type}", level=3) + next_vertex = self.get_vertex(e.head_uid) + + if is_deletion is None: + version, highest_edge = v.get_highest_edge_version(next_vertex.uid) + is_deletion = highest_edge.edge_type == EdgeType.DELETION + if is_deletion: + self.debug(f" highest deletion edge. will not mark any edges as modified", + level=3) + + found_path = _flag(next_vertex) + if found_path is True and seen.get(e.head_uid) is None: + self.debug(f" setting {v.uid}, {edge_type} active", level=3) + if not is_deletion: + e.modified = True + seen[e.head_uid] = True + else: + self.debug(f" edge is not {edge_type}", level=3) + + if found_path is True: + break + + if found_path is True and duplicate_data is True: + for e in v.edges: + if e.edge_type == EdgeType.DATA: + e.modified = True + break + + return found_path + + self.logger.debug("BEGIN delta graph edge detection") + for modified_vertex in modified_vertices: + _flag(modified_vertex) + self.logger.debug("END delta graph edge detection") + + def save(self, confirm: bool = False, delta_graph: bool = False): + """ + Save the graph + + The save process will only save edges that have been flagged as modified, or are newly added. + The process will get the edges from all vertices. + The UID of the vertex is the tail UID of the edge. + For DATA edges, the key (first key in the keychain) will be used for encryption. + + If the web service takes too long or hangs, the batch_count can be used to reduce the amount the web service + needs to handle per request. If set to None or non-positive value, it will not send in batches. + + :param confirm: Confirm save. + Only need this when deleting all vertices. + :param delta_graph: Make a standalone graph from the modifications. + Use sync points to load this graph. + + :return: + """ + + self.debug("== SAVE GRAPH ========================================================================", level=2) + + if self.is_corrupt: + self.logger.error(f"the graph is corrupt, there are problem UIDs: {','.join(self.corrupt_uids)}") + raise DAGCorruptException(f"Cannot save. Graph steam uid {self.uid}, " + f"graph {self.write_endpoint}:{self.graph_id} " + f"has corrupt vertices: {','.join(self.corrupt_uids)}") + + root_vertex = self.get_vertex(self.uid) + if root_vertex is None: + raise DAGVertexException("Cannot save. Could not find the root vertex.") + + if root_vertex.vertex_type != RefType.PAM_NETWORK and root_vertex.vertex_type != RefType.PAM_USER: + raise DAGVertexException("Cannot save. Root vertex type needs to be PAM_NETWORK or PAM_USER.") + + if self.need_save_confirm is True and confirm is False: + raise DAGConfirmException("Cannot save. Confirmation is required.") + self.need_save_confirm = False + + if delta_graph: + self._make_delta_graph() + + data_list = [] + + def _add_data(vertex): + self.debug(f"processing vertex {vertex.uid}, key {vertex.key}, type {vertex.vertex_type}", level=3) + + uid = vertex.uid + for edge in vertex.edges: + + if edge.skip_on_save: + continue + + self.debug(f" * edge {edge.edge_type.value}, head {edge.head_uid}, tail {vertex.uid}", level=3) + + if not edge.modified: + self.debug(f" not modified, not saving.", level=3) + continue + + content = edge.content + + if self.decrypt: + if edge.edge_type == EdgeType.DATA: + self.debug(f" edge is data, encrypt data: {edge.needs_encryption}", level=3) + if isinstance(content, dict): + content = json.dumps(content).encode() + if isinstance(content, str): + content = content.encode() + + if edge.needs_encryption is True or self.data_requires_encryption is True: + self.debug(f" content {edge.content}, enc key {vertex.key}", level=3) + content = dag_crypto.encrypt_aes(content, vertex.key) + self.debug(f" enc content {content}", level=3) + + self.debug(f" enc safe content {content}", level=3) + elif edge.edge_type == EdgeType.KEY: + self.debug(f" edge is key or acl, encrypt key", level=3) + head_vertex = self.get_vertex(edge.head_uid) + key = head_vertex.key + if key is None: + self.debug(f" the edges head vertex {edge.head_uid} did not have a key. " + "using root dag key.", level=3) + key = self.key + self.debug(f" key {vertex.key}, enc key {key}", level=3) + content = dag_crypto.encrypt_aes(vertex.key, key) + elif edge.edge_type == EdgeType.ACL: + content = edge.content + else: + self.debug(f" edge is {edge.edge_type}", level=3) + + parent_vertex = self.get_vertex(edge.head_uid) + + if content is not None and len(content) > 65_535: + self.debug(f" !! vertex {vertex.uid} data edge is {len(content)}, over 64K.") + raise DAGDataException(f"vertex {vertex.uid} DATA edge is {len(content)} bytes. " + "This is too large for the MySQL BLOB (64K).") + + dag_data = self.write_struct_obj.data( + data_type=edge.edge_type, + content=content, + tail_uid=uid, + tail_ref_type=vertex.vertex_type, + tail_name=vertex.name if self.is_dev is True else None, + head_uid=edge.head_uid, + head_ref_type=parent_vertex.vertex_type, + head_name=parent_vertex.name if self.is_dev is True else None, + path=edge.path) + + data_list.append(dag_data) + + edge.modified = False + + _add_data(self.get_root) + + for v in self.all_vertices: + if v.skip_save is False: + if v.uid != self.uid: + _add_data(v) + + if len(data_list) > 0: + + if self.debug_level >= 4: + + self.debug("EDGE LIST") + self.debug("##############################################") + for data in data_list: + self.debug(f"{data.ref.value} -> {data.parentRef.value} ({data.type})") + self.debug("##############################################") + + self.debug(f"total list has {len(data_list)} items", level=0) + self.debug(f"batch {self.save_batch_count} edges", level=0) + + batch_num = 0 + while len(data_list) > 0: + + if self.save_batch_count > 0: + batch_list = data_list[:self.save_batch_count] + data_list = data_list[self.save_batch_count:] + + else: + batch_list = data_list + data_list = [] + + if len(batch_list) == 0: + break + + self.debug(f"adding {len(batch_list)} edges, batch {batch_num}", level=0) + + payload = self.write_struct_obj.payload( + origin_ref=self.write_struct_obj.origin_ref( + origin_ref_value=self.origin_ref_value, + name=self.name if self.is_dev is True else None + ), + data_list=batch_list, + graph_id=self.graph_id + ) + + try: + self.conn.add_data(payload, + graph_id=self.graph_id, + endpoint=self.write_endpoint, + agent=self.agent) + except Exception as err: + self.logger.error(f"could not add data to graph for batch {batch_num}: {err}") + raise err + + batch_num += 1 + + else: + self.debug("data list was empty, not saving.", level=2) + + self.debug("====================================================================================", level=2) + + def do_auto_save(self): + if not self.allow_auto_save: + self.debug("cannot auto_save, allow_auto_save is False.", level=3) + return + if self.auto_save: + self.debug("... dag auto saving", level=1) + self.save() + + def add_vertex(self, name: Optional[str] = None, uid: Optional[str] = None, keychain: Optional[List[bytes]] = None, + vertex_type: RefType = RefType.GENERAL) -> DAGVertex: + """ + Add a vertex to the graph. + + :param name: Name for the vertex. + :param uid: String unique identifier. + It's a 16bit hex value that is base64 encoded. + :param keychain: List if key bytes to use for encryption/description. This is set by the load/save method. + :param vertex_type: A RefType enumeration type. If blank, it will default to GENERAL. + :return: + """ + + if name is None: + name = uid + + vertex = DAGVertex( + name=name, + dag=self, + uid=uid, + keychain=keychain, + vertex_type=vertex_type + ) + if self.vertex_exists(vertex.uid): + raise DAGVertexAlreadyExistsException(f"Vertex {vertex.uid} already exists.") + + self._uid_lookup[vertex.uid] = len(self._vertices) + self._vertices.append(vertex) + + return vertex + + @property + def is_modified(self) -> bool: + for vertex in self.all_vertices: + for edge in vertex.edges: + if edge.modified is True: + return True + return False + + @property + def modified_edges(self): + edges = [] + for vertex in self.all_vertices: + for edge in vertex.edges: + if edge.modified is True: + edges.append(edge) + return edges + + def delete(self): + """ + Delete the entire graph. + + This will delete all the vertex, which will delete all the edges. + This will not automatically save. + The save method will need to be called. + The save will require the 'confirm' parameter to be set to True. + :return: + """ + for vertex in self.vertices: + vertex.delete() + self.need_save_confirm = True + + def _search(self, content: Any, value: QueryValue, ignore_case: bool = False): + + if isinstance(value, dict): + if not isinstance(content, dict): + return False + for next_key, next_value in value.items(): + if next_key not in content: + return False + if not self._search(content=content[next_key], + value=next_value, + ignore_case=ignore_case): + return False + return True + elif isinstance(value, list): + for next_value in value: + if self._search(content=content, + value=next_value, + ignore_case=ignore_case): + return True + return False + else: + content = str(content) + value = str(value) + if ignore_case: + content = content.lower() + value = value.lower() + + return value in content + + def search_content(self, query, ignore_case: bool = False): + results = [] + for vertex in self.vertices: + if vertex.has_data is False or vertex.active is False: + continue + content = vertex.content + if isinstance(query, bytes): + if query == content: + results.append(vertex) + elif isinstance(query, str): + try: + content = content.decode() + if query in content: + results.append(vertex) + continue + + except (Exception,): + pass + elif isinstance(query, dict): + try: + content = content.decode() + content = json.loads(content) + search_result = self._search(content, value=query, ignore_case=ignore_case) + if search_result: + results.append(vertex) + except (Exception,): + pass + else: + raise ValueError("Query is not an accepted type.") + return results + + def walk_down_path(self, path: Union[str, List[str]]) -> Optional[DAGVertex]: + """ + Walk the vertices using the path and return the vertex starting at root vertex. + + :param path: An array of path string, or string where the path is joined with a "/" + :return: DAGVertex is the path completes, None is failure. + """ + + self.debug("walking path starting at the root vertex", level=2) + vertex = self.get_vertex(self.uid) + return vertex.walk_down_path(path) + + def edge_count(self, only_active: bool = True, only_modified: bool = False) -> int: + """ + Return number of edges in graph. + + Edges that have been flagged as dups will not be included. + + :param only_active: Default True. If True, only edges that are active (not DELETION) are counted. + :param only_modified: Default False. If True, only edges that are new or have been modified are counted. + :return: + """ + + count = 0 + for v in self.all_vertices: + for e in v.edges: + if e.skip_on_save: + continue + if (only_active and not e.active) or (only_modified and not e.modified): + continue + count += 1 + + return count + + def to_dot(self, graph_format: str = "svg", show_hex_uid: bool = False, + show_version: bool = True, show_only_active: bool = False): + """ + Generate a graphviz Gigraph in DOT format that is marked up. + + :param graph_format: + :param show_hex_uid: + :param show_version: + :param show_only_active: + :return: + """ + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"GraphSync for {self.name}", format=graph_format) + dot.attr(rankdir='BT') + + for v in self._vertices: + if show_only_active is True and v.active is False: + continue + if not v.corrupt: + fillcolor = "white" + if not v.active: + fillcolor = "grey" + label = f"uid={v.uid}" + if v.name is not None and v.name != v.uid: + label += f"\\nname={v.name}" + if show_hex_uid: + label += f"\\nhex={dag_crypto.urlsafe_str_to_bytes(v.uid).hex()}" + else: + fillcolor = "red" + label = f"{v.uid} (CORRUPT)" + + dot.node(v.uid, label, fillcolor=fillcolor, style="filled") + for edge in v.edges: + if edge.skip_on_save: + continue + + if not edge.corrupt: + color = "grey" + style = "solid" + + if edge.active: + color = "black" + style = "bold" + elif show_only_active: + continue + + if not edge.active: + color = "grey" + + if edge.edge_type == EdgeType.DELETION: + style = "dotted" + + label = DAG.EDGE_LABEL.get(edge.edge_type) + if label is None: + label = "UNK" + if edge.path is not None and edge.path != "": + label += f"\\npath={edge.path}" + if show_version: + label += f"\\ne{edge.version}" + else: + color = "red" + style = "solid" + label = f"{DAG.EDGE_LABEL.get(edge.edge_type)} (CORRUPT)" + + dot.edge(v.uid, edge.head_uid, label, style=style, fontcolor=color, color=color) + + return dot + + def to_dot_raw(self, graph_format: str = "svg", sync_point: int = 0, rank_dir="BT"): + """ + Generate a graphviz Gigraph in DOT format that is not (heavily) marked up. + + :param graph_format: + :param sync_point: + :param rank_dir: + :return: + """ + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"GraphSync for {self.name}", format=graph_format) + dot.attr(rankdir=rank_dir) + + all_data, sync_point = self._sync(sync_point=sync_point) + + for edge in all_data: + edge_type = edge.type + tail_uid = edge.ref.value + dot.node(tail_uid, tail_uid) + if edge.parentRef is not None: + head_uid = edge.parentRef.value + dot.edge(tail_uid, head_uid, edge_type) + else: + dot.edge(tail_uid, tail_uid, edge_type) + return dot diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_crypto.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_crypto.py new file mode 100644 index 00000000..890e23ca --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_crypto.py @@ -0,0 +1,52 @@ + +import base64 +from typing import Optional, Union +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +import os + + +def encrypt_aes(data: bytes, key: bytes, iv: bytes = None) -> bytes: + aesgcm = AESGCM(key) + iv = iv or os.urandom(12) + enc = aesgcm.encrypt(iv, data, None) + return iv + enc + + +def decrypt_aes(data: bytes, key: bytes) -> bytes: + aesgcm = AESGCM(key) + return aesgcm.decrypt(data[:12], data[12:], None) + + +def bytes_to_urlsafe_str(b: Union[str, bytes]) -> str: + """ + Convert bytes to a URL-safe base64 encoded string. + + Args: + b (bytes): The bytes to be encoded. + + Returns: + str: The URL-safe base64 encoded representation of the input bytes. + """ + if isinstance(b, str): + b = b.encode() + + return base64.urlsafe_b64encode(b).decode().rstrip('=') + + +def generate_random_bytes(length: int) -> bytes: + return os.urandom(length) + + +def generate_uid_bytes(length: int = 16) -> bytes: + return generate_random_bytes(length) + + +def generate_uid_str(uid_bytes: Optional[bytes] = None) -> str: + if uid_bytes is None: + uid_bytes = generate_uid_bytes() + return bytes_to_urlsafe_str(uid_bytes) + + +def urlsafe_str_to_bytes(s: str) -> bytes: + b = base64.urlsafe_b64decode(s + '==') + return b \ No newline at end of file diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_edge.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_edge.py new file mode 100644 index 00000000..cba13f35 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_edge.py @@ -0,0 +1,266 @@ +import json +import logging +from typing import Optional, Union, Any, TYPE_CHECKING + +import pydantic +from ..keeper_dag.dag_types import EdgeType +from ..keeper_dag.exceptions import DAGContentException + +if TYPE_CHECKING: + from ..keeper_dag.dag_vertex import DAGVertex + +class DAGEdge: + def __init__(self, + vertex: "DAGVertex", + edge_type: EdgeType, + head_uid: str, + version: int = 0, + content: Optional[Any] = None, + path: Optional[str] = None, + modified: bool = True, + block_content_auto_save: bool = False, + is_serialized: bool = False, + is_encrypted: bool = False, + needs_encryption: bool = False): + """ + Create an instance of DAGEdge. + + A primary key of the edge the vertex UID, the head UID, and edge_type. + + :param vertex: The DAGVertex instance that owns these edges. + :param edge_type: The enumeration EdgeType. Indicate the type of the edge. + :param head_uid: The vertex uid that has this edge's vertex. The vertex uid that the edge arrow points at. + :param version: Version of this edge. + :param content: The content of this edge. + :param path: Short tag about this edge. Do + :param modified: + :param block_content_auto_save: + :param is_serialized: From the load, is the content serialized from to a base64 string? + :param needs_encryption: Flag to indicate if the content needs to be encrypted. + :return: An instance of DAGEdge + """ + + # This is the vertex that owns this edge. + self.vertex = vertex + self.edge_type = edge_type + self.head_uid = head_uid + + # Flag to indicate if the edge has been modified. Used to determine if the edge should be part of saved data. + # Set this before setting the content, else setting the content will cause an auto save. + self._modified = None + self.modified = modified + + # Should this edge be skipped when saving. + # This could happen if we create a duplicate new or modified edge. + # We want to only save the newest duplicated edge, so skip prior ones. + self.skip_on_save: bool = False + + # Block auto save in the content setter. + # When creating an edge, don't save until the edge is added to the edge list. + self.block_content_auto_save = block_content_auto_save + + # Does this edge's content need encryption? + self.needs_encryption = needs_encryption + + # If the content is being populated from a the load() method, and the edge type is a KEY or DATA, then the + # content will be encrypted (str). + + # If the edge data need encryption, is _content, currently encrypted. + self.is_encrypted = is_encrypted + + # If the content could not be decrypted, set + self.corrupt = False + + # Is the content base64 encoded? + # For JSON non-DATA edges, it will be deserialized. + # For JSON DATA edges, it will be serialized and the decryption will deserialize it. + # For Protobuf all edges are deserialized; Protobuf does not serialize bytes. + self.is_serialized = is_serialized + + self._content = None # type: Optional[Any] + self.content = content + self.path = path + + self.version = version + + # If a higher version edge exists, this will be False. + # If True, this is the highest edge. + self.active = True + + def __str__(self) -> str: + return f"" + + def debug(self, msg, level=0): + self.vertex.dag.debug(msg, level=level) + + @property + def modified(self): + return self._modified + + @modified.setter + def modified(self, value): + if value is True: + self.debug(f"vertex {self.vertex.uid}, type {self.vertex.dag.__class__.EDGE_LABEL.get(self.edge_type)}, " + f"head {self.head_uid} has been modified", level=5) + else: + self.debug(f"vertex {self.vertex.uid}, type {self.vertex.dag.__class__.EDGE_LABEL.get(self.edge_type)}, " + f"head {self.head_uid} had modified RESET", level=5) + self._modified = value + + @property + def content(self) -> Optional[Union[str, bytes]]: + """ + Get the content of the edge. + If the content is a str, then the content is encrypted. + """ + return self._content + + @property + def content_as_dict(self) -> Optional[dict]: + """ + Get the content from the DATA edge as a dictionary. + :return: Content as a dictionary. + """ + + if self.is_encrypted: + raise DAGContentException("The content is still encrypted.") + + content = self.content + if content is not None: + try: + content = json.loads(content) + except Exception as err: + raise DAGContentException(f"Cannot decode JSON. Is the content a dictionary? : {err}") + return content + + @property + def content_as_str(self) -> Optional[str]: + """ + Get the content from the DATA edge as string + :return: + """ + + if self.is_encrypted: + raise DAGContentException("The content is still encrypted.") + + content = self.content + try: + content = content.decode() + except (Exception,): + pass + return content + + def content_as_object(self, + meta_class: pydantic._internal._model_construction.ModelMetaclass) -> Optional[pydantic.BaseModel]: + """ + Get the content as a pydantic based object. + + :param meta_class: The class to return + :return: + """ + + if self.is_encrypted: + raise DAGContentException("The content is still encrypted.") + + content = self.content_as_str + if content is not None: + content = meta_class.model_validate_json(self.content_as_str) + return content + + @content.setter + def content(self, value: Any): + + """ + Set the content in the edge. + + The content should be stored as bytes. + If the encrypted flag is set, the content will be stored as is. + Content that is a str type is encrypted data (A Base64, AES encrypted bytes, str) + """ + + self.debug(f"vertex {self.vertex.uid}, type {self.vertex.dag.__class__.EDGE_LABEL.get(self.edge_type)}, " + f"head {self.head_uid} setting content", level=2) + + if self.is_encrypted: + self.debug(" content is encrypted.", level=3) + self._content = value + return + + if self._content is not None: + raise DAGContentException("Cannot update existing content. Use add_data() to change the content.") + + if isinstance(value, dict): + value = json.dumps(value) + + if hasattr(value, "model_dump_json"): + value = value.model_dump_json() + + if isinstance(value, str): + value = value.encode() + + self._content = value + + def delete(self): + """ + Delete the edge. + + Deleting an edge does not remove the existing edge. + It will create another edge with the same tail and head, but will be type DELETION. + """ + + if not self.active: + return + + version, _ = self.vertex.get_highest_edge_version(head_uid=self.head_uid) + + for edge in self.vertex.edges: + edge.active = False + if self.vertex.dag.dedup_edge and edge.modified: + edge.skip_on_save = True + + all_modified_edges = True + for edge in self.vertex.edges: + if not edge.modified: + all_modified_edges = False + break + + if not all_modified_edges: + self.vertex.edges.append( + DAGEdge( + vertex=self.vertex, + edge_type=EdgeType.DELETION, + head_uid=self.head_uid, + version=version + 1 + ) + ) + + current_allow_auto_save = self.vertex.dag.allow_auto_save + self.vertex.dag.allow_auto_save = False + + if not self.vertex.belongs_to_a_vertex: + self.vertex.delete(ignore_vertex=self.vertex) + + self.vertex.dag.allow_auto_save = current_allow_auto_save + self.vertex.dag.do_auto_save() + + @property + def is_deleted(self) -> bool: + """ + Does this edge have a DELETION edge that has the same head? + + This should be used to check in a non-DELETION edge type has a matching DELETION edge. + :return: + """ + + if self.edge_type == EdgeType.DELETION: + logging.info(f"The edge is_deleted() just check if the DELETION edge is DELETION " + f"for vertex {self.vertex.uid}, head UID {self.head_uid}. Returned True, but code should " + "not be checking this edge.") + return True + + for edge in self.vertex.edges: + if edge.edge_type == EdgeType.DELETION and edge.head_uid == self.head_uid and edge.active is True: + return True + + return False diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_sort.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_sort.py new file mode 100644 index 00000000..7407a393 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_sort.py @@ -0,0 +1,117 @@ + + +import functools +import logging +import re +from typing import TYPE_CHECKING, List, Optional, Union +from .constants import VERTICES_SORT_MAP +from .dag_vertex import DAGVertex +from .dag_types import DiscoveryObject +Logger = Union[logging.RootLogger, logging.Logger] +if TYPE_CHECKING: + from .dag_vertex import DAGVertex + + +def sort_infra_name(vertices: List[DAGVertex]) -> List[DAGVertex]: + """ + Sort the vertices by name in ascending order. + """ + + def _sort(t1: DAGVertex, t2: DAGVertex): + t1_name = t1.content_as_dict.get("name") + t2_name = t2.content_as_dict.get("name") + if t1_name < t2_name: + return -1 + elif t1_name > t2_name: + return 1 + else: + return 0 + + return sorted(vertices, key=functools.cmp_to_key(_sort)) + + +def sort_infra_host(vertices: List[DAGVertex]) -> List[DAGVertex]: + """ + Sort the vertices by host name. + + Host name should appear first in ascending order. + IP should appear second in ascending order. + + """ + + def _is_ip(host: str) -> bool: + if re.match(r'^\d+\.\d+\.\d+\.\d+', host) is not None: + return True + return False + + def _make_ip_number(ip: str) -> int: + ip_port = ip.split(":") + parts = ip_port[0].split(".") + value = "" + for part in parts: + value += part.zfill(3) + return int(value) + + def _sort(t1: DAGVertex, t2: DAGVertex): + t1_name = t1.content_as_dict.get("name") + t2_name = t2.content_as_dict.get("name") + + if _is_ip(t1_name) and _is_ip(t2_name): + t1_num = _make_ip_number(t1_name) + t2_num = _make_ip_number(t2_name) + + if t1_num < t2_num: + return -1 + elif t1_num > t2_num: + return 1 + else: + return 0 + + # T1 is an IP, T2 is a host name + elif _is_ip(t1_name) and not _is_ip(t2_name): + return 1 + # T2 is not an IP and T2 is an IP + elif not _is_ip(t1_name) and _is_ip(t2_name): + return -1 + # T1 and T2 are host name + else: + if t1_name < t2_name: + return -1 + elif t1_name > t2_name: + return 1 + else: + return 0 + + return sorted(vertices, key=functools.cmp_to_key(_sort)) + + +def sort_infra_vertices(current_vertex: DAGVertex, logger: Optional[logging.Logger] = None) -> dict: + + if logger is None: + logger = logging.getLogger() + + record_type_to_vertices_map = {k: [] for k, v in VERTICES_SORT_MAP.items()} + + vertices = current_vertex.has_vertices() + logger.debug(f" found {len(vertices)} vertices") + for vertex in vertices: + if vertex.active is True: + content = DiscoveryObject.get_discovery_object(vertex) + logger.debug(f" * {content.description}") + for vertex in vertices: + if vertex.active is False: + logger.debug(" vertex is not active") + continue + + content_dict = vertex.content_as_dict + record_type = content_dict.get("record_type") + if record_type in record_type_to_vertices_map: + record_type_to_vertices_map[record_type].append(vertex) + + for k, v in VERTICES_SORT_MAP.items(): + if v["sort"] == "sort_infra_name": + record_type_to_vertices_map[k] = sort_infra_name(record_type_to_vertices_map[k]) + elif v["sort"] == "sort_infra_host": + record_type_to_vertices_map[k] = sort_infra_host(record_type_to_vertices_map[k]) + + return record_type_to_vertices_map diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_types.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_types.py new file mode 100644 index 00000000..bbe7f192 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_types.py @@ -0,0 +1,787 @@ +import base64 +import datetime +from enum import Enum +import json +import time +from pydantic import BaseModel +from typing import Any, List, Optional, Union, TYPE_CHECKING + +from . import dag_crypto + +if TYPE_CHECKING: + from .dag_vertex import DAGVertex + +class BaseEnum(Enum): + + @classmethod + def find_enum(cls, value: Union[Enum, str, int], default: Optional[Enum] = None): + if value is not None: + for e in cls: + if e == value or e.value == value: + return e + if hasattr(cls, str(value).upper()): + return getattr(cls, value.upper()) + return default + + +class PamGraphId(BaseEnum): + PAM = 0 + DISCOVERY_RULES = 10 + DISCOVERY_JOBS = 11 + INFRASTRUCTURE = 12 + SERVICE_LINKS = 13 + + +class PamEndpoints(BaseEnum): + PAM = "/graph-sync/pam" + DISCOVERY_RULES = "/graph-sync/discovery_rules" + DISCOVERY_JOBS = "/graph-sync/discovery_jobs" + INFRASTRUCTURE = "/graph-sync/infrastructure" + SERVICE_LINKS = "/graph-sync/service_links" + + +ENDPOINT_TO_GRAPH_ID_MAP = { + PamEndpoints.PAM.value: PamGraphId.PAM.value, + PamEndpoints.DISCOVERY_RULES.value: PamGraphId.DISCOVERY_RULES.value, + PamEndpoints.DISCOVERY_JOBS.value: PamGraphId.DISCOVERY_JOBS.value, + PamEndpoints.INFRASTRUCTURE.value: PamGraphId.INFRASTRUCTURE.value, + PamEndpoints.SERVICE_LINKS.value: PamGraphId.SERVICE_LINKS.value, +} + + +class SyncQuery(BaseModel): + streamId: Optional[str] = None # base64 of a user's ID who is syncing. + deviceId: Optional[str] = None + syncPoint: Optional[int] = None + graphId: Optional[int] = 0 + + +class RefType(BaseEnum): + # 0 + GENERAL = "general" + # 1 + USER = "user" + # 2 + DEVICE = "device" + # 3 + REC = "rec" + # 4 + FOLDER = "folder" + # 5 + TEAM = "team" + # 6 + ENTERPRISE = "enterprise" + # 7 + PAM_DIRECTORY = "pam_directory" + # 8 + PAM_MACHINE = "pam_machine" + # 9 + PAM_DATABASE = "pam_database" + # 10 + PAM_USER = "pam_user" + # 11 + PAM_NETWORK = "pam_network" + # 12 + PAM_BROWSER = "pam_browser" + # 13 + CONNECTION = "connetion" + # 14 + WORKFLOW = "workflow" + # 15 + NOTIFICATION = "notification" + # 16 + USER_INFO = "user_info" + # 17 + TEAM_INFO = "team_info" + # 18 + ROLE = "role" + + def __str__(self): + return self.value + + +class EdgeType(BaseEnum): + + """ + DAG data type enum + + * DATA - encrypted data + * KEY - encrypted key + * LINK - like a key, but not encrypted + * ACL - unencrypted set of access control flags + * DELETION - removal of the previous edge at the same coordinates + * DENIAL - an element that was shared through graph relationship, can be explicitly denied + * UNDENIAL - negates the effect of denial, bringing back the share + + """ + DATA = "data" + KEY = "key" + LINK = "link" + ACL = "acl" + DELETION = "deletion" + DENIAL = "denial" + UNDENIAL = "undenial" + + def __str__(self) -> str: + return str(self.value) + + +class Ref(BaseModel): + type: RefType + value: str + name: Optional[str] = None + + +class DAGData(BaseModel): + type: EdgeType + ref: Ref + parentRef: Optional[Ref] = None + content: Optional[str] = None + path: Optional[str] = None + + +class DataPayload(BaseModel): + origin: Ref + dataList: List + graphId: Optional[int] = 0 + + +class SyncDataItem(BaseModel): + ref: Ref + parentRef: Optional[Ref] = None + content: Optional[str] = None + content_is_base64: bool = True + type: Optional[str] = None + path: Optional[str] = None + deletion: Optional[bool] = False + + +class SyncData(BaseModel): + syncPoint: int + data: List[SyncDataItem] + hasMore: bool + + +class RecordField(BaseModel): + type: str + label: Optional[str] = None + value: List[Any] = [] + required: bool = False + + +class UserAclRotationSettings(BaseModel): + # Base64 JSON schedule + schedule: Optional[str] = "" + + # Base64 JSON, encrypted + pwd_complexity: Optional[str] = "" + + disabled: bool = False + + # If true, do not rotate the username/password on remote system, if it exists. + noop: bool = False + + # A list of SaaS Record configuration records. + saas_record_uid_list: List[str] = [] + + def set_pwd_complexity(self, complexity: Union[dict, str, bytes], record_key_bytes: bytes): + if isinstance(complexity, dict): + complexity = json.dumps(complexity) + if isinstance(complexity, str): + complexity = complexity.encode() + + if not isinstance(complexity, bytes): + raise ValueError("The complexity is not a dictionary, string or is bytes.") + + self.pwd_complexity = base64.b64encode(dag_crypto.encrypt_aes(complexity, record_key_bytes)).decode() + + def get_pwd_complexity(self, record_key_bytes: bytes) -> Optional[dict]: + if self.pwd_complexity is None or self.pwd_complexity == "": + return None + complexity_enc_bytes = base64.b64decode(self.pwd_complexity.encode()) + complexity_bytes = dag_crypto.decrypt_aes(complexity_enc_bytes, record_key_bytes) + return json.loads(complexity_bytes) + + def set_schedule(self, schedule: Union[dict, str]): + if isinstance(schedule, dict): + schedule = json.dumps(schedule) + self.schedule = schedule + + def get_schedule(self) -> Optional[dict]: + if self.pwd_complexity is None or self.pwd_complexity == "": + return None + return json.loads(self.schedule) + + +class UserAcl(BaseModel): + + belongs_to: bool = False + + is_admin: bool = False + + is_iam_user: Optional[bool] = False + + rotation_settings: Optional[UserAclRotationSettings] = None + + @staticmethod + def default(): + """ + Make an empty UserAcl that contains all the default values for the attributes. + """ + return UserAcl( + rotation_settings=UserAclRotationSettings() + ) + +class DiscoveryItem(BaseModel): + pass + + +class DiscoveryConfiguration(DiscoveryItem): + """ + This is very general. + We are not going to make a class for each configuration/provider. + Populate a dictionary for the important information (i.e., Network CIDR) + """ + type: str + info: dict + + allows_admin: bool = False + + +class DiscoveryUser(DiscoveryItem): + user: Optional[str] = None + dn: Optional[str] = None + database: Optional[str] = None + managed: bool = False + + # These are for directory services. + active: bool = True + expired: bool = False + source: Optional[str] = None + + password: Optional[str] = None + private_key: Optional[str] = None + private_key_passphrase: Optional[str] = None + + could_login: Optional[bool] = False + + +class FactsDirectory(BaseModel): + domain: str + software: Optional[str] = None + login_format: Optional[str] = None + + +class FactsId(BaseModel): + machine_id: Optional[str] = None + product_id: Optional[str] = None + board_serial: Optional[str] = None + + +class FactsNameUser(BaseModel): + name: str + user: str + + +class Facts(BaseModel): + name: Optional[str] = None + + # For devices + make: Optional[str] = None + model: Optional[str] = None + + directories: List[FactsDirectory] = [] + id: Optional[FactsId] = None + services: List[FactsNameUser] = [] + tasks: List[FactsNameUser] = [] + iis_pools: List[FactsNameUser] = [] + + @property + def has_services(self): + return self.services is not None and len(self.services) > 0 + + @property + def has_tasks(self): + return self.tasks is not None and len(self.tasks) > 0 + + @property + def has_iis_pools(self): + return self.iis_pools is not None and len(self.iis_pools) > 0 + + @property + def has_service_items(self): + return self.has_services or self.has_tasks or self.has_iis_pools + + +class DiscoveryMachine(DiscoveryItem): + host: str + ip: str + port: Optional[int] = None + os: Optional[str] = None + provider_region: Optional[str] = None + provider_group: Optional[str] = None + is_gateway: bool = False + allows_admin: bool = True + admin_reason: Optional[str] = None + facts: Optional[Facts] = None + + +class DiscoveryDatabase(DiscoveryItem): + host: str + ip: str + port: int + type: str + use_ssl: bool = False + database: Optional[str] = None + provider_region: Optional[str] = None + provider_group: Optional[str] = None + allows_admin: bool = True + admin_reason: Optional[str] = None + + +class DiscoveryDirectory(DiscoveryItem): + host: str + ip: str + ips: List[str] = [] + port: int + type: str + use_ssl: bool = False + provider_region: Optional[str] = None + provider_group: Optional[str] = None + allows_admin: bool = True + admin_reason: Optional[str] = None + + +class DiscoveryObject(BaseModel): + uid: str + id: str + object_type_value: str + record_uid: Optional[str] = None + parent_record_uid: Optional[str] = None + record_type: str + fields: List[RecordField] + ignore_object: bool = False + action_rules_result: Optional[str] = None + admin_uid: Optional[str] = None + shared_folder_uid: Optional[str] = None + name: str + title: str + description: str + notes: List[str] = [] + error: Optional[str] = None + stacktrace: Optional[str] = None + + missing_since_ts: Optional[int] = None + + allow_delete: bool = False + + access_user: Optional[DiscoveryUser] = None + + item: Union[DiscoveryConfiguration, DiscoveryUser, DiscoveryMachine, DiscoveryDatabase, DiscoveryDirectory] + + @property + def record_exists(self): + return self.record_uid is not None + + def get_field_value(self, label): + for field in self.fields: + if field.label == label or field.type == label: + value = field.value + if len(value) == 0: + return None + return field.value[0] + return None + + def set_field_value(self, label, value): + if not isinstance(value, list): + value = [value] + for field in self.fields: + if field.label == label or field.type == label: + field.value = value + return + raise ValueError(f"Cannot not find field with label {label}") + + @staticmethod + def get_discovery_object(vertex: "DAGVertex") -> "DiscoveryObject": + """ + Get DiscoveryObject with correct item instance. + + Pydantic doesn't like Unions on the item attribute. + Item needs to be validated using the correct class. + + :param vertex: + :return: + """ + + mapping = { + "pamUser": DiscoveryUser, + "pamDirectory": DiscoveryDirectory, + "pamMachine": DiscoveryMachine, + "pamDatabase": DiscoveryDatabase + } + + content_dict = vertex.content_as_dict + + if content_dict is None: + raise Exception(f"The discovery vertex {vertex.uid} does not have any content data.") + record_type = content_dict.get("record_type") + if record_type in mapping: + content_dict["item"] = mapping[record_type].model_validate(content_dict["item"]) + else: + content_dict["item"] = DiscoveryConfiguration.model_validate(content_dict["item"]) + + return DiscoveryObject.model_validate(content_dict) + + +class CredentialBase(BaseModel): + + user: Optional[Any] = None + dn: Optional[Any] = None + password: Optional[Any] = None + private_key: Optional[Any] = None + private_key_passphrase: Optional[Any] = None + database: Optional[Any] = None + + +class Settings(BaseModel): + + """ + credentials: List of Credentials used to test connections for resources. + default_shared_folder_uid: The default shared folder that should be used when adding records. + include_azure_aadds - Include Azure AD Domain Service. + skip_rules: Do not run the rule engine. + user_map: Map used to map found users to Keeper record UIDs + skip_machines: Do not discovery machines. + skip_databases: Do not discovery databases. + skip_directories: Do not discovery directoires. + skip_cloud_users - Skip cloud users like AWS IAM, or Azure Tenant users. + allow_resource_deletion - Allow discovery to remove resources. + allow_resource_deletion - Allow discovery to remove resources if missing. + allow_user_deletion - Allow discovery to remove users if missing. + resource_deletion_limit - Remove resource if not seen for # seconds; 0 will delete right away. + user_deletion_limit - Remove user right away if not seen for # seconds; 0 will delete right away. + """ + + credentials: List[CredentialBase] = [] + default_shared_folder_uid: Optional[str] = None + include_azure_aadds: bool = False + skip_rules: bool = False + user_map: Optional[List[dict]] = None + skip_machines: bool = False + skip_databases: bool = False + skip_directories: bool = False + skip_cloud_users: bool = False + + allow_resource_deletion: bool = False + allow_user_deletion: bool = False + + resource_deletion_limit: int = 0 + user_deletion_limit: int = 0 + + def set_user_map(self, obj): + if self.user_map is not None: + obj.user_map = self.user_map + + @property + def has_credentials(self): + return len(self.credentials) > 0 + +class DiscoveryDeltaItem(BaseModel): + uid: str + version: int + record_uid: Optional[str] = None + changes: Optional[dict] = None + + @property + def has_record(self) -> bool: + return self.record_uid is not None + +class DiscoveryDelta(BaseModel): + added: List[DiscoveryDeltaItem] = [] + changed: List[DiscoveryDeltaItem] = [] + deleted: List[DiscoveryDeltaItem] = [] + + +class JobItem(BaseModel): + job_id: str + start_ts: int + settings: Settings + end_ts: Optional[int] = None + success: Optional[bool] = None + resource_uid: Optional[str] = None + conversation_id: Optional[str] = None + error: Optional[str] = None + stacktrace: Optional[str] = None + + sync_point: Optional[int] = None + + delta: Optional[DiscoveryDelta] = None + + @property + def duration_sec(self) -> Optional[int]: + if self.end_ts is not None: + return self.end_ts - self.start_ts + return None + + @property + def start_ts_str(self): + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.start_ts)) + + @property + def end_ts_str(self): + if self.end_ts is not None: + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.end_ts)) + return "" + + @property + def duration_sec_str(self): + if self.is_running is True: + duration_sec = int(time.time()) - self.start_ts + else: + duration_sec = self.duration_sec + + if duration_sec is not None: + return str(datetime.timedelta(seconds=int(duration_sec))) + else: + return "" + + @property + def is_running(self): + return self.end_ts is None and self.start_ts is not None and self.success is None + + +class JobContent(BaseModel): + active_job_id: Optional[str] = None + job_history: List[JobItem] = [] + + +class PamGraphId(BaseEnum): + PAM = 0 + DISCOVERY_RULES = 10 + DISCOVERY_JOBS = 11 + INFRASTRUCTURE = 12 + SERVICE_LINKS = 13 + + +class RuleTypeEnum(BaseEnum): + ACTION = "action" + SCHEDULE = "schedule" + COMPLEXITY = "complexity" + + +class RuleActionEnum(BaseEnum): + PROMPT = "prompt" + ADD = "add" + IGNORE = "ignore" + + +class Statement(BaseModel): + field: str + operator: str + value: Any + + +class RuleItem(BaseModel): + name: Optional[str] = None + added_ts: Optional[int] = None + rule_id: Optional[str] = None + enabled: bool = True + priority: int = 0 + case_sensitive: bool = True + statement: List[Statement] + + engine_rule: Optional[object] = None + + @property + def added_ts_str(self): + return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.added_ts)) + + def search(self, search: str) -> bool: + for item in self.statement: + if search in item.field or search in item.value: + return True + + if search in self.rule_id.lower() or search == self.rule_action.value or search == str(self.priority): + return True + + return False + + def close(self): + try: + if self.engine_rule and hasattr(self.rule_engine, "close"): + self.engine_rule.close() + self.engine_rule = None + del self.engine_rule + except (Exception,): + pass + + def __del__(self): + self.close() + + +class ActionRuleItem(RuleItem): + action: Optional[RuleActionEnum] = RuleActionEnum.PROMPT + shared_folder_uid: Optional[str] = None + admin_uid: Optional[str] = None + + +class ScheduleRuleItem(RuleItem): + tag: str + + +class ComplexityRuleItem(RuleItem): + tag: str + + +class RuleSet(BaseModel): + rules: List[RuleItem] = [] + + @property + def count(self) -> int: + return len(self.rules) + + def __str__(self): + rule_set = [] + for item in self.rules: + rule_set.append(item.model_dump_json()) + + return "[" + ",\n" .join(rule_set) + "]" + + +class ActionRuleSet(RuleSet): + rules: List[ActionRuleItem] = [] + + +class ScheduleRuleSet(RuleSet): + rules: List[ScheduleRuleItem] = [] + + +class ComplexityRuleSet(RuleSet): + rules: List[ComplexityRuleItem] = [] + +class ServiceAcl(BaseModel): + is_service: bool = False + is_task: bool = False + is_iis_pool: bool = False + + def is_used(self): + return self.is_service or self.is_task or self.is_iis_pool + +class PromptActionEnum(BaseEnum): + ADD = "add" + IGNORE = "ignore" + SKIP = "skip" + +class DirectoryInfo(BaseModel): + directory_record_uids: List[str] = [] + directory_user_record_uids: List[str] = [] + + def has_directories(self) -> bool: + return len(self.directory_record_uids) > 0 + + +class NormalizedRecord(BaseModel): + """ + This class attempts to normalize KeeperRecord, TypedRecord, KSM Record into a normalized record. + """ + record_uid: str + record_type: str + title: str + fields: List[RecordField] = [] + note: Optional[str] = None + + def _field(self, field_type, label) -> Optional[RecordField]: + for field in self.fields: + value = field.value + if value is None or len(value) == 0: + continue + if field.label == field_type and value[0].lower() == label.lower(): + return field + return None + + def find_user(self, user): + + from .dag_utils import split_user_and_domain + + res = self._field("login", user) + if res is None: + user, _ = split_user_and_domain(user) + res = self._field("login", user) + + return res + + def find_dn(self, user): + return self._field("distinguishedName", user) + + +class PromptResult(BaseModel): + + # "add" and "ignore" are the only action + action: PromptActionEnum + + # The acl is only needs for pamUser record. + acl: Optional[UserAcl] = None + + # If the discovery object content has been modified, set it here. + content: Optional[DiscoveryObject] = None + + # Existing record that should be the admin. + record_uid: Optional[str] = None + + is_directory_user: bool = False + + note: Optional[str] = None + + +class BulkRecordAdd(BaseModel): + + title: str + + note: Optional[str] = None + + record: Any + record_type: str + + admin_uid: Optional[str] = None + + record_uid: str + parent_record_uid: Optional[str] = None + + shared_folder_uid: str + + +class BulkRecordConvert(BaseModel): + record_uid: str + parent_record_uid: Optional[str] = None + note: Optional[str] = None + + +class BulkRecordSuccess(BaseModel): + title: str + record_uid: str + + +class BulkRecordFail(BaseModel): + title: str + error: str + + +class BulkProcessResults(BaseModel): + success: List[BulkRecordSuccess] = [] + failure: List[BulkRecordFail] = [] + + @property + def has_failures(self) -> bool: + return len(self.failure) > 0 + + @property + def num_results(self) -> int: + return self.failure_count + self.success_count + + @property + def failure_count(self) -> int: + return len(self.failure) + + @property + def success_count(self) -> int: + return len(self.success) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_utils.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_utils.py new file mode 100644 index 00000000..72d1d6d4 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_utils.py @@ -0,0 +1,111 @@ +import os +from typing import List, Optional, Tuple +from .__version__ import __version__ + + +def value_to_boolean(value): + value = str(value) + if value.lower() in ['true', 'yes', 'on', '1']: + return True + elif value.lower() in ['false', 'no', 'off', '0']: + return False + else: + return None + + +def kotlin_bytes(data: bytes): + return [b if b < 128 else b - 256 for b in data] + + +def get_connection(**kwargs): + + """ + This method will return the proper connection based on the params passed in. + + If `ksm` and a KDNRM KSM instance, it will connect using keeper secret manager. + If `params` and a KeeperParam instance, it will connect using Commander. + If the env var `USE_LOCAL_DAG` is True, it will connect using the Local test DAG engine. + + It returns a child instance of the Connection class. + """ + + if kwargs.get("connection") is not None: + return kwargs.get("connection") + + vault = kwargs.get("vault") + logger = kwargs.get("logger") + if value_to_boolean(os.environ.get("USE_LOCAL_DAG")): + from ..keeper_dag.connection.local import Connection + conn = Connection(logger=logger) + else: + use_read_protobuf = kwargs.get("use_read_protobuf") + use_write_protobuf = kwargs.get("use_write_protobuf") + + if vault is not None: + from ..keeper_dag.connection.commander import Connection + conn = Connection(vault=vault, + logger=logger, + use_read_protobuf=use_read_protobuf, + use_write_protobuf=use_write_protobuf) + else: + raise ValueError("Must pass 'vault' for Keeper SDK. Found neither.") + return conn + + +def make_agent(text) -> str: + return f"{text}/{__version__}" + + +def split_user_and_domain(user: str) -> Tuple[Optional[str], Optional[str]]: + + if user is None: + return None, None + + domain = None + + if "\\" in user: + user_parts = user.split("\\", maxsplit=1) + user = user_parts[0] + domain = user_parts[1] + elif "@" in user: + user_parts = user.split("@") + domain = user_parts.pop() + user = "@".join(user_parts) + + return user, domain + + +def user_check_list(user: str, name: Optional[str] = None, source: Optional[str] = None) -> List[str]: + user, domain = split_user_and_domain(user) + user = user.lower() + + check_list = [user, f".\\{user}"] + if name is not None: + name = name.lower() + check_list += [name, f".\\{name}"] + if source is not None: + source = source.lower() + check_list.append(f"{source[:15]}\\{user}") + check_list.append(f"{user}@{source}") + netbios_parts = source.split(".") + if len(netbios_parts) > 1: + check_list.append(f"{netbios_parts[0][:15]}\\{user}") + check_list.append(f"{user}@{netbios_parts[0]}") + if domain is not None: + domain = domain.lower() + check_list.append(f"{domain[:15]}\\{user}") + check_list.append(f"{user}@{domain}") + domain_parts = domain.split(".") + if len(domain_parts) > 1: + check_list.append(f"{domain_parts[0][:15]}\\{user}") + check_list.append(f"{user}@{domain_parts[0]}") + + return list(set(check_list)) + + +def user_in_lookup(user: str, lookup: dict, name: Optional[str] = None, source: Optional[str] = None) -> bool: + + for check_user in user_check_list(user, name, source): + if check_user in lookup: + return True + return False diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_vertex.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_vertex.py new file mode 100644 index 00000000..a2e6958b --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/dag_vertex.py @@ -0,0 +1,809 @@ +from typing import Optional, Union, List, Any, Tuple, TYPE_CHECKING + +import pydantic +from .dag_types import EdgeType, RefType +from . import dag_crypto +from .dag_edge import DAGEdge +from .exceptions import DAGVertexException, DAGDeletionException, DAGIllegalEdgeException, DAGKeyException + +if TYPE_CHECKING: + from .dag import DAG + + +class DAGVertex: + + def __init__(self, dag: "DAG", uid: Optional[str] = None, name: Optional[str] = None, + keychain: Optional[bytes] = None, vertex_type: RefType = RefType.GENERAL): + + self.dag = dag + + if uid is None: + uid = dag_crypto.generate_uid_str() + else: + if len(uid) != 22: + raise ValueError(f"The uid {uid} is not a 22 characters in length.") + try: + b = dag_crypto.urlsafe_str_to_bytes(uid) + if len(b) != 16: + raise ValueError("not 16 bytes") + except Exception: + raise ValueError("The uid does not appear to be web-safe base64 string contains a 16 bytes value.") + + # If the UID is the root UID, make sure the vertex type is not general. + # The root vertex needs to be either PAM_NETWORK or PAM_USER, if not set to PAM_NETWORK. + if uid == self.dag.uid and (vertex_type != RefType.PAM_NETWORK and vertex_type != RefType.PAM_USER): + vertex_type = RefType.PAM_NETWORK + self.vertex_type = vertex_type + + if name is None: + name = uid + + self._uid = uid + self._name = name + + # The keychain is a list of keys that can be used. + # The keychain may contain multiple keys, when loading the default graph (graph_id) + # For normal editing, the keychain will contain only one key. + self._keychain = [] + if keychain is not None: + if not isinstance(keychain, list): + keychain = [keychain] + self._keychain += keychain + + self.corrupt = False + + self.edges: list[Optional[DAGEdge]] = [] + self.has_uid = [] + + self.active = True + + self._skip_save = False + + def __str__(self): + ret = f"Vertex {self.uid}\n" + ret += f" python instance id: {id(self)}\n" + ret += f" name: {self.name}\n" + ret += f" keychain: {self.keychain}\n" + ret += f" active: {self.active}\n" + ret += f" edges:\n" + for edge in self.edges: + ret += f" * type {self.dag.__class__.EDGE_LABEL.get(edge.edge_type)}" + ret += f", connect to {edge.head_uid}" + ret += f", path {edge.path}, " + ret += f", active: {edge.active}" + ret += f", modified: {edge.modified}" + ret += f", content: {'yes' if edge.content is not None else 'no'}" + ret += f", content type: {type(edge.content)}" + ret += "\n" + return ret + + def __repr__(self): + return f"" + + def debug(self, msg: str, level: int = 0): + self.dag.debug(msg, level=level) + + @property + def name(self) -> str: + """ + Get the name for vertex + + If the name is not defined, the UID will be returned. + The name is not persistent. + If loading a DAG, the name will not be set. + + :return: + """ + if self._name is not None: + return self._name + return self._uid + + @property + def key(self) -> Optional[Union[str, bytes]]: + """ + Get a single key from the keychain. + + :return: + """ + keychain = self.keychain + if len(keychain) > 0: + return self.keychain[0] + + return None + + @property + def skip_save(self): + return self._skip_save + + @skip_save.setter + def skip_save(self, value): + self._skip_save = value + + for vertex in self.has_vertices(): + vertex._skip_save = value + + def add_to_keychain(self, key: Union[str, bytes]): + """ + Add a key to the keychain + + :param key: A decrypted key bytes or encrypted key str + :return: + """ + if key not in self._keychain: + self._keychain.append(key) + + @property + def keychain(self) -> Optional[List[Union[str, bytes]]]: + """ + Get the keychain for the vertex. + + The key is stored on the edges, however, the key belongs to the vertex. + KEY and ACL edges from this vertex will have the same encrypted key. + It is simpler to store the key on the DAGVertex instance. + + The keychain in an array of keys. + When using graph_id = 0, different graphs that have the same UID will + have different keys. + When decrypting DATA edges, each key in the keychain will be tried. + + If the keychain has not been set, check if any edges exist that require a key. + If there are, then generate a random key. + The load process will populate the key. + If the vertex does not have a key in the keychain, it is because this is a newly + added vertex. + + If there are no edges that require a key, then return None. + """ + if self.dag.get_root == self: + self._keychain = [self.dag.key] + + elif len(self._keychain) == 0: + for e in self.edges: + if e.edge_type in [EdgeType.KEY, EdgeType.DATA]: + self._keychain.append(dag_crypto.generate_random_bytes(self.dag.__class__.UID_KEY_BYTES_SIZE)) + break + + return self._keychain + + @keychain.setter + def keychain(self, value: List[Union[str, bytes]]): + """ + Set the key in the vertex. + + The save method will use this key for any KEY/ACL edges. + A key of str type means it is encrypted. + """ + self._keychain = value + + @property + def has_decrypted_keys(self) -> Optional[bool]: + """ + If the vertex contains a KEY, ACL or DATA edge and if the key is bytes, then the key is decrypted. + If it is a str type, then it is encrypted. + """ + if len(self._keychain) > 0: + for e in self.edges: + if e.edge_type in [EdgeType.KEY, EdgeType.DATA]: + all_decrypted = True + for key in self._keychain: + if not isinstance(key, bytes): + all_decrypted = False + break + return all_decrypted + return None + + @property + def uid(self): + """ + Get the vertex UID. + """ + return self._uid + + def get_edge(self, vertex: "DAGVertex", edge_type: EdgeType) -> DAGEdge: + high_edge = None + high_version = -1 + for edge in self.edges: + if edge.head_uid == vertex.uid and edge.edge_type == edge_type: + if edge.version > high_version: + high_version = edge.version + high_edge = edge + return high_edge + + def get_highest_edge_version(self, head_uid: str) -> Tuple[int, Optional[DAGEdge]]: + """ + Find the highest DAGEdge version of all edge types. + + :param head_uid: + :return: + """ + + high_edge = None + high_version = -1 + for edge in self.edges: + if edge.head_uid == head_uid: + if edge.version > high_version: + high_edge = edge + high_version = edge.version + return high_version, high_edge + + def edge_count(self, vertex: "DAGVertex", edge_type: EdgeType) -> int: + """ + Get the number of edges between two vertices. + + :param vertex: + :param edge_type: + :return: + """ + count = 0 + for edge in self.edges: + if edge.head_uid == vertex.uid and edge.edge_type == edge_type: + count += 1 + return count + + def edge_by_type(self, vertex: "DAGVertex", edge_type: EdgeType) -> List[DAGEdge]: + edge_list = [] + for edge in self.edges: + if edge.edge_type == edge_type and edge.head_uid == vertex.uid: + edge_list.append(edge) + return edge_list + + @property + def has_data(self) -> bool: + """ + :return: True if vertex has a DATA edge. + """ + + for item in self.edges: + if item.edge_type == EdgeType.DATA: + return True + return False + + def get_data(self, index: Optional[int] = None) -> Optional[DAGEdge]: + """ + Get data edge + + If the index is None or 0, the latest data edge will be returned. + A positive and negative, non-zero, index will return the same data. + It will be the absolute value of the index from the latest data. + This means the 1 or -1 will return the prior data. + + If there is no data, None is returned. + + :param index: + :return: + """ + + data_list = self.edge_by_type(self, EdgeType.DATA) + data_count = len(data_list) + if data_count == 0: + return None + + if index is None or index == 0: + index = -1 + + elif index > 0: + index *= -1 + index -= 1 + + else: + index -= 1 + + try: + data = data_list[index] + except IndexError: + raise ValueError(f"The index is not valid. Currently there are {data_count} data edges") + + return data + + def add_data(self, + content: Any, + is_encrypted: bool = False, + is_serialized: bool = False, + path: Optional[str] = None, + modified: bool = True, + from_load: bool = False, + needs_encryption: bool = True): + + """ + Add a DATA edge to the vertex. + + :param content: The content to store in the DATA edge. + :param is_encrypted: Is the content encrypted? + :param is_serialized: Is the content base64 serialized? + :param path: Simple string tag to identify the edge. + :param modified: Does this modify the content? + By default, adding a DATA edge will flag that the edge has been modified. + If loading, modified will be set to False. + :param from_load: This call is being performed the load() method. + Do not validate adding data. + :param needs_encryption: Default is True. + Does the content need to be encrypted? + """ + + self.debug(f"connect {self.uid} to DATA edge", level=1) + + if not self.active: + # If deleted, there will not be a KEY to decrypt the data. + # Throw an exception if not from the loading method. + if not from_load: + raise DAGDeletionException("This vertex is not active. Cannot add DATA edge.") + # If from loading, do not add and do not throw an exception. + return + + # Make sure the vertex belongs before auto saving. If it does not belong, it is an orphan right now. + # This only is checked if using this module to create the graph. + if self.belongs_to_a_vertex is False and from_load is False: + raise DAGVertexException(f"Before adding data, connect this vertex {self.uid} to another vertex.") + + if needs_encryption: + found_key_edge = self.dag.get_root == self or from_load is True + if found_key_edge is False: + for edge in self.edges: + if edge.edge_type == EdgeType.KEY: + found_key_edge = True + if found_key_edge is False: + raise DAGKeyException(f"Cannot add DATA edge without a KEY edge for vertex {self.uid}.") + + version = 0 + prior_data = self.get_data() + if prior_data is not None: + version = prior_data.version + 1 + prior_data.active = False + + # Check if DATA has already been created/modified per this session. + if self.dag.dedup_edge and prior_data.modified: + prior_data.skip_on_save = True + if self.dag.dedup_edge_warning: + self.dag.debug("DATA edge added multiple times for session. stacktrace on what did it follows ...") + self.dag.debug_stacktrace() + + self.edges.append( + DAGEdge( + vertex=self, + edge_type=EdgeType.DATA, + head_uid=self.uid, + version=version, + content=content, + path=path, + modified=modified, + is_serialized=is_serialized, + is_encrypted=is_encrypted, + needs_encryption=needs_encryption + ) + ) + + if self.dag.history_level > 0: + data_count = self.data_count() + while data_count > self.dag.history_level: + for index in range(0, len(self.edges) - 1): + if self.edges[index].edge_type == EdgeType.DATA: + del self.edges[index] + data_count -= 1 + break + + self.dag.do_auto_save() + + def data_count(self): + return self.edge_count(self, EdgeType.DATA) + + def data_delete(self): + data_edge = self.get_edge(self, EdgeType.DATA) + if data_edge is None: + self.debug("cannot delete the data, no data edge exists.") + + data_edge.active = False + + self.belongs_to( + vertex=self, + edge_type=EdgeType.DELETION + ) + self.debug(f"deleted data edge for {self.uid}") + + @property + def latest_data_version(self): + version = -1 + for edge in self.edges: + if edge.edge_type == EdgeType.DATA and edge.version > version: + version = edge.version + return version + + @property + def content(self) -> Optional[Union[str, bytes]]: + """ + Get the content of the active DATA edge. + + If the content is a str, then the content is encrypted. + """ + data_edge = self.get_data() + if data_edge is None: + return None + return data_edge.content + + @property + def content_as_dict(self) -> Optional[dict]: + """ + Get the content from the active DATA edge as a dictionary. + :return: Content as a dictionary. + """ + data_edge = self.get_data() + if data_edge is None: + return None + return data_edge.content_as_dict + + @property + def content_as_str(self) -> Optional[str]: + """ + Get the content from the active DATA edge as a str. + :return: Content as a str. + """ + + data_edge = self.get_data() + if data_edge is None: + return None + return data_edge.content_as_str + + def content_as_object(self, + meta_class: pydantic._internal._model_construction.ModelMetaclass) -> Optional[pydantic.BaseModel]: + """ + Get the content as a pydantic based object. + + :param meta_class: The class to return + :return: + """ + data_edge = self.get_data() + if data_edge is None: + return None + + return data_edge.content_as_object(meta_class) + + @property + def has_key(self) -> bool: + """ + :return: True if vertex has a KEY or ACL edge. + """ + + for item in self.edges: + if item.edge_type == EdgeType.KEY: + return True + return False + + def belongs_to(self, + vertex: "DAGVertex", + edge_type: EdgeType, + content: Optional[Any] = None, + is_encrypted: bool = False, + path: Optional[str] = None, + modified: bool = True, + from_load: bool = False): + """ + Connect a vertex to another vertex (as the owner). + + This will create an edge between this vertex and the passed in vertex. + The passed in vertex will own this vertex. + + If the edge_type is a KEY or ACL, data will be treated as a key. If a DATA edge already exists, the + edge_type will be changed to a KEY, if not a KEY or ACL edge_type. + + :param vertex: The vertex has this vertex. + :param edge_type: The edge type that connects the two vertices. + :param content: Data to store as the edges content. + :param is_encrypted: Is the content encrypted? + :param path: Text tag for the edge. + :param modified: Does adding this edge modify the stored DAG? + :param from_load: Is being connected from load() method? + :return: + """ + + self.debug(f"connect {self.uid} to {vertex.uid} with edge type {edge_type.value}", level=1) + + if vertex is None: + raise ValueError("Vertex is blank.") + if self.uid == self.dag.uid and not (edge_type == EdgeType.DATA or edge_type == EdgeType.DELETION): + if not from_load: + raise DAGIllegalEdgeException(f"Cannot create edge to self for edge type {edge_type}.") + self.dag.debug(f"vertex {self.uid} , the root vertex, " + f"attempted to create '{edge_type.value}' edge to self, skipping.") + return + + # Cannot make an edge to the same vertex, unless the edge type is a DELETION. + # A DELETION edge to self is allowed. + if self.uid == vertex.uid and not (edge_type == EdgeType.DATA or edge_type == EdgeType.DELETION): + if not from_load: + raise DAGIllegalEdgeException(f"Cannot create edge to self for edge type {edge_type}.") + self.dag.debug(f"vertex {self.uid} attempted to make '{edge_type.value}' to self, skipping.") + return + + version, version_edge = self.get_highest_edge_version(head_uid=vertex.uid) + + # If the new edge is not DELETION + if edge_type != EdgeType.DELETION: + current_edge_by_type = self.get_edge(vertex, edge_type) + if current_edge_by_type is not None: + current_edge_by_type.active = False + + if self.dag.dedup_edge and current_edge_by_type.modified: + current_edge_by_type.skip_on_save = True + if self.dag.dedup_edge_warning: + self.dag.debug(f"{edge_type.value.upper()} edge added multiple times for session. " + "stacktrace on what did it follows ...") + self.dag.debug_stacktrace() + + highest_deletion_edge = self.get_edge(vertex, EdgeType.DELETION) + if highest_deletion_edge is not None: + highest_deletion_edge.active = False + + if edge_type != EdgeType.DATA: + is_encrypted = False + + if not self.active: + + if edge_type == EdgeType.DELETION: + return + + if self.dag.dedup_edge and version_edge.modified: + version_edge.skip_on_save = True + if self.dag.dedup_edge_warning: + self.dag.debug("edge was deleted in session, will not save DELETION edge") + self.dag.debug_stacktrace() + else: + self.dag.debug(f"vertex {self.uid} was inactive; reactivating vertex.") + self.active = True + + # Create and append a new DAGEdge instance. + edge = DAGEdge( + vertex=self, + edge_type=edge_type, + head_uid=vertex.uid, + version=version + 1, + block_content_auto_save=True, + content=content, + is_encrypted=is_encrypted, + path=path, + modified=modified + ) + edge.block_content_auto_save = False + + self.edges.append(edge) + if self.uid not in vertex.has_uid: + vertex.has_uid.append(self.uid) + + self.dag.do_auto_save() + + def belongs_to_root(self, + edge_type: EdgeType, + path: Optional[str] = None): + """ + Connect the vertex to the root vertex. + + :param edge_type: The type of edge to use for the connection. + :param path: Short tag for this edge. + :return: + """ + + self.debug(f"connect {self.uid} to root", level=1) + + if self.uid == self.dag.uid: + raise DAGIllegalEdgeException("Cannot create edge to self.") + + if not self.active: + raise DAGDeletionException("This vertex is not active. Cannot connect to root.") + + self.belongs_to(self.dag.get_root, edge_type=edge_type, path=path) + + self.dag.allow_auto_save = True + self.dag.do_auto_save() + + def has_vertices(self, edge_type: Optional[EdgeType] = None, allow_inactive: bool = False, + allow_self_ref: bool = False) -> List["DAGVertex"]: + """ + Get a list of vertices that belong to this vertex. + :return: List of DAGVertex + """ + + vertices = [] + for uid in self.has_uid: + + if uid == self.uid and allow_self_ref is False: + continue + + vertex = self.dag.get_vertex(uid) + if edge_type is not None: + edge = vertex.get_edge(self, edge_type=edge_type) + if edge is not None: + vertices.append(vertex) + + elif edge_type != EdgeType.DATA and edge_type != EdgeType.DELETION: + if vertex.active is True or allow_inactive is True: + vertices.append(vertex) + + return vertices + + def has(self, vertex: "DAGVertex", edge_type: Optional[EdgeType] = None) -> bool: + """ + :return: True if request vertex belongs to this vertex. + False if it does not. + """ + + vertices = self.has_vertices(edge_type=edge_type) + return vertex in vertices + + def belongs_to_vertices(self) -> List["DAGVertex"]: + """ + Get a list of vertices that this vertex belongs to + :return: + """ + + vertices = [] + for edge in self.edges: + if edge.edge_type != EdgeType.DATA and edge.edge_type != EdgeType.DELETION and edge.active is True: + + vertex = self.dag.get_vertex(edge.head_uid) + if vertex.active is True and vertex not in vertices: + vertices.append(vertex) + return vertices + + @property + def belongs_to_a_vertex(self) -> bool: + """ + Does this vertex belong to another vertex? + :return: + """ + if self.dag.get_root == self: + return True + + return len(self.belongs_to_vertices()) > 0 + + def disconnect_from(self, vertex: "DAGVertex", path: Optional[str] = None): + """ + Disconnect this vertex from another vertex. + + This will add a DELETION edge between two vertices. + If the vertex no longer belongs to another vertex, the vertex will be deleted. + + :param vertex: The vertex this vertex belongs to + :param path: an Optional path for the DELETION edge. + :return: + """ + + if vertex is None: + raise ValueError("Vertex is blank.") + + for edge in self.edges: + if edge.head_uid == vertex.uid and edge.edge_type: + edge.active = False + + self.belongs_to( + vertex=vertex, + edge_type=EdgeType.DELETION, + path=path + ) + + has_active_key_edge = False + for edge in self.edges: + if edge.edge_type == EdgeType.KEY and edge.active is True: + has_active_key_edge = True + break + if not has_active_key_edge: + for edge in self.edges: + if edge.edge_type == EdgeType.DATA: + edge.active = False + + if not self.belongs_to_a_vertex: + self.debug(f"vertex {self.uid} is now not active", level=1) + self.active = False + + def delete(self, ignore_vertex: Optional["DAGVertex"] = None): + """ + Delete a vertex + + Deleting a vertex will inactivate the vertex. + It will also inactivate any vertices, and their edges, that belong to the vertex. + It will not inactivate a vertex that belongs to multiple vertices. + :return: + """ + + def _delete(vertex, prior_vertex): + if vertex.uid == self.dag.uid: + self.debug(f" * vertex is root, cannot delete root", level=2) + return + + self.debug(f"> checking vertex {vertex.uid}") + + if ignore_vertex is not None and vertex.uid == ignore_vertex.uid: + return + + has_v = vertex.has_vertices() + + if len(has_v) > 0: + self.debug(f" * vertex has {len(has_v)} vertices that belong to it.", level=2) + for v in has_v: + self.debug(f" checking {v.uid}") + _delete(v, vertex) + else: + self.debug(f" * vertex {vertex.uid} has NO vertices.", level=2) + + for e in list(vertex.edges): + if e.edge_type != EdgeType.DATA and (prior_vertex is None or e.head_uid == prior_vertex.uid): + e.delete() + if vertex.belongs_to_a_vertex is False: + self.debug(f" * inactive vertex {vertex.uid}") + vertex.active = False + + self.debug(f"DELETING vertex {self.uid}", level=3) + + current_allow_auto_save = self.dag.allow_auto_save + self.dag.allow_auto_save = False + + _delete(self, None) + + self.dag.allow_auto_save = current_allow_auto_save + self.dag.do_auto_save() + + def walk_down_path(self, path: Union[str, List[str]]) -> Optional["DAGVertex"]: + """ + Walk the vertices using the path and return the vertex starting at this vertex. + + :param path: An array of path string, or string where the path is joined with a "/" + :return: DAGVertex is the path completes, None is failure. + """ + + self.debug(f"walking path in vertex {self.uid}", level=2) + + if isinstance(path, str): + self.debug("path is str, break into array", level=2) + if path.startswith("/"): + path = path[1:] + path = path.split("/") + + current_path = path[0] + path = path[1:] + self.debug(f"current path: {current_path}", level=2) + self.debug(f"path left: {path}", level=2) + + for edge in self.edges: + if edge.edge_type != EdgeType.DATA: + continue + if edge.path == current_path: + return self + + for vertex in self.has_vertices(): + self.debug(f"vertex {self.uid} has {vertex.uid}", level=2) + for edge in vertex.edges: + + if edge.path == current_path and edge.head_uid == self.uid: + + if len(path) == 0: + return vertex + + else: + return vertex.walk_down_path(path) + return None + + def get_paths(self) -> List[str]: + """ + Get paths from this vertex to vertex owned by this vertex. + :return: List of string paths + """ + + paths = [] + for vertex in self.has_vertices(): + for edge in vertex.edges: + if edge.path is None or edge.path == "": + continue + paths.append(edge.path) + + return paths + + def clean_edges(self): + """ + Recursively clean edges and break circular references. + + This method clears all edge lists and reference tracking to help + Python's garbage collector clean up circular references between + DAG, DAGVertex, and DAGEdge objects. + """ + for vertex in self.has_vertices(): + vertex.clean_edges() + + self.edges.clear() + self.has_uid.clear() diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/exceptions.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/exceptions.py new file mode 100644 index 00000000..d1ce951c --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/exceptions.py @@ -0,0 +1,80 @@ +from __future__ import annotations +from typing import Any, Optional + + +class DAGException(Exception): + + def __init__(self, msg: Any, uid: Optional[str] = None): + if not isinstance(msg, str): + msg = str(msg) + + self.msg = msg + self.uid = uid + + super().__init__(self.msg) + + def __str__(self): + return self.msg + + def __repr__(self): + return self.msg + + +class DAGKeyIsEncryptedException(DAGException): + pass + + +class DAGDataEdgeNotFoundException(DAGException): + pass + + +class DAGDeletionException(DAGException): + pass + + +class DAGConfirmException(DAGException): + pass + + +class DAGPathException(DAGException): + pass + + +class DAGVertexAlreadyExistsException(DAGException): + pass + + +class DAGContentException(DAGException): + pass + + +class DAGDefaultGraphException(DAGException): + pass + + +class DAGIllegalEdgeException(DAGException): + pass + + +class DAGKeyException(DAGException): + pass + + +class DAGDataException(DAGException): + pass + + +class DAGVertexException(DAGException): + pass + + +class DAGEdgeException(DAGException): + pass + + +class DAGCorruptException(DAGException): + pass + + +class DAGConnectionException(DAGException): + pass diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/infrastructure.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/infrastructure.py new file mode 100644 index 00000000..a0a32e0b --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/infrastructure.py @@ -0,0 +1,333 @@ + +import importlib +import logging +import os +import time +from typing import Any, Optional + +from .dag import DAG +from .dag_types import EdgeType, PamGraphId +from .dag_vertex import DAGVertex +from .dag_utils import get_connection, make_agent +from .exceptions import DAGVertexException +from ... import utils + + +logger = logging.getLogger() + + +class Infrastructure: + + """ + Create a graph of the infrastructure. + + The first run will create a full graph since the vertices do not exist. + Further discovery run will only show vertices that ... + * do not have vaults records. + * the data has changed. + * the ACL has changed. + + """ + + KEY_PATH = "infrastructure" + DELTA_PATH = "delta" + ADMIN_PATH = "ADMINS" + USER_PATH = "USERS" + + def __init__(self, record: Any, logger: Optional[Any] = None, history_level: int = 0, + debug_level: int = 0, fail_on_corrupt: bool = True, log_prefix: str = "GS Infrastructure", + save_batch_count: int = 200, agent: Optional[str] = None, + **kwargs): + + self.record = record + self._dag = None + if logger is None: + logger = logging.getLogger() + self.logger = logger + self.log_prefix = log_prefix + self.history_level = history_level + self.debug_level = debug_level + self.fail_on_corrupt = fail_on_corrupt + self.save_batch_count = save_batch_count + + self.auto_save = False + self.delta_graph = True + self.last_sync_point = -1 + + self.agent = make_agent("infra") + if agent is not None: + self.agent += "; " + agent + + self.conn = get_connection(logger=logger, **kwargs) + + @property + def dag(self) -> DAG: + if self._dag is None: + + self.logger.debug(f"loading the dag graph {PamGraphId.INFRASTRUCTURE.value}") + self.logger.debug(f"setting graph save batch count to {self.save_batch_count}") + + self._dag = DAG(conn=self.conn, + record=self.record, + graph_id=PamGraphId.INFRASTRUCTURE, + auto_save=self.auto_save, + logger=self.logger, + history_level=self.history_level, + debug_level=self.debug_level, + name="Discovery Infrastructure", + fail_on_corrupt=self.fail_on_corrupt, + log_prefix=self.log_prefix, + save_batch_count=self.save_batch_count, + agent=self.agent) + + return self._dag + + @property + def has_discovery_data(self) -> bool: + + if not self.dag.has_graph: + return False + + if not self.get_root.has_vertices(): + return False + + return True + + @property + def get_root(self) -> DAGVertex: + return self.dag.get_root + + @property + def get_configuration(self) -> DAGVertex: + try: + configuration = self.get_root.has_vertices()[0] + except (Exception,): + raise DAGVertexException("Could not find the configuration vertex for the infrastructure graph.") + return configuration + + @property + def sync_point(self): + return self._dag.load(sync_point=0) + + def load(self, sync_point: int = 0): + ts = time.time() + res = self.dag.load(sync_point=sync_point) or 0 + self.logger.debug(f"infrastructure took {time.time()-ts} secs to load") + return res + + def close(self): + """ + Clean up resources held by this Infrastructure instance. + Releases the DAG instance and connection to prevent memory leaks. + """ + if self._dag is not None: + self._dag = None + self.conn = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + def save(self, delta_graph: Optional[bool] = None): + if delta_graph is None: + delta_graph = self.delta_graph + + self.logger.debug(f"current sync point {self.last_sync_point}") + if delta_graph: + self.logger.debug("saving delta graph of the infrastructure") + ts = time.time() + self._dag.save(delta_graph=delta_graph) + self.logger.debug(f"infrastructure took {time.time()-ts} secs to save") + + def to_dot(self, graph_format: str = "svg", show_hex_uid: bool = False, + show_version: bool = True, show_only_active_vertices: bool = False, + show_only_active_edges: bool = False, sync_point: int = None, graph_type: str = "dot"): + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"DAG for Discovery", format=graph_format) + + if sync_point is None: + sync_point = self.last_sync_point + + self.logger.debug(f"generating infrastructure dot starting at sync point {sync_point}") + + self.dag.load(sync_point=sync_point) + + count = 0 + if len(self.dag.get_root.has_vertices()) > 0: + config_vertex = self.dag.get_root.has_vertices()[0] + count = len(config_vertex.has_vertices()) + + if graph_type == "dot": + dot.attr(rankdir='RL') + rank_sep = 10 + if count > 10: + rank_sep += int(count * 0.10) + dot.attr(ranksep=str(rank_sep)) + elif graph_type == "twopi": + rank_sep = 20 + if count > 20: + rank_sep += int(count * 0.10) + + dot.attr(layout="twopi") + dot.attr(ranksep=str(rank_sep)) + dot.attr(ratio="auto") + else: + dot.attr(layout=graph_type) + dot.attr(ranksep=10) + + for v in self.dag.all_vertices: + if show_only_active_vertices is True and v.active is False: + continue + + shape = "ellipse" + fillcolor = "white" + color = "black" + + if not v.corrupt: + + if not v.active: + fillcolor = "grey" + + record_type = None + record_uid = None + name = v.name + source = None + try: + data = v.content_as_dict + record_type = data.get("record_type") + record_uid = data.get("record_uid") + name = data.get("name") + item = data.get("item") + if item is not None: + if item.get("managed", False) is True: + shape = "box" + source = item.get("source") + if record_uid is not None: + fillcolor = "#AFFFAF" + if data.get("ignore_object", False): + fillcolor = "#DFDFFF" + except (Exception,): + pass + + label = f"uid={v.uid}" + if record_type is not None: + label += f"\\nrt={record_type}" + if name is not None and name != v.uid: + name = name.replace("\\", "\\\\") + label += f"\\nname={name}" + if source is not None: + label += f"\\nsource={source}" + if record_uid is not None: + label += f"\\nruid={record_uid}" + if show_hex_uid: + label += f"\\nhex={utils.base64_url_decode(v.uid).hex()}" + if v.uid == self.dag.get_root.uid: + fillcolor = "gold" + label += f"\\nsp={sync_point}" + + tooltip = f"ACTIVE={v.active}\\n\\n" + try: + content = v.content_as_dict + for k, val in content.items(): + if k == "item": + continue + if isinstance(val, str): + val = val.replace("\\", "\\\\") + tooltip += f"{k}={val}\\n" + + item = content.get("item") + if item is not None: + tooltip += f"------------------\\n" + for k, val in item.items(): + if isinstance(val, str): + val = val.replace("\\", "\\\\") + tooltip += f"{k}={val}\\n" + except Exception as err: + tooltip += str(err) + else: + fillcolor = "red" + label = f"{v.uid} (CORRUPT)" + tooltip = "CORRUPT" + + dot.node(v.uid, label, color=color, fillcolor=fillcolor, style="filled", shape=shape, tooltip=tooltip) + + head_uids = [] + for edge in v.edges: + + if edge.head_uid == v.uid: + continue + + if edge.head_uid not in head_uids: + head_uids.append(edge.head_uid) + + def _render_edge(e): + + edge_color = "grey" + style = "solid" + + if e.corrupt is False: + + if e.active is True: + edge_color = "black" + style = "bold" + elif show_only_active_edges: + return + + if e.edge_type == EdgeType.DATA and v.active is False: + edge_color = "grey" + + if e.edge_type == EdgeType.DELETION: + style = "dotted" + + edge_tip = "" + if e.edge_type == EdgeType.ACL and v.active is True: + edge_content = e.content_as_dict + for key, value in content.items(): + edge_tip += f"{key}={value}\\n" + if edge_content.get("is_admin") is True: + edge_color = "red" + + edge_label = DAG.EDGE_LABEL.get(e.edge_type) + if edge_label is None: + edge_label = "UNK" + if e.path is not None and e.path != "": + edge_label += f"\\npath={e.path}" + if show_version: + edge_label += f"\\nv={e.version}" + else: + edge_label = f"{e.edge_type.value} (CORRUPT)" + edge_color = "red" + edge_tip = "CORRUPT" + + dot.edge(v.uid, e.head_uid, edge_label, style=style, fontcolor=edge_color, color=edge_color, + tooltip=edge_tip) + + for head_uid in head_uids: + version, edge = v.get_highest_edge_version(head_uid) + _render_edge(edge) + + data_edge = v.get_data() + if data_edge is not None: + _render_edge(data_edge) + + return dot + + def render(self, name: str, **kwargs): + + output_name = os.environ.get("GRAPH_DIR", os.environ.get("HOME", os.environ.get("PROFILENAME", "."))) + output_name = os.path.join(output_name, name) + dot = self.to_dot(**kwargs) + dot.render(output_name) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/jobs.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/jobs.py new file mode 100644 index 00000000..eb24648a --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/jobs.py @@ -0,0 +1,480 @@ + +import base64 +import copy +import logging +import os +from time import time + + +from ..keeper_dag.dag_utils import get_connection, make_agent +from ..keeper_dag.dag import DAG, EdgeType +from ..keeper_dag.dag_types import PamGraphId, JobContent, Settings, JobItem, DiscoveryDelta +import importlib +from typing import Any, Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from ..keeper_dag.dag import DAGVertex + +class Jobs: + + KEY_PATH = "jobs" + + DELTA_SIZE = 48_000 + + HISTORY_LIMIT = 30 + + STACKTRACE_LIMIT = 20_000 + + ERROR_LIMIT = 10_000 + SUMMARY_ERROR_LIMIT = 40 + + def __init__(self, record: Any, logger: Optional[Any] = None, debug_level: int = 0, fail_on_corrupt: bool = True, + log_prefix: str = "GS Jobs", save_batch_count: int = 200, agent: Optional[str] = None, + **kwargs): + + self.conn = get_connection(logger=logger, **kwargs) + + self.record = record + self._dag = None + if logger is None: + logger = logging.getLogger() + logger.propagate = False + self.logger = logger + self.log_prefix = log_prefix + self.debug_level = debug_level + self.fail_on_corrupt = fail_on_corrupt + self.save_batch_count = save_batch_count + + self.agent = make_agent("jobs") + if agent is not None: + self.agent += "; " + agent + + @property + def dag(self) -> DAG: + if self._dag is None: + + self._dag = DAG(conn=self.conn, + record=self.record, + graph_id=PamGraphId.DISCOVERY_JOBS, + auto_save=False, + logger=self.logger, + debug_level=self.debug_level, + name="Discovery Jobs", + fail_on_corrupt=self.fail_on_corrupt, + log_prefix=self.log_prefix, + save_batch_count=self.save_batch_count, + agent=self.agent) + + ts = time() + self._dag.load() + self.logger.debug(f"jobs took {time() - ts} secs to load") + + if not self._dag.has_graph: + self._dag.allow_auto_save = False + status = self._dag.add_vertex() + status.belongs_to_root( + EdgeType.KEY, + path=Jobs.KEY_PATH) + status.add_data( + content=JobContent( + active_job_id=None, + job_history=[] + ), + ) + self._dag.save() + return self._dag + + def close(self): + """ + Clean up resources held by this Jobs instance. + Releases the DAG instance and connection to prevent memory leaks. + """ + if self._dag is not None: + self._dag = None + self.conn = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + @property + def data_path(self): + return f"/{Jobs.KEY_PATH}" + + def get_jobs(self): + + self.logger.debug("loading discovery jobs from DAG") + + vertex = self.dag.walk_down_path(self.data_path) + current_dict = vertex.content_as_dict + + if current_dict is None: + self.logger.debug(" there is no job content, creating empty job content") + vertex.add_data( + content=JobContent( + active_job_id=None, + job_history=[] + ), + ) + current_dict = vertex.content_as_dict + + for job in current_dict.get("job_history", []): + job["settings"] = {} + + return JobContent.model_validate(current_dict) + + def _chunk_delta_data(self, job_vertex: "DAGVertex", delta: DiscoveryDelta): + + delta_content = delta.model_dump_json() + self.logger.debug(f"job delta content is {len(delta_content)} bytes, chunk size is {Jobs.DELTA_SIZE} bytes") + + existing_delta_vertices = job_vertex.has_vertices() + if len(existing_delta_vertices) > 0: + self.logger.debug(f"job delta exists, remove old delta") + for delta_vertex in existing_delta_vertices: + delta_vertex.delete() + + chunk_num = 0 + while delta_content != "": + path = str(chunk_num) + + chunk = delta_content[:Jobs.DELTA_SIZE] + delta_content = delta_content[Jobs.DELTA_SIZE:] + + new_vertex = job_vertex.dag.add_vertex() + new_vertex.belongs_to(job_vertex, edge_type=EdgeType.KEY, path=path) + new_vertex.add_data(chunk) + + self.logger.debug(f" * vertex {new_vertex.uid}, chunk {chunk_num}, {len(chunk)} bytes") + + chunk_num += 1 + + def set_jobs(self, jobs: JobContent): + + self.logger.debug("saving discovery jobs to DAG") + + jobs_vertex = self.dag.walk_down_path(self.data_path) + + clean_jobs = [] + for job in jobs.job_history: + + job_vertex = jobs_vertex.walk_down_path(job.job_id) + if job_vertex is None: + self.logger.debug(f" create a job vertex for {job.job_id}") + job_vertex = jobs_vertex.dag.add_vertex() + job_vertex.belongs_to(jobs_vertex, edge_type=EdgeType.KEY, path=job.job_id) + else: + self.logger.debug(f" job vertex for {job.job_id} exists") + + if job.delta is not None: + self.logger.debug(" included discovery delta") + self._chunk_delta_data(job_vertex, job.delta) + job.delta = None + else: + self.logger.debug(" did not include discovery delta") + + if job.stacktrace is not None: + self.logger.debug(f"stacktrace is {len(job.stacktrace)} characters") + if len(job.stacktrace) > Jobs.STACKTRACE_LIMIT: + self.logger.debug(f" stacktrace too long; truncate to {Jobs.STACKTRACE_LIMIT} characters") + start = len(job.stacktrace) - Jobs.STACKTRACE_LIMIT + job.stacktrace = job.stacktrace[start:] + + if job.error is not None: + self.logger.debug(f"error is {len(job.error)} characters") + if len(job.error) > Jobs.ERROR_LIMIT: + self.logger.debug(f" error too long; truncate to {Jobs.ERROR_LIMIT} characters") + job.error = job.error[:Jobs.ERROR_LIMIT] + "..." + + job_vertex.add_data( + content=job + ) + + if job.error is not None and len(job.error) > Jobs.SUMMARY_ERROR_LIMIT: + job.error = job.error[:Jobs.SUMMARY_ERROR_LIMIT] + "..." + job.stacktrace = None + job.settings = Settings() + + clean_jobs.append(job) + + jobs.job_history = clean_jobs + jobs_vertex.add_data( + content=jobs + ) + + ts = time() + self.dag.save() + self.logger.debug(f"jobs took {time()-ts} secs to save") + + self.logger.debug(" finished saving") + + def _remove_old_history(self, job_history: List[JobItem], limit: int) -> List[JobItem]: + + self.logger.debug("clean up job history and migrate discovery delta") + + job_history = sorted(job_history, key=lambda j: j.start_ts) + + while (len(list(job_history))) > limit: + job = job_history[0] + self.logger.debug(f"remove job {job.job_id} item") + job_history = job_history[1:] + job_vertex = self.dag.walk_down_path(f"{self.data_path}/{job.job_id}") + if job_vertex is not None: + self.logger.debug(f"remove job {job.job_id} vertex") + job_vertex.delete() + + self.logger.debug(f"found {len(job_history)} items in job history") + + return job_history + + def start(self, settings: Optional[Settings] = None, resource_uid: Optional[str] = None, + conversation_id: Optional[str] = None) -> str: + """ + Start a discovery job. + """ + + self.logger.debug("starting a discovery job") + + if settings is None: + settings = Settings() + else: + + settings = copy.deepcopy(settings) + settings.user_map = None + + jobs = self.get_jobs() + + job_history = self._remove_old_history(jobs.job_history, limit=Jobs.HISTORY_LIMIT - 1) + + new_job = JobItem( + job_id="JOB" + base64.urlsafe_b64encode(os.urandom(8)).decode().rstrip('='), + start_ts=int(time()), + settings=settings, + resource_uid=resource_uid, + conversation_id=conversation_id, + delta=DiscoveryDelta() + ) + jobs.active_job_id = new_job.job_id + job_history.append(new_job) + jobs.job_history = job_history + + self.set_jobs(jobs) + + return new_job.job_id + + def get_job_content(self) -> JobContent: + jobs = self.dag.walk_down_path(path=self.data_path) + return jobs.content_as_object(JobContent) + + def get_job(self, job_id) -> Optional[JobItem]: + jobs = self.get_jobs() + for job in jobs.job_history: + if job.job_id == job_id: + + job_vertex = self.dag.walk_down_path(path=f"{self.data_path}/{job.job_id}") + if job_vertex is not None: + + try: + job = job_vertex.content_as_object(JobItem) + except Exception as err: + self.logger.debug(f"could not find job item on job vertex, use job histry entry: {err}") + + delta_lookup = {} + vertices = job_vertex.has_vertices() + self.logger.debug(f"found {len(vertices)} delta vertices") + for vertex in vertices: + edge = vertex.get_edge(job_vertex, edge_type=EdgeType.KEY) + delta_lookup[int(edge.path)] = vertex + + json_value = "" + + for key in sorted(delta_lookup): + json_value += delta_lookup[key].content_as_str + if json_value != "": + self.logger.debug(f"delta content length is {len(json_value)}") + job.delta = DiscoveryDelta.model_validate_json(json_value) + else: + self.logger.debug("could not find job vertex") + + if job.settings is None: + job.settings = Settings() + + return job + return None + + def error(self, job_id: str, error: Optional[str], stacktrace: Optional[str] = None): + + self.logger.debug("flag discovery job as error") + + jobs = self.get_jobs() + for job in jobs.job_history: + if job.job_id == job_id: + logging.debug("found job to add error message") + job.end_ts = int(time()) + job.success = False + job.error = error + job.stacktrace = stacktrace + + self.set_jobs(jobs) + + def finish(self, job_id: str, sync_point: int, delta: DiscoveryDelta): + + self.logger.debug("finish discovery job") + + jobs = self.get_jobs() + for job in jobs.job_history: + if job.job_id == job_id: + self.logger.debug("found job to finish") + job.sync_point = sync_point + job.end_ts = int(time()) + job.success = True + job.delta = delta + + self.set_jobs(jobs) + + def cancel(self, job_id): + + self.logger.debug("cancel discovery job") + + jobs = self.get_jobs() + for job in jobs.job_history: + if job.job_id == job_id: + self.logger.debug("found job to cancel") + if job.end_ts is None: + job.end_ts = int(time()) + jobs.active_job_id = None + self.set_jobs(jobs) + + @property + def history(self) -> List[JobItem]: + jobs = self.get_jobs() + return jobs.job_history + + @property + def job_id_list(self) -> List[str]: + return [j.job_id for j in self.history] + + @property + def current_job(self) -> Optional[JobItem]: + """ + Get the current job + The current job is the oldest unprocessed job + """ + jobs = self.get_jobs() + if jobs.active_job_id is None: + return None + return self.get_job(jobs.active_job_id) + + def __str__(self): + def _h(i: JobItem): + return f"Job ID: {i.job_id}, {i.success}, {i.sync_point} " + + ret = "HISTORY\n" + for item in self.history: + ret += _h(item) + return ret + + def to_dot(self, graph_format: str = "svg", show_version: bool = True, show_only_active_vertices: bool = True, + show_only_active_edges: bool = True, graph_type: str = "dot"): + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"DAG for Jobs", format=graph_format) + + if graph_type == "dot": + dot.attr(rankdir='RL') + elif graph_type == "twopi": + dot.attr(layout="twopi") + dot.attr(ranksep="10") + dot.attr(ratio="auto") + else: + dot.attr(layout=graph_type) + + self.logger.debug(f"have {len(self.dag.all_vertices)} vertices") + for v in self.dag.all_vertices: + + if show_only_active_vertices is True and v.active is False: + continue + + fillcolor = "white" + tooltip = "" + + for edge in v.edges: + + color = "grey" + style = "solid" + + if edge.active: + color = "black" + style = "bold" + elif show_only_active_edges: + continue + + if edge.edge_type == EdgeType.DATA: + if not v.active: + color = "grey" + elif v.has_data: + + try: + data = v.content_as_object(JobContent) # type: JobContent + if data.active_job_id is not None: + tooltip = f"Current Job Id: {data.active_job_id}\n"\ + f"History: \n" + for item in data.job_history: + tooltip += f" * {item.job_id}, {item.sync_point}, {item.start_ts_str}, "\ + f"{item.delta}, {item.error}\n" + fillcolor = "#FFFF00" + else: + fillcolor = "#CFCFFF" + except (Exception,): + try: + data = v.content_as_object(JobItem) # type: JobItem + if data.job_id is not None: + tooltip = f"Job Id: {data.job_id}\n" \ + f"Resource ID: {data.resource_uid}\n" \ + f"Start Ts: {data.start_ts}\n" \ + f"End Ts: {data.end_ts}\n" \ + f"Converstion ID: {data.conversation_id}\n" \ + f"Error: {data.error}\n" \ + f"Stack Trace: {data.stacktrace}\n" \ + f"Sync Point: {data.sync_point}\n" + fillcolor = "#FFFFF0" + else: + fillcolor = "#CFCFFF" + except (Exception,): + fillcolor = "#CFCFFF" + + if edge.edge_type == EdgeType.DELETION: + style = "dotted" + + label = DAG.EDGE_LABEL.get(edge.edge_type) + if label is None: + label = "UNK" + if edge.path is not None and edge.path != "": + label += f"\\npath={edge.path}" + if show_version: + label += f"\\nv={edge.version}" + + dot.edge(v.uid, edge.head_uid, label, style=style, fontcolor=color, color=color) + + shape = "ellipse" + + color = "black" + if not v.active: + fillcolor = "grey" + + label = f"uid={v.uid}" + dot.node(v.uid, label, color=color, fillcolor=fillcolor, style="filled", shape=shape, tooltip=tooltip) + + return dot diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/process.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/process.py new file mode 100644 index 00000000..56c53a34 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/process.py @@ -0,0 +1,1367 @@ +from __future__ import annotations +import logging +import os +from .constants import PAM_DIRECTORY, PAM_USER, VERTICES_SORT_MAP, LOCAL_USER, PAM_CONFIGURATIONS +from .jobs import Jobs +from .infrastructure import Infrastructure +from .record_link import RecordLink +from .user_service import UserService +from .rule import Rules +from .dag_types import (DiscoveryObject, DiscoveryUser, RecordField, RuleActionEnum, UserAcl, + PromptActionEnum, BulkRecordAdd, BulkRecordConvert, BulkProcessResults, + DirectoryInfo, NormalizedRecord) +from .dag_utils import value_to_boolean, split_user_and_domain +from .dag_sort import sort_infra_vertices +from .dag_types import EdgeType +from .dag_crypto import bytes_to_urlsafe_str +import hashlib +from typing import Any, Callable, List, Optional, Union, TYPE_CHECKING + + +if TYPE_CHECKING: + from .dag_vertex import DAGVertex + DirectoryResult = Union[DirectoryInfo, List] + DirectoryUserResult = Union[NormalizedRecord, DAGVertex] + + +class QuitException(Exception): + """ + This exception used when the user wants to stop processing of the results, before the end. + """ + pass + + +class UserNotFoundException(Exception): + """ + We could not find the user. + """ + pass + + +class DirectoryNotFoundException(Exception): + """ + We could not find the directory. + """ + pass + + +class NoDiscoveryDataException(Exception): + """ + This exception is thrown when there is no discovery data. + This is not an error. + There is just nothing to do. + """ + pass + + +class Process: + """ + Process discovery results + While this class update the PAM/record linking graph, it does not save it. + """ + + BULK_LIST_WARNING_THRESHOLD = 10000 + BULK_LIST_MAX_SIZE = 50000 + + def __init__(self, record: Any, job_id: str, logger: Optional[Any] = None, debug_level: int = 0, **kwargs): + self.job_id = job_id + self.record = record + + env_debug_level = os.environ.get("PROCESS_GS_DEBUG_LEVEL") + if env_debug_level is not None: + debug_level = int(env_debug_level) + + self.passed_kwargs = kwargs + + self.jobs = Jobs(record=record, logger=logger, debug_level=debug_level, **kwargs) + self.job = self.jobs.get_job(self.job_id) + + self.infra = Infrastructure(record=record, logger=logger, + debug_level=debug_level, + fail_on_corrupt=False, + **kwargs) + self.record_link = RecordLink(record=record, logger=logger, debug_level=debug_level, **kwargs) + self.user_service = UserService(record=record, logger=logger, debug_level=debug_level, **kwargs) + + self.configuration_uid = self.jobs.dag.uid + + if logger is None: + logger = logging.getLogger() + self.logger = logger + self.debug_level = debug_level + + self.logger.debug(f"discovery process is using configuration uid {self.configuration_uid}") + + def close(self): + """ + Clean up resources held by this Process instance. + Releases all DAG instances and connections to prevent memory leaks. + """ + + if self.jobs: + self.jobs.close() + self.jobs = None + if self.infra: + self.infra.close() + self.infra = None + if self.record_link: + self.record_link.close() + self.record_link = None + if self.user_service: + self.user_service.close() + self.user_service = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + @staticmethod + def get_key_field(record_type: str) -> str: + return VERTICES_SORT_MAP.get(record_type)["key"] + + @staticmethod + def set_user_based_ids(configuration_uid: str, content: DiscoveryObject, parent_vertex: Optional[DAGVertex] = None): + + if configuration_uid is None: + raise ValueError("The configuration UID is None when trying to create an id and UID for user.") + + if content.item.user is None: + raise Exception("The user name is blank. Cannot make an ID for the user.") + + parent_content = DiscoveryObject.get_discovery_object(parent_vertex) + object_id = content.item.user + if "\\" in content.item.user: + object_id = object_id.split("\\")[1] + if parent_content.record_type == PAM_DIRECTORY: + domain = parent_content.name + if not object_id.endswith(domain): + object_id += f"@{domain}" + else: + object_id += parent_content.id + + content.id = object_id + + uid = configuration_uid + content.object_type_value + object_id + m = hashlib.sha256() + m.update(uid.lower().encode()) + + content.uid = bytes_to_urlsafe_str(m.digest()[:16]) + + def populate_admin_content_ids(self, content: DiscoveryObject, parent_vertex: Optional[DAGVertex] = None): + """ + Populate the id and uid attributes for content. + """ + + return self.set_user_based_ids(self.configuration_uid, content, parent_vertex) + + def get_keys_for_vertex(self, vertex: DAGVertex) -> List[str]: + """ + For the vertex + :param vertex: + :return: + """ + + content = DiscoveryObject.get_discovery_object(vertex) + key_field = self.get_key_field(content.record_type) + keys = [] + if key_field == "host_port": + if content.item.port is not None: + if content.item.host is not None: + keys.append(f"{content.item.host}:{content.item.port}".lower()) + if content.item.ip is not None: + keys.append(f"{content.item.ip}:{content.item.port}".lower()) + elif key_field == "host": + if content.item.host is not None: + keys.append(content.item.host.lower()) + if content.item.ip is not None: + keys.append(content.item.ip.lower()) + elif key_field == "user": + if content.parent_record_uid is not None: + if content.item.user is not None: + keys.append(f"{content.parent_record_uid}:{content.item.user}".lower()) + if content.item.dn is not None: + keys.append(f"{content.parent_record_uid}:{content.item.dn}".lower()) + return keys + + def _update_with_record_uid(self, record_cache: dict, current_vertex: DAGVertex): + if not current_vertex.active: + return + + for vertex in current_vertex.has_vertices(): + + if vertex.active is False or vertex.has_data is False: + continue + + content = DiscoveryObject.get_discovery_object(vertex) + + if content.action_rules_result == RuleActionEnum.IGNORE.value or content.ignore_object is True: + continue + elif content.record_uid is not None: + cache_keys = self.get_keys_for_vertex(vertex) + for key in cache_keys: + + if key in record_cache.get(content.record_type): + content.record_uid = record_cache.get(content.record_type).get(key) + vertex.add_data(content) + break + + self._update_with_record_uid( + record_cache=record_cache, + current_vertex=vertex, + ) + + @staticmethod + def _prepare_record(record_prepare_func: Callable, + bulk_add_records: List[BulkRecordAdd], + content: DiscoveryObject, + parent_content: DiscoveryObject, + vertex: DAGVertex, + admin_uid: Optional[str] = None, + context: Optional[Any] = None) -> DiscoveryObject: + """ + Prepare a record to be added. + + :param record_prepare_func: Function to call to prepare a record to be created. + :param bulk_add_records: List of records to be added. + :param content: Discovery content of the current discovery item. + :param parent_content: Discovery content of the parent of the current discovery item. + :param vertex: Infrastructure vertex of the current discovery item. + :params admin_uid: If resource, if there is an admin, this is the UID of that PAM User + :param context: The context; dictionary of random instances. + :return: + """ + + record_to_be_added, record_uid = record_prepare_func( + content=content, + context=context + ) + if record_to_be_added is None: + raise Exception("Did not get prepare record.") + if record_uid is None: + raise Exception("The prepared record did not contain a record UID.") + + parent_record_uid = parent_content.record_uid + if parent_content.object_type_value == "providers": + parent_record_uid = None + bulk_add_records.append( + BulkRecordAdd( + title=content.title, + record=record_to_be_added, + record_type=content.record_type, + record_uid=record_uid, + parent_record_uid=parent_record_uid, + shared_folder_uid=content.shared_folder_uid, + admin_uid=admin_uid + ) + ) + + content.record_uid = record_uid + content.parent_record_uid = parent_content.record_uid + vertex.add_data(content) + + return content + + def _default_acl(self, + discovery_vertex: DAGVertex, + content: DiscoveryObject, + discovery_parent_vertex: DAGVertex) -> UserAcl: + + belongs_to = False + is_admin = False + is_iam_user = False + + parent_content = DiscoveryObject.get_discovery_object(discovery_parent_vertex) + + if content.record_exists is False: + belongs_to = True + + if parent_content.access_user is not None: + + if parent_content.access_user.user == content.item.user: + is_admin = True + + else: + belongs_to_record_vertex = self.record_link.acl_has_belong_to_vertex(discovery_vertex) + + if belongs_to_record_vertex is None: + self.logger.debug(" user vertex does not belong to another resource vertex") + belongs_to = True + + else: + parent_record_vertex = self.record_link.get_record_uid(discovery_parent_vertex) + if parent_record_vertex is not None: + if belongs_to_record_vertex == parent_record_vertex: + self.logger.debug(" user vertex already belongs to the parent resource vertex") + belongs_to = True + else: + self.logger.debug(" user vertex does not belong to any other resource vertex") + + if parent_content.object_type_value == "providers": + is_iam_user = True + + acl = UserAcl.default() + acl.belongs_to = belongs_to + acl.is_admin = is_admin + acl.is_iam_user = is_iam_user + + return acl + + def _directory_exists(self, domain: str, directory_info_func: Callable, context: Any) -> Optional[DirectoryResult]: + + """ + This method will find the directory in the Infrastructure graph or in the Vault. + + If the domain contains more than one DC, the domain will be split and the full DC will be search and then + the first DC. + For example, if EXAMPLE.COM is passed in for the domain, EXAMPLE.COM and EXAMPLE will be searched for. + + The Infrastructure graph will be searched first. + If nothing is found, the Vault will be searched. + + If the directory is found in the graph, a list if directory vertices will be returned. + If the directory is found in the Vault, a DirectoryInfo instance will be returned. + If nothing is found, None is returned. + + The returned results can be passed to the _find_directory_user method. + + """ + + domains = [domain] + if "." in domains: + domains.append(domain.split(".")[0]) + + self.logger.debug(f"search for directories: {', '.join(domains)}") + + provider_vertices = self.infra.dag.search_content({ + "record_type": ["pamAzureConfiguration", "pamDomainConfiguration"], + }, ignore_case=True) + found_provider_directories = [] + for provider_vertex in provider_vertices: + content = DiscoveryObject.get_discovery_object(provider_vertex) + found = False + for domain in domains: + for provider_domain in content.item.info.get("domains", []): + if domain.lower() in provider_domain.lower(): + found = True + break + if found: + break + if found: + found_provider_directories.append(provider_vertex) + if len(found_provider_directories) > 0: + return found_provider_directories + + for domain_name in domains: + directories = self.infra.dag.search_content({ + "record_type": ["pamDirectory", "pamDomainConfiguration"], + "name": domain_name + }, ignore_case=True) + + self.logger.debug(f"found {len(directories)} directories in the graph") + + if len(directories) > 0: + return directories + + for domain_name in domains: + info = directory_info_func(domain=domain_name, skip_users=False, context=context) + if info is not None: + return info + + return None + + def _find_directory_user(self, + results: DirectoryResult, + record_lookup_func: Callable, + context: Any, + find_user: Optional[str] = None, + find_dn: Optional[str] = None) -> Optional[DirectoryUserResult]: + + if isinstance(results, DirectoryInfo): + self.logger.debug("search for directory user from vault records") + self.logger.debug(f"have {len(results.directory_user_record_uids)} users") + for user_record_id in results.directory_user_record_uids: + record = record_lookup_func(record_uid=user_record_id, context=context) # type: NormalizedRecord + if record is not None: + found = None + self.logger.debug(f"find user {find_user}, dn {find_dn}") + if find_user is not None: + found = record.find_user(find_user) + if found is None and find_dn is not None: + found = record.find_dn(find_dn) + return found + return None + + else: + self.logger.debug("search for directory user from the graph") + for directory_vertex in results: # type: DAGVertex + for user_vertex in directory_vertex.has_vertices(): + user_content = DiscoveryObject.get_discovery_object(user_vertex) + + if user_content.record_type != PAM_USER: + self.logger.debug(f"in find directory user, a vertex {user_vertex.uid} was not a pamUser, " + f"was {user_content.record_type}.") + continue + + found_vertex = None + if find_user is not None: + user, domain = split_user_and_domain(find_user) + if user_content.item.user.lower() == user.lower(): + found_vertex = user_vertex + elif user_content.item.user.lower() == find_user.lower(): + found_vertex = user_vertex + elif find_dn is not None: + if user_content.item.dn.lower() == find_dn.lower(): + found_vertex = user_vertex + + if found_vertex is not None: + return found_vertex + return None + + def _record_link_directory_users(self, + directory_vertex: DAGVertex, + directory_content: DiscoveryObject, + directory_info_func: Callable, + context: Optional[Any] = None): + """ + Link user record to directory when adding a new directory. + + When adding a new directory, there may be other directories for the same domain. + We need to link existing directory users, of the same domain, to this new directory. + """ + + self.logger.debug(f"resource is directory; connect users to this directory for {directory_vertex.uid}") + + record_link = context.get("record_link") + + directory_info = directory_info_func( + domain=directory_content.name, + context=context + ) + if directory_info is None: + self.logger.debug("there were no directory record for this domain") + directory_info = DirectoryInfo() + + user_record_uids = directory_info.directory_user_record_uids + + self.logger.debug(f"found {len(directory_info.directory_user_record_uids)} users" + f"from {len(directory_info.directory_record_uids)} directories.") + + self.logger.debug("finding directories in discovery vertices") + for parent_vertex in directory_vertex.belongs_to_vertices(): + self.logger.debug(f"find directories under {parent_vertex.uid}") + for other_directory_vertex in parent_vertex.has_vertices(): + if other_directory_vertex.uid == directory_vertex.uid: + self.logger.debug(" skip this directory, it's the current one") + continue + other_directory_content = DiscoveryObject.get_discovery_object(other_directory_vertex) + self.logger.debug(f"{other_directory_content.record_type}, {other_directory_content.name}, " + f"{other_directory_content.uid}, {other_directory_content.record_uid}") + if (other_directory_content.record_type == PAM_DIRECTORY + and other_directory_content.name == directory_content.name + and other_directory_content.record_uid is not None): + self.logger.debug(f"check {other_directory_content.uid} for users") + for user_vertex in other_directory_vertex.has_vertices(): + user_content = DiscoveryObject.get_discovery_object(user_vertex) + self.logger.debug(f" * {user_vertex.uid}, {user_content.record_uid}") + if user_content.record_uid is not None and user_content.record_uid not in user_record_uids: + user_record_uids.append(user_content.record_uid) + del user_content + del other_directory_content + + self.logger.debug(f"found {len(user_record_uids)} user to connect to directory") + + for record_uid in user_record_uids: + if record_link.get_acl(record_uid, directory_content.record_uid) is None: + record_link.belongs_to(record_uid, directory_content.record_uid, acl=UserAcl.default()) + + found_vertices = directory_vertex.dag.search_content({"record_uid": record_uid}) + if len(found_vertices) == 1: + user_vertex = found_vertices[0] + if user_vertex.get_edge(directory_vertex, EdgeType.KEY) is None: + self.logger.debug(f"adding a KEY edge from the user {user_vertex.uid} to {directory_vertex.uid}") + user_vertex.belongs_to(directory_vertex, EdgeType.KEY) + else: + self.logger.debug("could not find user vertex") + + def _find_admin_directory_user(self, + domain: str, + admin_acl: UserAcl, + directory_info_func: Callable, + record_lookup_func: Callable, + context: Any, + user: Optional[str] = None, + dn: Optional[str] = None) -> Optional[str]: + + results = self._directory_exists(domain=domain, + directory_info_func=directory_info_func, + context=context) + + if results is not None: + directory_user = self._find_directory_user(results=results, + record_lookup_func=record_lookup_func, + context=context, + find_user=user, + find_dn=dn) + if directory_user is not None: + + if isinstance(directory_user, NormalizedRecord): + admin_acl.belongs_to = False + return directory_user.record_uid + else: + admin_content = DiscoveryObject.get_discovery_object(directory_user) + + if admin_content.record_type != PAM_USER: + self.logger.warning( + f"found record type {admin_content.record_type} instead of " + f"pamUser for record UID {admin_content.record_uid}") + return None + + if admin_content.record_uid is not None: + admin_acl.belongs_to = False + return admin_content.record_uid + + return None + else: + raise UserNotFoundException(f"Could not find the directory user in domain {domain}") + else: + raise DirectoryNotFoundException(f"Could not find the directory for domain {domain}") + + def _process_auto_add_level(self, + current_vertex: DAGVertex, + bulk_add_records: List[BulkRecordAdd], + bulk_convert_records: List[BulkRecordConvert], + record_lookup_func: Callable, + record_prepare_func: Callable, + directory_info_func: Callable, + record_cache: dict, + context: Optional[Any] = None): + """ + This method will add items to the bulk_add_records queue to be added by the client. + + These are items where the rule engine has flagged them to be added. + + :param current_vertex: The current/parent discovery vertex. + :param bulk_add_records: List of records to be added. + :param bulk_convert_records: List of existing records to be covert to this gateway. + :params record_lookup_func: A function to lookup records to see if they exist. + :param record_prepare_func: Function to convert content into an unsaved record. + :param directory_info_func: Function to lookup directories. + :param record_cache: + :param context: Client context; could be anything. + :return: + """ + + if not current_vertex.active: + self.logger.debug(f"vertex {current_vertex.uid} is not active, skip") + return + + current_content = DiscoveryObject.get_discovery_object(current_vertex) + if current_content.record_uid is None: + self.logger.debug(f"vertex {current_content.uid} does not have a record id") + return + + self.logger.debug(f"Current Vertex: {current_content.record_type}, {current_vertex.uid}, " + f"{current_content.name}") + + record_type_to_vertices_map = sort_infra_vertices(current_vertex, logger=self.logger) + + for record_type in sorted(record_type_to_vertices_map, key=lambda i: VERTICES_SORT_MAP[i]['order']): + self.logger.debug(f" processing {record_type}") + for vertex in record_type_to_vertices_map[record_type]: + + child_content = DiscoveryObject.get_discovery_object(vertex) + self.logger.debug(f" child vertex {vertex.uid}, {child_content.name}") + + admin_acl = UserAcl.default() + admin_acl.is_admin = True + + default_acl = None + if child_content.record_type == PAM_USER: + default_acl = self._default_acl( + discovery_vertex=vertex, + content=child_content, + discovery_parent_vertex=current_vertex) + + # Check for a vault record, if it exists. + # Default to the DAG content. + # Check the bulk_add_records list, to make sure it is not in the list of record we are about to add. + + existing_record = child_content.record_exists + if record_lookup_func is not None: + check_the_vault = True + for item in bulk_add_records: + if item.record_uid == child_content.record_uid: + self.logger.debug(f" record is in the bulk add list, do not check the vault if exists") + check_the_vault = False + break + if check_the_vault: + existing_record = record_lookup_func(record_uid=child_content.record_uid, + context=context) is not None + self.logger.debug(f" record exists in the vault: {existing_record}") + else: + self.logger.debug(f" record lookup function not defined, record existing: {existing_record}") + + add_record = False + if (child_content.record_exists is False + and child_content.action_rules_result == RuleActionEnum.ADD.value): + self.logger.debug(f" vertex {vertex.uid} had an ADD result for the rule engine, auto add") + add_record = True + + if add_record: + + self.logger.debug(f"adding resource record") + + self.record_link.belongs_to(child_content.record_uid, current_content.record_uid, acl=default_acl) + + admin_uid = None + + if (child_content.admin_uid is not None + and child_content.record_type != PAM_USER + and record_lookup_func is not None): + + self.logger.debug("the admin UID has been set for this resource") + + admin_record = record_lookup_func(record_uid=child_content.admin_uid, + context=context) # type: NormalizedRecord + if admin_record is not None and admin_record.record_type == PAM_USER: + self.logger.debug("was able to find the admin record, connect to resource") + admin_uid = child_content.admin_uid + admin_acl.is_admin = True + self.record_link.belongs_to(child_content.admin_uid, child_content.record_uid, + acl=admin_acl) + else: + self.logger.info(f"The PAM User record {child_content.admin_uid} does not exists. " + "Cannot set the administrator for an auto added " + f"record {child_content.title}.") + + # The record could be a resource or user record. + self._prepare_record( + record_prepare_func=record_prepare_func, + bulk_add_records=bulk_add_records, + content=child_content, + parent_content=current_content, + vertex=vertex, + context=context, + admin_uid=admin_uid + ) + if child_content.record_uid is None: + raise Exception(f"the record uid is blank for {child_content.description} after prepare") + + if child_content.record_type != PAM_USER: + # Process the vertices that belong to the current vertex. + self._process_auto_add_level( + current_vertex=vertex, + bulk_add_records=bulk_add_records, + bulk_convert_records=bulk_convert_records, + record_lookup_func=record_lookup_func, + record_prepare_func=record_prepare_func, + directory_info_func=directory_info_func, + record_cache=record_cache, + context=context + ) + + self.logger.debug(f" finished auto add processing {record_type}") + self.logger.debug(f" Finished auto add current Vertex: {current_vertex.uid}, {current_content.name}") + + @staticmethod + def _apply_admin_uid(bulk_add_records: List[BulkRecordAdd], + resource_uid: str, + admin_uid: str): + + for item in bulk_add_records: + if item.record_uid == resource_uid: + item.admin_uid = admin_uid + break + + def _process_level(self, + current_vertex: DAGVertex, + bulk_add_records: List[BulkRecordAdd], + bulk_convert_records: List[BulkRecordConvert], + record_lookup_func: Callable, + prompt_func: Callable, + prompt_admin_func: Callable, + record_prepare_func: Callable, + directory_info_func: Callable, + record_cache: dict, + item_count: int = 0, + items_left: int = 0, + indent: int = 0, + context: Optional[Any] = None): + """ + This method will walk the user through discovery delta objects. + + :param current_vertex: The current/parent discovery vertex. + :param bulk_add_records: List of records to be added. + :param bulk_convert_records: List of existing records to be covert to this gateway. + :param prompt_func: Function to call for user prompt. + :param record_prepare_func: Function to convert content into an unsaved record. + :param indent: Amount to indent text. + :param context: Client context; could be anything. + :return: + """ + + if not current_vertex.active: + self.logger.debug(f"vertex {current_vertex.uid} is not active, skip") + return + + current_content = DiscoveryObject.get_discovery_object(current_vertex) + if current_content.record_uid is None: + self.logger.debug(f"vertex {current_content.uid} does not have a record id") + return + + self.logger.debug(f"Current Vertex: {current_content.record_type}, {current_vertex.uid}, " + f"{current_content.name}") + + # Return a dictionary where the record type is the key. + record_type_to_vertices_map = sort_infra_vertices(current_vertex, logger=self.logger) + + # Process the record type by their map order in ascending order. + for record_type in sorted(record_type_to_vertices_map, key=lambda i: VERTICES_SORT_MAP[i]['order']): + self.logger.debug(f" processing {record_type}") + for vertex in record_type_to_vertices_map[record_type]: + + child_content = DiscoveryObject.get_discovery_object(vertex) + self.logger.debug(f" child vertex {vertex.uid}, {child_content.name}") + + default_acl = None + if child_content.record_type == PAM_USER: + default_acl = self._default_acl( + discovery_vertex=vertex, + content=child_content, + discovery_parent_vertex=current_vertex) + + # Check for a vault record, if it exists. + # Default to the DAG content. + # Check the bulk_add_records list, to make sure it is not in the list of record we are about to add. + + existing_record = child_content.record_exists + if record_lookup_func is not None: + check_the_vault = True + for item in bulk_add_records: + if item.record_uid == child_content.record_uid: + self.logger.debug(f" record is in the bulk add list, do not check the vault if exists") + check_the_vault = False + break + if check_the_vault: + existing_record = record_lookup_func(record_uid=child_content.record_uid, + context=context) is not None + self.logger.debug(f" record exists in the vault: {existing_record}") + else: + self.logger.debug(f" record lookup function not defined, record existing: {existing_record}") + + if existing_record is True: + self.logger.debug(f" record already exists.") + + elif child_content.action_rules_result == RuleActionEnum.IGNORE.value: + self.logger.debug(f" vertex {vertex.uid} had a IGNORE result for the rule engine, " + "skip processing") + continue + + elif child_content.ignore_object: + self.logger.debug(f" vertex {vertex.uid} was flagged as ignore, skip processing") + continue + + else: + self.logger.debug(f" vertex {vertex.uid} had an PROMPT result, prompt user") + + # For user record, check if the resource record has an admin. + # If not, prompt the user if they want to add this user as the admin. + resource_has_admin = False + if child_content.record_type == PAM_USER: + resource_has_admin = (self.record_link.get_admin_record_uid(current_content.record_uid) + is not None) + self.logger.debug(f"resource has an admin is {resource_has_admin}") + + if hasattr(current_content.item, "allows_admin"): + if not current_content.item.allows_admin: + self.logger.debug(f"resource allows an admin is {current_content.item.allows_admin}") + resource_has_admin = True + else: + self.logger.debug(f"resource type {current_content.record_type} does not have " + "allows_admin attr") + + result = prompt_func( + vertex=vertex, + parent_vertex=current_vertex, + content=child_content, + acl=default_acl, + resource_has_admin=resource_has_admin, + indent=indent, + item_count=item_count, + items_left=items_left, + context=context) + + if result.action == PromptActionEnum.IGNORE: + self.logger.debug(f" vertex {vertex.uid} is being ignored from prompt") + result.content.ignore_object = True + + action_rule_item = Rules.make_action_rule_from_content( + content=result.content, + action=RuleActionEnum.IGNORE + ) + + rules = Rules(record=self.record, **self.passed_kwargs) + rules.add_rule(action_rule_item) + + vertex.add_data(result.content) + + elif result.action == PromptActionEnum.ADD: + self.logger.debug(f" vertex {vertex.uid} is being added from prompt") + + add_content = result.content + acl = result.acl + + if current_content.record_type in PAM_CONFIGURATIONS and add_content.record_type == PAM_USER: + acl.is_iam_user = True + + # The record could be a resource or user record. + self._prepare_record( + record_prepare_func=record_prepare_func, + bulk_add_records=bulk_add_records, + content=add_content, + parent_content=current_content, + vertex=vertex, + context=context + ) + + admin_uid = None + + # The acl will be None if not a pamUser. + self.record_link.discovery_belongs_to(vertex, current_vertex, acl) + + # If the object is NOT a pamUser and the resource allows an admin. + # Prompt the user to create an admin. + should_prompt_for_admin = True + self.logger.debug(f" added record type was {add_content.record_type}") + if (add_content.record_type != PAM_USER and add_content.item.allows_admin is True and + prompt_admin_func is not None): + + self.logger.debug("checking if can add admin") + + if child_content.admin_uid is not None and record_lookup_func is not None: + + self.logger.debug(f"the resource rule set the admin uid to {add_content.admin_uid}") + + admin_record = record_lookup_func(record_uid=add_content.admin_uid, + context=context) + if admin_record is not None and admin_record.record_type == PAM_USER: + self.logger.debug("was able to find the admin record, connect to resource") + + admin_uid = add_content.admin_uid + should_prompt_for_admin = False + else: + self.logger.info(f"The PAM User record {child_content.admin_uid} does not exists. " + "Cannot set the administrator for an auto added " + f"record {child_content.title}.") + + elif add_content.access_user is not None and add_content.access_user.user is not None: + + self.logger.debug(" for this resource, credentials were provided.") + self.logger.error(f" {add_content.access_user.user}, {add_content.access_user.dn}, " + f"{add_content.access_user.password}") + + source = add_content.access_user.source + if add_content.record_type == PAM_DIRECTORY: + source = add_content.name + elif source == LOCAL_USER: + _, domain = split_user_and_domain(add_content.access_user.user) + if domain is not None: + source = domain + + if source != LOCAL_USER: + self.logger.debug(" admin was not a local user, " + f"find user in directory {source}, if exists.") + + acl = UserAcl.default() + acl.is_admin = True + + try: + admin_uid = self._find_admin_directory_user( + domain=source, + admin_acl=acl, + directory_info_func=directory_info_func, + record_lookup_func=record_lookup_func, + context=context, + user=add_content.access_user.user, + dn=add_content.access_user.dn + ) + + if admin_uid is not None: + self.logger.debug(" found directory user admin, connect to resource") + should_prompt_for_admin = False + else: + self.logger.debug(" did not find the directory user for the admin, " + "prompt the user") + except DirectoryNotFoundException: + self.logger.debug(f" directory {source} was not found for admin user") + except UserNotFoundException: + self.logger.debug(f" directory user was not found in directory {source}") + + if should_prompt_for_admin: + self.logger.debug(f" prompt for admin user") + admin_uid = self._process_admin_user( + resource_vertex=vertex, + resource_content=add_content, + bulk_add_records=bulk_add_records, + bulk_convert_records=bulk_convert_records, + record_lookup_func=record_lookup_func, + directory_info_func=directory_info_func, + prompt_admin_func=prompt_admin_func, + record_prepare_func=record_prepare_func, + indent=indent, + context=context + ) + + if admin_uid is not None: + + self._apply_admin_uid( + bulk_add_records=bulk_add_records, + resource_uid=add_content.record_uid, + admin_uid=admin_uid + ) + + items_left -= 1 + + if child_content.record_type != PAM_USER: + # Process the vertices that belong to the current vertex. + self._process_level( + current_vertex=vertex, + bulk_add_records=bulk_add_records, + bulk_convert_records=bulk_convert_records, + record_lookup_func=record_lookup_func, + prompt_func=prompt_func, + prompt_admin_func=prompt_admin_func, + record_prepare_func=record_prepare_func, + directory_info_func=directory_info_func, + record_cache=record_cache, + indent=indent + 1, + item_count=item_count, + items_left=items_left, + context=context + ) + self.logger.debug(f" finished processing {record_type}") + self.logger.debug(f" Finished current Vertex: {current_vertex.uid}, {current_content.name}") + + def _process_admin_user(self, + resource_vertex: DAGVertex, + resource_content: DiscoveryObject, + bulk_add_records: List[BulkRecordAdd], + bulk_convert_records: List[BulkRecordConvert], + record_lookup_func: Callable, + directory_info_func: Callable, + prompt_admin_func: Callable, + record_prepare_func: Callable, + indent: int = 0, + context: Optional[Any] = None) -> Optional[str]: + + if resource_content.access_user is None: + resource_content.access_user = DiscoveryUser() + + values = {} + for field in ["user", "password", "private_key", "dn", "database"]: + value = getattr(resource_content.access_user, field) + if value is None: + value = [] + else: + value = [value] + values[field] = value + + managed = [False] + if resource_content.access_user.managed is not None: + managed = [resource_content.access_user.managed] + + admin_content = DiscoveryObject( + uid="PLACEHOLDER", + object_type_value="users", + parent_record_uid=resource_content.record_uid, + record_type=PAM_USER, + id="PLACEHOLDER", + name="PLACEHOLDER", + description=resource_content.description + ", Administrator", + title=resource_content.title + ", Administrator", + item=DiscoveryUser( + user="PLACEHOLDER" + ), + fields=[ + RecordField(type="login", label="login", value=values["user"], required=True), + RecordField(type="password", label="password", value=values["password"], required=False), + RecordField(type="secret", label="privatePEMKey", value=values["private_key"], required=False), + RecordField(type="text", label="distinguishedName", value=values["dn"], required=False), + RecordField(type="text", label="connectDatabase", value=values["database"], required=False), + RecordField(type="checkbox", label="managed", value=managed, required=False), + ] + ) + + admin_acl = UserAcl.default() + admin_acl.is_admin = True + + # Prompt to add an admin user to this resource. + admin_result = prompt_admin_func( + parent_vertex=resource_vertex, + content=admin_content, + acl=admin_acl, + bulk_convert_records=bulk_convert_records, + indent=indent, + context=context + ) + + if admin_result.action == PromptActionEnum.ADD: + self.logger.debug("adding admin user") + + source = "local" + if resource_content.record_type == PAM_DIRECTORY: + source = resource_content.name + + admin_record_uid = admin_result.record_uid + + if admin_record_uid is None: + admin_content = admin_result.content + + admin_content.item.user = admin_content.get_field_value("login") + admin_content.item.password = admin_content.get_field_value("password") + admin_content.item.private_key = admin_content.get_field_value("privatePEMKey") + admin_content.item.dn = admin_content.get_field_value("distinguishedName") + admin_content.item.database = admin_content.get_field_value("connectDatabase") + admin_content.item.managed = value_to_boolean( + admin_content.get_field_value("managed")) or False + admin_content.item.source = source + admin_content.name = admin_content.item.user + + self.logger.debug(f"added admin user from content") + + if admin_content.item.user is None or admin_content.item.user == "": + raise ValueError("The user name is missing or is blank. Cannot create the administrator user.") + + if admin_content.name is not None: + admin_content.description = (resource_content.description + ", User " + + admin_content.name) + + self.populate_admin_content_ids(admin_content, resource_vertex) + + ad_user, ad_domain = split_user_and_domain(admin_content.item.user) + if ad_domain is not None and admin_content.item.source == LOCAL_USER: + self.logger.debug("The admin is an directory user, but the source is set to a local user") + + found_admin_record_uid = None + try: + found_admin_record_uid = self._find_admin_directory_user( + domain=ad_domain, + admin_acl=admin_acl, + directory_info_func=directory_info_func, + record_lookup_func=record_lookup_func, + context=context, + user=admin_content.item.user, + dn=admin_content.item.dn + ) + except DirectoryNotFoundException: + self.logger.debug(f" directory {source} was not found for admin user") + except UserNotFoundException: + self.logger.debug(f" directory user was not found in directory {source}") + + if found_admin_record_uid is not None: + self.logger.debug(" found directory user admin, connect to resource") + found_admin_vertices = self.infra.dag.search_content({"record_uid": found_admin_record_uid}) + if len(found_admin_vertices) == 1: + found_admin_vertices[0].belongs_to(resource_vertex, edge_type=EdgeType.KEY) + self.record_link.belongs_to(found_admin_record_uid, resource_content.record_uid, + acl=admin_acl) + return found_admin_record_uid + + admin_vertex = self.infra.dag.get_vertex(admin_content.uid) + if admin_vertex is not None and admin_vertex.active is True and admin_vertex.has_data is True: + self.logger.debug("admin exists in the graph") + found_content = DiscoveryObject.get_discovery_object(admin_vertex) + admin_record_uid = found_content.record_uid + else: + self.logger.debug("admin does not exists in the graph") + + if admin_record_uid is not None: + self.logger.debug("the admin has a record UID") + + if self.record_link.get_parent_record_uid(admin_record_uid) is None: + self.logger.debug("the admin does not belong to another resources, " + "setting it belong to this resource") + admin_acl.belongs_to = True + + admin_vertex.belongs_to(resource_vertex, edge_type=EdgeType.KEY) + self.record_link.belongs_to(admin_record_uid, resource_content.record_uid, acl=admin_acl) + else: + if admin_vertex is None: + self.logger.debug("creating an entry in the graph for the admin") + admin_vertex = self.infra.dag.add_vertex(uid=admin_content.uid, + name=admin_content.description) + + admin_acl.belongs_to = True + + # Connect the user vertex to the resource vertex. + admin_vertex.belongs_to(resource_vertex, edge_type=EdgeType.KEY) + admin_vertex.add_data(admin_content) + + # The record will be a user record; admin_acl will not be None + self._prepare_record( + record_prepare_func=record_prepare_func, + bulk_add_records=bulk_add_records, + content=admin_content, + parent_content=resource_content, + vertex=admin_vertex, + context=context + ) + + self.record_link.discovery_belongs_to(admin_vertex, resource_vertex, acl=admin_acl) + + admin_record_uid = admin_content.record_uid + else: + self.logger.debug("add admin user from existing record") + + if admin_result.is_directory_user is False: + + self.logger.debug("the admin user is NOT a directory user, convert record's rotation settings") + + parent_record_uid = resource_content.record_uid + if resource_content.object_type_value == "providers": + parent_record_uid = None + + bulk_convert_records.append( + BulkRecordConvert( + record_uid=admin_record_uid, + parent_record_uid=parent_record_uid + ) + ) + + record_vertex = self.record_link.acl_has_belong_to_record_uid(admin_record_uid) + if record_vertex is None: + admin_acl.belongs_to = True + + else: + self.logger.debug("the admin user is a directory user") + + return admin_record_uid + + return None + + def _get_count(self, current_vertex: DAGVertex) -> int: + """ + Get the number of vertices that have not been converted to record. + + This will recurse down the graph. + To be counted, the current vertex being evaluated, must ... + * not have record UID. + * not be ignored either by flag or rule. + * not be auto added. + + To recurse down, the current vertex being evaluated, must ... + * have a record UID + * not be ignored either by flag or rule. + """ + count = 0 + + for vertex in current_vertex.has_vertices(): + if not vertex.active: + continue + content = DiscoveryObject.get_discovery_object(vertex) + + if (content.record_uid is None + and content.ignore_object is False + and content.action_rules_result != "add" + and content.action_rules_result != "ignore"): + count += 1 + + if ( + content.record_uid is not None + and content.ignore_object is False + and content.action_rules_result != "ignore"): + count += self._get_count(vertex) + + return count + + @property + def no_items_left(self): + return self._get_count(self.infra.get_root) == 0 + + def run(self, + prompt_func: Callable, + record_prepare_func: Callable, + smart_add: bool = False, + record_lookup_func: Optional[Callable] = None, + record_create_func: Optional[Callable] = None, + record_convert_func: Optional[Callable] = None, + prompt_confirm_add_func: Optional[Callable] = None, + prompt_admin_func: Optional[Callable] = None, + auto_add_result_func: Optional[Callable] = None, + directory_info_func: Optional[Callable] = None, + context: Optional[Any] = None, + record_cache: Optional[dict] = None, + force_quit: bool = False + ) -> BulkProcessResults: + """ + Process the discovery results. + + :param record_cache: A dictionary of record types to keys to record UID. + :param prompt_func: Function to call when the user needs to make a decision about an object. + :param smart_add: If we have resource cred, add the resource and the users. DEPRECATED + :param record_lookup_func: Function to look up a record by UID. + :param record_prepare_func: Function to call to prepare a record to be created. + :param record_create_func: Function to call to save the prepared records. + :param record_convert_func: Function to convert record to use this gateway. + :param prompt_confirm_add_func: Function to call if quiting and record have been added to queue. + :param prompt_admin_func: Function to prompt user for admin. + :param auto_add_result_func: Function to call after auto adding. Provided records to bulk add. + :param directory_info_func: Function to get users of a directory from vault records. + :param context: Context passed to the prompt and add function. These could be objects that are not in the scope + of the function. + :param force_quit: Used for testing. Throw a Quit exception after processing. + :return: + """ + sync_point = self.job.sync_point + if sync_point is None: + raise Exception("The job does not have a sync point for the graph.") + + self.logger.debug(f"loading the graph at sync point {sync_point}") + self.infra.load(sync_point=sync_point) + if not self.infra.has_discovery_data: + raise NoDiscoveryDataException("There is no discovery data to process.") + + if self.infra.dag.is_corrupt is True: + self.logger.debug("the graph is corrupt, deleting vertex") + for uid in self.infra.dag.corrupt_uids: + vertex = self.infra.dag.get_vertex(uid) + vertex.delete() + self.infra.dag.corrupt_uids = [] + self.logger.info("fixed the corrupted vertices") + + root = self.infra.get_root + configuration = root.has_vertices()[0] + + if record_cache is not None: + self._update_with_record_uid( + record_cache=record_cache, + current_vertex=configuration, + ) + + bulk_add_records = [] # type: List[BulkRecordAdd] + bulk_convert_records = [] # type: List[BulkRecordConvert] + + should_add_records = True + bulk_process_results = None + + if context is None: + context = {} + + context["record_link"] = self.record_link + context["infra"] = self.infra + + try: + + self.logger.debug("# ####################################################################################") + self.logger.debug("# AUTO ADD ITEMS") + self.logger.debug("#") + self.logger.debug(f"smart add = {smart_add}") + + self._process_auto_add_level( + current_vertex=configuration, + bulk_add_records=bulk_add_records, + bulk_convert_records=bulk_convert_records, + record_lookup_func=record_lookup_func, + record_prepare_func=record_prepare_func, + directory_info_func=directory_info_func, + record_cache=record_cache, + context=context) + + if auto_add_result_func is not None: + auto_add_result_func(bulk_add_records=bulk_add_records) + + self.logger.debug("# ####################################################################################") + self.logger.debug("# PROMPT USER ITEMS") + self.logger.debug("#") + + item_count = self._get_count(configuration) + + self._process_level( + current_vertex=configuration, + bulk_add_records=bulk_add_records, + bulk_convert_records=bulk_convert_records, + record_lookup_func=record_lookup_func, + prompt_func=prompt_func, + prompt_admin_func=prompt_admin_func, + record_prepare_func=record_prepare_func, + directory_info_func=directory_info_func, + record_cache=record_cache, + indent=0, + item_count=item_count, + items_left=item_count, + context=context) + + if force_quit: + raise QuitException() + + except QuitException: + should_add_records = False + if (len(bulk_add_records) > 0 and prompt_confirm_add_func is not None and + prompt_confirm_add_func(bulk_add_records) is True): + should_add_records = True + + modified_count = len(self.infra.dag.modified_edges) + self.logger.debug(f"quiting and there are {modified_count} modified edges.") + + if record_create_func is None: + should_add_records = False + + if should_add_records: + + self.logger.debug("# ####################################################################################") + self.logger.debug("# CREATE NEW RECORD") + self.logger.debug("#") + + bulk_process_results = record_create_func( + bulk_add_records=bulk_add_records, + context=context + ) + self.logger.debug("# ####################################################################################") + + self.logger.debug("# ####################################################################################") + self.logger.debug("# CONVERT EXISTING RECORD") + self.logger.debug("#") + + record_convert_func( + bulk_convert_records=bulk_convert_records, + context=context + ) + self.logger.debug("# ####################################################################################") + else: + + self.logger.debug("# ####################################################################################") + self.logger.debug("# ROLLBACK GRAPH") + self.logger.debug("#") + + for record in bulk_add_records: + vertices = self.infra.dag.search_content({"record_uid": record.record_uid}) + for vertex in vertices: + self.logger.debug(f" * {record.title}, flagged") + vertex.skip_save = True + for record in bulk_convert_records: + vertices = self.infra.dag.search_content({"record_uid": record.record_uid}) + for vertex in vertices: + self.logger.debug(f" * {record.title}, flagged") + vertex.skip_save = True + + self.logger.debug("# ####################################################################################") + + self.logger.debug("# ####################################################################################") + self.logger.debug("# Save INFRASTRUCTURE graph") + self.logger.debug("#") + + self.logger.debug(f"saving additions from process run") + self.infra.save(delta_graph=False) + self.logger.debug("# ####################################################################################") + + self.user_service.run(infra=self.infra) + + return bulk_process_results diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/record_link.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/record_link.py new file mode 100644 index 00000000..9cbb7f43 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/record_link.py @@ -0,0 +1,463 @@ +from __future__ import annotations +import logging +from ..keeper_dag.dag_utils import get_connection, make_agent +from ..keeper_dag.dag import DAG, EdgeType +from ..keeper_dag.dag_types import PamGraphId, PamEndpoints, UserAcl, DiscoveryObject +import importlib +from typing import Any, Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from ..keeper_dag.dag import DAGVertex + + +class RecordLink: + + def __init__(self, + record: Any, + logger: Optional[Any] = None, + debug_level: int = 0, + fail_on_corrupt: bool = True, + log_prefix: str = "GS Record Linking", + save_batch_count: int = 200, + agent: Optional[str] = None, + use_read_protobuf: bool = False, + use_write_protobuf: bool = True, + **kwargs): + + self.conn = get_connection(logger=logger, + use_read_protobuf=use_read_protobuf, + use_write_protobuf=use_write_protobuf, + **kwargs) + + self.record = record + self._dag = None + if logger is None: + logger = logging.getLogger() + self.logger = logger + self.log_prefix = log_prefix + self.debug_level = debug_level + self.save_batch_count = save_batch_count + + self.write_endpoint = None + if self.conn.use_write_protobuf: + self.write_endpoint = PamEndpoints.PAM + + self.read_endpoint = None + if self.conn.use_read_protobuf: + self.read_endpoint = PamEndpoints.PAM + + self.agent = make_agent("record_linking") + if agent is not None: + self.agent += "; " + agent + + self.fail_on_corrupt = fail_on_corrupt + + @property + def dag(self) -> DAG: + if self._dag is None: + + self._dag = DAG(conn=self.conn, + record=self.record, + write_endpoint=self.write_endpoint, + read_endpoint=self.read_endpoint, + graph_id=PamGraphId.PAM, + auto_save=False, + logger=self.logger, + debug_level=self.debug_level, + name="Record Linking", + fail_on_corrupt=self.fail_on_corrupt, + log_prefix=self.log_prefix, + save_batch_count=self.save_batch_count, + agent=self.agent) + sync_point = self._dag.load(sync_point=0) + self.logger.debug(f"the record linking sync point is {sync_point or 0}") + if not self.dag.has_graph: + self.dag.add_vertex(name=self.record.title, uid=self._dag.uid) + + return self._dag + + def close(self): + """ + Clean up resources held by this RecordLink instance. + Releases the DAG instance and connection to prevent memory leaks. + """ + if self._dag is not None: + self._dag = None + self.conn = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + @property + def has_graph(self) -> bool: + return self.dag.has_graph + + def reload(self): + self._dag.load(sync_point=0) + + def get_record_link(self, uid: str) -> DAGVertex: + return self.dag.get_vertex(uid) + + def get_parent_uid(self, uid: str) -> Optional[str]: + """ + Get the vertex that the UID belongs to. + + This method will check the vertex ACL to see which edge has a True value for belongs_to. + If it is found, the record UID that the head points at will be returned. + If not found, None is returned. + """ + + vertex = self.dag.get_vertex(uid) + if vertex is not None: + for edge in vertex.edges: + if edge.edge_type == EdgeType.ACL: + content = edge.content_as_object(UserAcl) + if content.belongs_to is True: + return edge.head_uid + return None + + @staticmethod + def get_record_uid(discovery_vertex: DAGVertex, validate_record_type: Optional[str] = None) -> str: + """ + Get the record UID from the vertex + """ + data = discovery_vertex.get_data() + if data is None: + raise Exception(f"The discovery vertex {discovery_vertex.uid} does not have a DATA edge. " + "Cannot get record UID.") + content = DiscoveryObject.get_discovery_object(discovery_vertex) + + if validate_record_type is not None: + if validate_record_type != content.record_type: + raise Exception(f"The vertex is not record type {validate_record_type}") + + if content.record_uid is not None: + return content.record_uid + raise Exception(f"The discovery vertex {discovery_vertex.uid} data does not have a populated record UID.") + + def add_configuration(self, discovery_vertex: DAGVertex): + """ + Add the configuration vertex to the DAG root. + The configuration record UID will be the same as root UID. + """ + + record_uid = self.get_record_uid(discovery_vertex) + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + record_vertex = self.dag.add_vertex(uid=record_uid, name=discovery_vertex.name) + if not self.dag.get_root.has(record_vertex): + record_vertex.belongs_to_root(EdgeType.LINK) + + def discovery_belongs_to(self, discovery_vertex: DAGVertex, discovery_parent_vertex: DAGVertex, + acl: Optional[UserAcl] = None): + """ + Link vault record using the vertices from discovery. + If a link already exists, no additional link will be created. + """ + + try: + record_uid = self.get_record_uid(discovery_vertex) + except Exception as err: + self.logger.warning(f"The discovery vertex is missing a record uid, cannot connect record: {err}") + return + + if discovery_parent_vertex.uid == self.dag.uid: + parent_record_uid = discovery_parent_vertex.uid + else: + try: + parent_record_uid = self.get_record_uid(discovery_parent_vertex) + except Exception as err: + self.logger.warning("The discovery parent vertex is missing a record uid, cannot connect record: " + f"{err}") + return + + self.belongs_to( + record_uid=record_uid, + parent_record_uid=parent_record_uid, + acl=acl, + record_name=discovery_vertex.name, + parent_record_name=discovery_parent_vertex.name + ) + + def belongs_to(self, record_uid: str, parent_record_uid: str, acl: Optional[UserAcl] = None, + record_name: Optional[str] = None, parent_record_name: Optional[str] = None): + """ + Link vault records using record UIDs. + If a link already exists, no additional link will be created. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + self.logger.debug(f"adding record linking vertex for record UID {record_uid} ({record_name})") + record_vertex = self.dag.add_vertex(uid=record_uid, name=record_name) + + parent_record_vertex = self.dag.get_vertex(parent_record_uid) + if parent_record_vertex is None: + self.logger.debug(f"adding record linking vertex for parent record UID {parent_record_uid}") + parent_record_vertex = self.dag.add_vertex(uid=parent_record_uid, name=parent_record_name) + + self.logger.debug(f"record UID {record_vertex.uid} belongs to {parent_record_vertex.uid} " + f"({parent_record_name})") + + edge_type = EdgeType.LINK + if acl is not None: + edge_type = EdgeType.ACL + + existing_edge = record_vertex.get_edge(parent_record_vertex, edge_type=edge_type) + add_edge = True + if existing_edge is not None and existing_edge.active is True: + if edge_type == EdgeType.ACL: + content = existing_edge.content_as_object(UserAcl) + if content.model_dump_json() == acl.model_dump_json(): + add_edge = False + else: + add_edge = False + + if add_edge: + self.logger.debug(f" added {edge_type} edge") + record_vertex.belongs_to(parent_record_vertex, edge_type=edge_type, content=acl) + + def get_acl(self, record_uid: str, parent_record_uid: str, record_name: Optional[str] = None, + parent_record_name: Optional[str] = None) -> Optional[UserAcl]: + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + self.logger.debug(f"adding record linking vertex for record UID {record_uid} ({record_name})") + record_vertex = self.dag.add_vertex(uid=record_uid, name=record_name) + + parent_record_vertex = self.dag.get_vertex(parent_record_uid) + if parent_record_vertex is None: + self.logger.debug(f"adding record linking vertex for parent record UID {parent_record_uid}") + parent_record_vertex = self.dag.add_vertex(uid=parent_record_uid, name=parent_record_name) + + acl_edge = record_vertex.get_edge(parent_record_vertex, edge_type=EdgeType.ACL) + if acl_edge is None: + return None + + return acl_edge.content_as_object(UserAcl) + + def acl_has_belong_to_vertex(self, discovery_vertex: DAGVertex) -> Optional[DAGVertex]: + """ + Get the resource vertex for this user vertex that handles rotation, using the user's infrastructure vertex. + """ + + record_uid = self.get_record_uid(discovery_vertex, "pamUser") + if record_uid is None: + return None + + return self.acl_has_belong_to_record_uid(record_uid) + + def acl_has_belong_to_record_uid(self, record_uid: str) -> Optional[DAGVertex]: + """ + Get the resource vertex for this user vertex that handles rotation. using the user's record UID. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + return None + for edge in record_vertex.edges: + if edge.edge_type != EdgeType.ACL: + continue + content = edge.content_as_object(UserAcl) + if content.belongs_to is True: + return self.dag.get_vertex(edge.head_uid) + return None + + def get_parent_record_uid(self, record_uid: str) -> Optional[str]: + """ + Get the parent record uid. + Check the ACL edges for the one where belongs_to is True + If there is a LINK edge that leads to the parent. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + return None + for edge in record_vertex.edges: + if edge.edge_type == EdgeType.ACL: + content = edge.content_as_object(UserAcl) # type: UserAcl + if content.belongs_to: + return edge.head_uid + elif edge.edge_type == EdgeType.LINK: + return edge.head_uid + return None + + def get_child_record_uids(self, record_uid: str) -> List[str]: + """ + Get a list of child record for this parent. + The list contains any parent that this record uid has a LINK or ACL edge to. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + self.logger.debug(f"could not get the parent record for {record_uid}") + return [] + + record_uids = [] + self.logger.debug(f"has {record_vertex.has_vertices()}") + for child_vertex in record_vertex.has_vertices(EdgeType.ACL): + record_uids.append(child_vertex.uid) + for child_vertex in record_vertex.has_vertices(EdgeType.LINK): + record_uids.append(child_vertex.uid) + + return record_uids + + def get_parent_record_uids(self, record_uid: str) -> List[str]: + """ + Get a list of parent record this child record belongs to. + The list contains any parent that this record uid has a LINK or ACL edge to. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is None: + self.logger.debug(f"could not get the child record for {record_uid}") + return [] + + record_uids = [] + for vertex in record_vertex.belongs_to_vertices(): + edge = vertex.get_edge(record_vertex, EdgeType.ACL) + if edge is None: + edge = vertex.get_edge(record_vertex, EdgeType.LINK) + if edge is not None: + record_uids.append(record_vertex.uid) + return record_uids + + def get_admin_record_uid(self, record_uid: str) -> Optional[str]: + """ + Get the record that admins this resource record. + """ + + record_vertex = self.dag.get_vertex(record_uid) + if record_vertex is not None: + for vertex in record_vertex.has_vertices(): + for edge in vertex.edges: + if edge.head_uid != record_vertex.uid: + continue + if edge.edge_type == EdgeType.ACL: + content = edge.content_as_object(UserAcl) # type: UserAcl + if content.is_admin is True: + return vertex.uid + return None + + def discovery_disconnect_from(self, discovery_vertex: DAGVertex, discovery_parent_vertex: DAGVertex): + record_uid = self.get_record_uid(discovery_vertex) + parent_record_uid = self.get_record_uid(discovery_parent_vertex) + self.disconnect_from(record_uid=record_uid, parent_record_uid=parent_record_uid) + + def disconnect_from(self, record_uid: str, parent_record_uid: str): + record_vertex = self.dag.get_vertex(record_uid) + parent_record_vertex = self.dag.get_vertex(parent_record_uid) + + if record_vertex is None: + self.logger.info(f"for record linking, could not find the vertex for record UID {record_uid}." + f" cannot disconnect from parent vertex for record UID {parent_record_uid}") + return + if parent_record_vertex is None: + self.logger.info(f"for record linking, could not find the parent vertex for record UID {parent_record_uid}." + f" cannot disconnect the child vertex for record UID {record_uid}") + return + + parent_record_vertex.disconnect_from(record_vertex) + + @staticmethod + def delete(vertex: DAGVertex): + if vertex is not None: + vertex.delete() + + def save(self): + + self.logger.debug("DISCOVERY COMMON RECORD LINKING GRAPH SAVE CALLED") + if self.dag.has_graph: + self.logger.debug("saving the record linking.") + self.dag.save(delta_graph=False) + else: + self.logger.debug("the record linking graph does not contain any data, was not saved.") + + def to_dot(self, graph_format: str = "svg", show_version: bool = True, show_only_active_vertices: bool = True, + show_only_active_edges: bool = True, graph_type: str = "dot"): + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"DAG for Record Linking", format=graph_format) + + if graph_type == "dot": + dot.attr(rankdir='RL') + elif graph_type == "twopi": + dot.attr(layout="twopi") + dot.attr(ranksep="10") + dot.attr(ratio="auto") + else: + dot.attr(layout=graph_type) + + self.logger.debug(f"have {len(self.dag.all_vertices)} vertices") + for v in self.dag.all_vertices: + if show_only_active_vertices is True and v.active is False: + continue + + tooltip = "" + + for edge in v.edges: + + color = "grey" + style = "solid" + + if edge.active is True: + color = "black" + style = "bold" + elif show_only_active_edges is True: + continue + + if edge.edge_type == EdgeType.DATA and v.active is False: + color = "grey" + + if edge.edge_type == EdgeType.DELETION: + style = "dotted" + + edge_tip = "" + if edge.edge_type == EdgeType.ACL and v.active is True: + content = edge.content_as_dict + if content.get("is_admin") is True: + color = "red" + if content.get("belongs_to") is True: + if color == "red": + color = "purple" + else: + color = "blue" + + tooltip += f"TO {edge.head_uid}\\n" + for k, val in content.items(): + tooltip += f" * {k}={val}\\n" + tooltip += f"--------------------\\n\\n" + + label = DAG.EDGE_LABEL.get(edge.edge_type) + if label is None: + label = "UNK" + if edge.path is not None and edge.path != "": + label += f"\\npath={edge.path}" + if show_version is True: + label += f"\\nv={edge.version}" + + dot.edge(v.uid, edge.head_uid, label, style=style, fontcolor=color, color=color, tooltip=edge_tip) + + shape = "ellipse" + fillcolor = "white" + color = "black" + if v.active is False: + fillcolor = "grey" + + label = f"uid={v.uid}" + dot.node(v.uid, label, color=color, fillcolor=fillcolor, style="filled", shape=shape, tooltip=tooltip) + + return dot diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/rule.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/rule.py new file mode 100644 index 00000000..c84a0b54 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/rule.py @@ -0,0 +1,368 @@ +from __future__ import annotations +from .dag_types import (RuleTypeEnum, RuleItem, ActionRuleSet, ActionRuleItem, ScheduleRuleSet, ComplexityRuleSet, + Statement, RuleActionEnum) +from .dag_utils import value_to_boolean, get_connection, make_agent +from .dag import DAG, EdgeType +from .exceptions import DAGException +from .dag_types import PamGraphId +from time import time +import base64 +import os +from typing import Any, List, Optional, Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from .dag_types import DiscoveryObject + + +class Rules: + + DATA_PATH = "rules" + RULE_ITEM_TYPE_MAP = { + "ActionRuleItem": RuleTypeEnum.ACTION, + "ScheduleRuleItem": RuleTypeEnum.SCHEDULE, + "ComplexityRuleItem": RuleTypeEnum.COMPLEXITY + } + RULE_TYPE_TO_SET_MAP = { + RuleTypeEnum.ACTION: ActionRuleSet, + RuleTypeEnum.SCHEDULE: ScheduleRuleSet, + RuleTypeEnum.COMPLEXITY: ComplexityRuleSet + } + + RULE_FIELDS = { + # Attributes the records + "recordType": {"type": str}, + "parentRecordType": {"type": str}, + "recordTitle": {"type": str}, + "recordNotes": {"type": str}, + "recordDesc": {"type": str}, + "parentUid": {"type": str}, + + # Record fields + "login": {"type": str}, + "password": {"type": str}, + "privatePEMKey": {"type": str}, + "distinguishedName": {"type": str}, + "connectDatabase": {"type": str}, + "managed": {"type": bool, "default": False}, + "hostName": {"type": str}, + "port": {"type": float, "default": 0}, + "operatingSystem": {"type": str}, + "instanceName": {"type": str}, + "instanceId": {"type": str}, + "providerGroup": {"type": str}, + "providerRegion": {"type": str}, + "databaseId": {"type": str}, + "databaseType": {"type": str}, + "useSSL": {"type": bool, "default": False}, + "domainName": {"type": str}, + "directoryId": {"type": str}, + "directoryType": {"type": str}, + "ip": {"type": str}, + } + + BREAK_OUT = { + "pamHostname": { + "hostName": "hostName", + "port": "port" + } + } + + # If creating an ignore role, these fields are used in the rule. + RECORD_FIELD = { + "pamMachine": ["pamHostname"], + "pamDatabase": ["pamHostname", "databaseType"], + "pamDirectory": ["pamHostname", "directoryType"], + "pamUser": ["parentUid", "login", "distinguishedName"], + } + + OBJ_ATTR = { + "parentUid": "parent_record_uid" + } + + def __init__(self, record: Any, logger: Optional[Any] = None, debug_level: int = 0, fail_on_corrupt: bool = True, + agent: Optional[str] = None, **kwargs): + + self.conn = get_connection(**kwargs) + self.record = record + self._dag = None + self.logger = logger + self.debug_level = debug_level + self.fail_on_corrupt = fail_on_corrupt + + self.agent = make_agent("rules") + if agent is not None: + self.agent += "; " + agent + + @property + def dag(self) -> DAG: + if self._dag is None: + + # Turn auto_save on after the DAG has been created. + self._dag = DAG(conn=self.conn, + record=self.record, + graph_id=PamGraphId.DISCOVERY_RULES, + auto_save=False, + logger=self.logger, + debug_level=self.debug_level, + fail_on_corrupt=self.fail_on_corrupt, + agent=self.agent) + self._dag.load() + + if not self._dag.has_graph: + for rule_type_enum in Rules.RULE_TYPE_TO_SET_MAP: + rules = self._dag.add_vertex() + rules.belongs_to_root( + EdgeType.KEY, + path=rule_type_enum.value + ) + content = Rules.RULE_TYPE_TO_SET_MAP[rule_type_enum]() + rules.add_data( + content=content, + ) + self._dag.save() + + self._dag.auto_save = True + return self._dag + + def close(self): + """ + Clean up resources held by this Rules instance. + Releases the DAG instance and connection to prevent memory leaks. + """ + try: + if hasattr(self, "_dag"): + self.conn = None + del self._dag + if hasattr(self, "conn"): + self.conn = None + del self.conn + if hasattr(self, "record"): + self.conn = None + del self.conn + except (Exception,): + pass + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + @staticmethod + def data_path(rule_type: RuleTypeEnum): + return f"/{rule_type.value}" + + def get_ruleset(self, rule_type: RuleTypeEnum): + path = self.data_path(rule_type) + rule_json = self.dag.walk_down_path(path).content_as_str + if rule_json is None: + raise DAGException("Could not get the status data from the DAG.") + rule_set_class = Rules.RULE_TYPE_TO_SET_MAP[rule_type] + return rule_set_class.model_validate_json(rule_json) + + def set_ruleset(self, rule_type: RuleTypeEnum, rules: List[Rules]): + path = self.data_path(rule_type) + self.dag.walk_down_path(path).add_data( + content=rules, + ) + + def _rule_transaction(self, func: Callable, rule: Optional[RuleItem] = None): + rule_type = rule.__class__.__name__ + rule_type_enum = Rules.RULE_ITEM_TYPE_MAP.get(rule_type) + if rule_type_enum is None: + raise ValueError("rule is not a known rule instance") + + ruleset = self.get_ruleset(rule_type_enum) + + rules = func( + r=rule, + rs=ruleset.rules + ) + + ruleset.rules = list(sorted(rules, key=lambda x: x.priority)) + self.set_ruleset(rule_type_enum, ruleset) + + def add_rule(self, rule: RuleItem) -> RuleItem: + + if rule.rule_id is None: + rule.rule_id = "RULE" + base64.urlsafe_b64encode(os.urandom(8)).decode().rstrip('=') + if rule.added_ts is None: + rule.added_ts = int(time()) + + def _add_rule(r: RuleItem, rs: List[RuleItem]): + rs.append(r) + return rs + + self._rule_transaction( + rule=rule, + func=_add_rule + ) + + return rule + + def update_rule(self, rule: RuleItem) -> RuleItem: + + def _update_rule(r: RuleItem, rs: List[RuleItem]): + new_rule_list = [] + for _r in rs: + if _r.rule_id == r.rule_id: + new_rule_list.append(r) + else: + new_rule_list.append(_r) + return new_rule_list + + self._rule_transaction( + rule=rule, + func=_update_rule + ) + + return rule + + def remove_rule(self, rule: RuleItem): + + def _remove_rule(r: RuleItem, rs: List[RuleItem]): + new_rule_list = [] + for _r in rs: + if _r.rule_id != r.rule_id: + new_rule_list.append(_r) + return new_rule_list + + self._rule_transaction( + rule=rule, + func=_remove_rule + ) + + def remove_all(self, rule_type: RuleTypeEnum): + + def _remove_all_rules(r: Any, rs: List[RuleItem]): + return [] + + fake_rule = None + if rule_type == RuleTypeEnum.ACTION: + fake_rule = ActionRuleItem(statement=[]) + else: + raise ValueError("rule type not supported with remove_all") + + self._rule_transaction( + rule=fake_rule, + func=_remove_all_rules + ) + + def rule_list(self, rule_type: RuleTypeEnum, search: Optional[str] = None) -> List[RuleItem]: + rule_list = [] + for rule_item in self.get_ruleset(rule_type).rules: + if search is not None and rule_item.search(search) is False: + continue + rule_list.append(rule_item) + + return rule_list + + def get_rule_item(self, rule_type: RuleTypeEnum, rule_id: str) -> Optional[RuleItem]: + for rule_item in self.rule_list(rule_type=rule_type): + if rule_item.rule_id == rule_id: + return rule_item + return None + + @staticmethod + def make_action_rule_from_content(content: DiscoveryObject, action: RuleActionEnum, priority: Optional[int] = None, + case_sensitive: bool = True, + shared_folder_uid: Optional[str] = None) -> ActionRuleItem: + + if action == RuleActionEnum.IGNORE: + priority = -1 + + record_fields = Rules.RECORD_FIELD.get(content.record_type) + if record_fields is None: + raise ValueError(f"Record type {content.record_type} does not have fields maps.") + + statements = [ + Statement(field="recordType", operator="==", value=content.record_type) + ] + + for field_label in record_fields: + if field_label in Rules.OBJ_ATTR: + attr = Rules.OBJ_ATTR[field_label] + if not hasattr(content, attr): + raise Exception(f"Discovery object is missing attribute {attr}") + value = getattr(content, attr) + statements.append( + Statement(field=field_label, operator="==", value=value) + ) + else: + for field in content.fields: + label = field.label + if field_label != label: + continue + + value = field.value + if value is None or len(value) == 0: + continue + value = value[0] + + if label in Rules.BREAK_OUT: + for key in Rules.BREAK_OUT[label]: + key_value = value.get(key) + if key_value is None: + continue + statements.append( + Statement(field=key, operator="==", value=key_value) + ) + else: + statements.append( + Statement(field=label, operator="==", value=value) + ) + + return ActionRuleItem( + enabled=True, + priority=priority, + case_sensitive=case_sensitive, + statement=statements, + action=action, + shared_folder_uid=shared_folder_uid + ) + + @staticmethod + def make_action_rule_statement_str(statement: List[Statement]) -> str: + statement_str = "" + for item in statement: + if statement_str != "": + statement_str += " and " + statement_str += item.field + " " + item.operator + " " + field_type = Rules.RULE_FIELDS.get(item.field).get("type") + if field_type is None: + raise ValueError("Unknown field in rule") + + values = item.value + new_values = [] + if item.operator != "in": + values = [values] + + for value in values: + if field_type is str: + new_value = f"'{value}'" + elif field_type is bool: + if value_to_boolean(value) is True: + new_value = "true" + else: + new_value = "false" + elif field_type is float: + if int(value) == value: + new_value = str(int(value)) + else: + new_value = str(value) + else: + raise ValueError("Cannot determine the field type for rule statement.") + + new_values.append(new_value) + + if item.operator == "in": + statement_str += "[" + ", ".join(new_values) + "]" + else: + statement_str += new_values[0] + return statement_str diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/__init__.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/__init__.py new file mode 100644 index 00000000..d2798793 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/__init__.py @@ -0,0 +1,56 @@ +from __future__ import annotations +import logging +from ..dag_types import SyncQuery, Ref, RefType, DAGData, DataPayload, EdgeType +from ....proto import GraphSync_pb2 as gs_pb2 +from pydantic import BaseModel +from typing import Optional, Union, List, TYPE_CHECKING + +if TYPE_CHECKING: + Logger = Union[logging.RootLogger, logging.Logger] + + +class SyncResult(BaseModel): + sync_point: int = 0 + data: List[DAGData] = [] + has_more: bool = False + + +class DataStructBase: + + def __init__(self, + logger: Optional[Logger] = None): + + if logger is None: + logger = logging.getLogger() + self.logger = logger + + def sync_query(self, + stream_id: str, + sync_point: int = 0, + graph_id: Optional[int] = None) -> Union[SyncQuery, gs_pb2.GraphSyncQuery]: + pass + + @staticmethod + def origin_ref(origin_uid: str, + name: str) -> Union[Ref, gs_pb2.GraphSyncRef]: + pass + + def data(self, + data_type: EdgeType, + tail_uid: str, + content: Optional[bytes] = None, + head_uid: Optional[str] = None, + tail_name: Optional[str] = None, + head_name: Optional[str] = None, + tail_ref_type: Optional[RefType] = None, + head_ref_type: Optional[RefType] = None, + path: Optional[str] = None) -> Union[DAGData,gs_pb2.GraphSyncData]: + + pass + + @staticmethod + def payload(origin_ref: Union[Ref, gs_pb2.GraphSyncRef], + data_list: List[Union[DAGData, gs_pb2.GraphSyncData]], + graph_id: Optional[int] = None) -> Union[DataPayload, gs_pb2.GraphSyncAddDataRequest]: + + pass diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/default.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/default.py new file mode 100644 index 00000000..2e51647c --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/default.py @@ -0,0 +1,80 @@ +from __future__ import annotations +from . import DataStructBase +from ..dag_types import SyncQuery, Ref, RefType, DAGData, DataPayload, EdgeType, SyncData +from ..dag_crypto import generate_random_bytes, generate_uid_str +from .... import utils +import base64 +from typing import Optional, List + + +class DataStruct(DataStructBase): + + def sync_query(self, + stream_id: str, + sync_point: int = 0, + graph_id: Optional[int] = None) -> SyncQuery: + + return SyncQuery( + streamId=stream_id, + deviceId=base64.urlsafe_b64encode(generate_random_bytes(16)).decode(), + syncPoint=sync_point, + graphId=graph_id + ) + + @staticmethod + def get_sync_result(results: bytes) -> SyncData: + res = SyncData.model_validate_json(results) + return res + + @staticmethod + def origin_ref(origin_ref_value: bytes, + name: str) -> Ref: + + return Ref( + type=RefType.DEVICE, + value=generate_uid_str(uid_bytes=origin_ref_value), + name=name + ) + + def data(self, + data_type: EdgeType, + tail_uid: str, + content: Optional[bytes] = None, + head_uid: Optional[str] = None, + tail_name: Optional[str] = None, + head_name: Optional[str] = None, + tail_ref_type: Optional[RefType] = None, + head_ref_type: Optional[RefType] = None, + path: Optional[str] = None) -> DAGData: + + if content is not None: + content = utils.base64_url_encode(content) + + return DAGData( + type=data_type, + content=content, + + ref=Ref( + type=tail_ref_type, + value=tail_uid, + name=tail_name + ), + + parentRef=Ref( + type=head_ref_type, + value=head_uid, + name=head_name + ), + path=path + ) + + @staticmethod + def payload(origin_ref: Ref, + data_list: List[DAGData], + graph_id: Optional[int] = None) -> DataPayload: + + return DataPayload( + origin=origin_ref, + dataList=data_list, + graphId=graph_id + ) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/protobuf.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/protobuf.py new file mode 100644 index 00000000..b584d3a0 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/struct/protobuf.py @@ -0,0 +1,184 @@ +from __future__ import annotations +import logging +from typing import Optional, List, Union + +from ....proto import GraphSync_pb2 as gs_pb2 +from ..dag_types import RefType, EdgeType, Ref, SyncData, SyncDataItem, SyncQuery, DataPayload, DAGData +from .. import dag_crypto + + +class DataStructBase: + + def __init__(self, + logger: Optional[logging.Logger] = None): + + if logger is None: + logger = logging.getLogger() + self.logger = logger + + def sync_query(self, + stream_id: str, + sync_point: int = 0, + graph_id: Optional[int] = None) -> Union[SyncQuery, gs_pb2.GraphSyncQuery]: + pass + + @staticmethod + def origin_ref(origin_uid: str, + name: str) -> Union[Ref, gs_pb2.GraphSyncRef]: + pass + + def data(self, + data_type: EdgeType, + tail_uid: str, + content: Optional[bytes] = None, + head_uid: Optional[str] = None, + tail_name: Optional[str] = None, + head_name: Optional[str] = None, + tail_ref_type: Optional[RefType] = None, + head_ref_type: Optional[RefType] = None, + path: Optional[str] = None) -> Union[DAGData,gs_pb2.GraphSyncData]: + + pass + + @staticmethod + def payload(origin_ref: Union[Ref, gs_pb2.GraphSyncRef], + data_list: List[Union[DAGData, gs_pb2.GraphSyncData]], + graph_id: Optional[int] = None) -> Union[DataPayload, gs_pb2.GraphSyncAddDataRequest]: + + pass + + +class DataStruct(DataStructBase): + + REF_TO_PB_MAP = { + RefType.GENERAL: gs_pb2.RefType.RFT_GENERAL, + RefType.USER: gs_pb2.RefType.RFT_USER, + RefType.DEVICE: gs_pb2.RefType.RFT_DEVICE, + RefType.REC: gs_pb2.RefType.RFT_REC, + RefType.FOLDER: gs_pb2.RefType.RFT_FOLDER, + RefType.TEAM: gs_pb2.RefType.RFT_TEAM, + RefType.ENTERPRISE: gs_pb2.RefType.RFT_ENTERPRISE, + RefType.PAM_DIRECTORY: gs_pb2.RefType.RFT_PAM_DIRECTORY, + RefType.PAM_MACHINE: gs_pb2.RefType.RFT_PAM_MACHINE, + RefType.PAM_DATABASE: gs_pb2.RefType.RFT_PAM_DATABASE, + RefType.PAM_USER: gs_pb2.RefType.RFT_PAM_USER, + RefType.PAM_NETWORK: gs_pb2.RefType.RFT_PAM_NETWORK, + RefType.PAM_BROWSER: gs_pb2.RefType.RFT_PAM_BROWSER, + RefType.CONNECTION: gs_pb2.RefType.RFT_CONNECTION, + RefType.WORKFLOW: gs_pb2.RefType.RFT_WORKFLOW, + RefType.NOTIFICATION: gs_pb2.RefType.RFT_NOTIFICATION, + RefType.USER_INFO: gs_pb2.RefType.RFT_USER_INFO, + RefType.TEAM_INFO: gs_pb2.RefType.RFT_TEAM_INFO, + RefType.ROLE: gs_pb2.RefType.RFT_ROLE + } + + DATA_TO_PB_MAP = { + EdgeType.DATA: gs_pb2.GraphSyncDataType.GSE_DATA, + EdgeType.KEY: gs_pb2.GraphSyncDataType.GSE_KEY, + EdgeType.LINK: gs_pb2.GraphSyncDataType.GSE_LINK, + EdgeType.ACL: gs_pb2.GraphSyncDataType.GSE_ACL, + EdgeType.DELETION: gs_pb2.GraphSyncDataType.GSE_DELETION + } + + PB_TO_REF_MAP = {v: k for k, v in REF_TO_PB_MAP.items()} + PB_TO_DATA_MAP = {v: k for k, v in DATA_TO_PB_MAP.items()} + + def sync_query(self, + stream_id: str, + sync_point: int = 0, + graph_id: Optional[int] = None) -> gs_pb2.GraphSyncQuery: + + return gs_pb2.GraphSyncQuery( + streamId=dag_crypto.urlsafe_str_to_bytes(stream_id), + origin=dag_crypto.generate_random_bytes(16), + syncPoint=sync_point, + maxCount=0 + ) + + @staticmethod + def get_sync_result(results: bytes) -> SyncData: + + try: + result = gs_pb2.GraphSyncResult() + result.ParseFromString(results) + except Exception as err: + raise Exception(f"Could not parse the GraphSyncResult message: {err}") + + message = gs_pb2.GraphSyncResult() + message.ParseFromString(results) + + data_list: List[SyncDataItem] = [] + for item in message.data: + data_list.append( + SyncDataItem( + type=DataStruct.PB_TO_DATA_MAP.get(item.data.type), + content=item.data.content, + content_is_base64=False, + ref=Ref( + type=DataStruct.PB_TO_REF_MAP.get(item.data.ref.type), + value=dag_crypto.bytes_to_urlsafe_str(item.data.ref.value), + ), + parentRef=Ref( + type=DataStruct.PB_TO_REF_MAP.get(item.data.parentRef.type), + value=dag_crypto.bytes_to_urlsafe_str(item.data.parentRef.value) + ), + path=item.data.path + ) + ) + + return SyncData( + syncPoint=message.syncPoint, + data=data_list, + hasMore=message.hasMore + ) + + @staticmethod + def origin_ref(origin_ref_value: bytes, + name: str) -> gs_pb2.GraphSyncRef: + + return gs_pb2.GraphSyncRef( + type=gs_pb2.RefType.RFT_DEVICE, + value=origin_ref_value, + name=name + ) + + def data(self, + data_type: EdgeType, + tail_uid: str, + content: Optional[bytes] = None, + head_uid: Optional[str] = None, + tail_name: Optional[str] = None, + head_name: Optional[str] = None, + tail_ref_type: Optional[RefType] = None, + head_ref_type: Optional[RefType] = None, + path: Optional[str] = None) -> gs_pb2.GraphSyncData: + + if isinstance(tail_uid, str): + tail_uid = dag_crypto.urlsafe_str_to_bytes(tail_uid) + if head_uid is not None and isinstance(head_uid, str): + head_uid = dag_crypto.urlsafe_str_to_bytes(head_uid) + + return gs_pb2.GraphSyncData( + type=DataStruct.DATA_TO_PB_MAP.get(data_type), + content=content, + ref=gs_pb2.GraphSyncRef( + type=DataStruct.REF_TO_PB_MAP.get(tail_ref_type), + value=tail_uid, + name=tail_name + ), + parentRef=gs_pb2.GraphSyncRef( + type=DataStruct.REF_TO_PB_MAP.get(head_ref_type), + value=head_uid, + name=head_name + ), + path=path + ) + + @staticmethod + def payload(origin_ref: gs_pb2.GraphSyncRef, + data_list: List[gs_pb2.GraphSyncData], + graph_id: Optional[int] = None) -> gs_pb2.GraphSyncAddDataRequest: + + return gs_pb2.GraphSyncAddDataRequest( + origin=origin_ref, + data=data_list) diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/user_service.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/user_service.py new file mode 100644 index 00000000..b68601f1 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/user_service.py @@ -0,0 +1,626 @@ +from __future__ import annotations +import logging +from .constants import PAM_MACHINE, PAM_USER, PAM_DIRECTORY, DOMAIN_USER_CONFIGS +from .dag_utils import get_connection, user_in_lookup, user_check_list, make_agent +from .dag_types import DiscoveryObject, ServiceAcl, FactsNameUser +from .infrastructure import Infrastructure +from .dag import DAG, EdgeType +from .dag_types import PamGraphId +import importlib +from typing import Any, Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from .dag_vertex import DAGVertex + from .dag_edge import DAGEdge + + +class UserService: + + def __init__(self, record: Any, logger: Optional[Any] = None, history_level: int = 0, + debug_level: int = 0, fail_on_corrupt: bool = True, log_prefix: str = "GS Services/Tasks", + save_batch_count: int = 200, agent: Optional[str] = None, + **kwargs): + + self.conn = get_connection(**kwargs) + + self.record = record + self._dag = None + if logger is None: + logger = logging.getLogger() + self.logger = logger + self.log_prefix = log_prefix + self.history_level = history_level + self.debug_level = debug_level + self.fail_on_corrupt = fail_on_corrupt + self.save_batch_count = save_batch_count + + self.agent = make_agent("user_service") + if agent is not None: + self.agent += "; " + agent + + self.auto_save = False + self.last_sync_point = -1 + + @property + def dag(self) -> DAG: + if self._dag is None: + + self._dag = DAG(conn=self.conn, + record=self.record, + graph_id=PamGraphId.SERVICE_LINKS, + auto_save=False, + logger=self.logger, + history_level=self.history_level, + debug_level=self.debug_level, + name="Discovery Service/Tasks", + fail_on_corrupt=self.fail_on_corrupt, + log_prefix=self.log_prefix, + save_batch_count=self.save_batch_count, + agent=self.agent) + + self._dag.load(sync_point=0) + + return self._dag + + def close(self): + """ + Clean up resources held by this UserService instance. + Releases the DAG instance and connection to prevent memory leaks. + """ + if self._dag is not None: + self._dag = None + self.conn = None + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit - ensures cleanup.""" + self.close() + return False + + def __del__(self): + self.close() + + @property + def has_graph(self) -> bool: + return self.dag.has_graph + + def reload(self): + self._dag.load(sync_point=0) + + def get_record_link(self, uid: str) -> DAGVertex: + return self.dag.get_vertex(uid) + + @staticmethod + def get_record_uid(discovery_vertex: DAGVertex) -> str: + """ + Get the record UID from the vertex + """ + data = discovery_vertex.get_data() + if data is None: + raise Exception(f"The discovery vertex {discovery_vertex.uid} does not have a DATA edge. " + "Cannot get record UID.") + content = DiscoveryObject.get_discovery_object(discovery_vertex) + if content.record_uid is not None: + return content.record_uid + raise Exception(f"The discovery vertex {discovery_vertex.uid} data does not have a populated record UID.") + + def belongs_to(self, resource_uid: str, user_uid: str, acl: Optional[ServiceAcl] = None, + resource_name: Optional[str] = None, user_name: Optional[str] = None): + """ + Link vault records using record UIDs. + If a link already exists, no additional link will be created. + """ + + resource_vertex = self.dag.get_vertex(resource_uid) + if resource_vertex is None: + self.logger.debug(f"adding resource vertex for record UID {resource_uid} ({resource_name})") + resource_vertex = self.dag.add_vertex(uid=resource_uid, name=resource_name) + + user_vertex = self.dag.get_vertex(user_uid) + if user_vertex is None: + self.logger.debug(f"adding user vertex for record UID {user_uid} ({user_name})") + user_vertex = self.dag.add_vertex(uid=user_uid, name=user_name) + + self.logger.debug(f"user {user_vertex.uid} controls services on {resource_vertex.uid}") + + edge_type = EdgeType.LINK + if acl is not None: + edge_type = EdgeType.ACL + + user_vertex.belongs_to(resource_vertex, edge_type=edge_type, content=acl) + + def disconnect_from(self, resource_uid: str, user_uid: str): + resource_vertex = self.dag.get_vertex(resource_uid) + user_vertex = self.dag.get_vertex(user_uid) + user_vertex.disconnect_from(resource_vertex) + + def get_acl(self, resource_uid, user_uid) -> Optional[ServiceAcl]: + """ + Get the service/task ACL between a resource and the user. + """ + resource_vertex = self.dag.get_vertex(resource_uid) + user_vertex = self.dag.get_vertex(user_uid) + if resource_vertex is None or user_vertex is None: + self.logger.debug(f"there is no acl between {resource_uid} and {user_uid}") + return ServiceAcl() + + acl_edge = user_vertex.get_edge(resource_vertex, edge_type=EdgeType.ACL) # type: DAGEdge + if acl_edge is None: + return None + + return acl_edge.content_as_object(ServiceAcl) + + def resource_has_link(self, resource_uid) -> bool: + """ + Is this resource linked to the configuration? + """ + resource_vertex = self.dag.get_vertex(resource_uid) + if resource_vertex is None: + return False + link_edge = resource_vertex.get_edge(self.dag.get_root, edge_type=EdgeType.LINK) # type: DAGEdge + return link_edge is not None + + def get_resource_vertices(self, user_uid: str) -> List[DAGVertex]: + """ + Get the resource vertices where the user is used for a service or task. + """ + + user_vertex = self.dag.get_vertex(user_uid) + if user_vertex is None: + return [] + return user_vertex.belongs_to_vertices() + + def get_user_vertices(self, resource_uid: str) -> List[DAGVertex]: + """ + Get the user vertices that control a service or task on this machine. + """ + resource_vertex = self.dag.get_vertex(resource_uid) + if resource_vertex is None: + return [] + return resource_vertex.has_vertices() + + @staticmethod + def delete(vertex: DAGVertex): + if vertex is not None: + vertex.delete() + + def save(self): + if self.dag.has_graph: + self.logger.debug("saving the service user.") + self.dag.save(delta_graph=False) + else: + self.logger.debug("the service user graph does not contain any data, was not saved.") + + def to_dot(self, graph_format: str = "svg", show_version: bool = True, show_only_active_vertices: bool = True, + show_only_active_edges: bool = True, graph_type: str = "dot"): + + try: + mod = importlib.import_module("graphviz") + except ImportError: + raise Exception("Cannot to_dot(), graphviz module is not installed.") + + dot = getattr(mod, "Digraph")(comment=f"DAG for Services/Tasks", format=graph_format) + + if graph_type == "dot": + dot.attr(rankdir='RL') + elif graph_type == "twopi": + dot.attr(layout="twopi") + dot.attr(ranksep="10") + dot.attr(ratio="auto") + else: + dot.attr(layout=graph_type) + + self.logger.debug(f"have {len(self.dag.all_vertices)} vertices") + for v in self.dag.all_vertices: + if show_only_active_vertices is True and v.active is False: + continue + + tooltip = "" + + for edge in v.edges: + + color = "grey" + style = "solid" + + # To reduce the number of edges, only show the active edges + if edge.active: + color = "black" + style = "bold" + elif show_only_active_edges: + continue + + # If the vertex is not active, gray out the DATA edge + if edge.edge_type == EdgeType.DATA and v.active is False: + color = "grey" + + if edge.edge_type == EdgeType.DELETION: + style = "dotted" + + edge_tip = "" + if edge.edge_type == EdgeType.ACL and v.active is True: + content = edge.content_as_dict + red = "00" + green = "00" + blue = "00" + if content.get("is_service"): + red = "FF" + if content.get("is_task"): + blue = "FF" + if content.get("is_iis_pool"): + green = "FF" + if red == "FF" and blue == "FF" and green == "FF": + color = "#808080" + else: + color = f"#{red}{green}{blue}" + style = "bold" + + tooltip += f"TO {edge.head_uid}\\n" + for k, val in content.items(): + tooltip += f" * {k}={val}\\n" + tooltip += f"--------------------\\n\\n" + + label = DAG.EDGE_LABEL.get(edge.edge_type) + if label is None: + label = "UNK" + if edge.path is not None and edge.path != "": + label += f"\\npath={edge.path}" + if show_version: + label += f"\\nv={edge.version}" + + dot.edge(v.uid, edge.head_uid, label, style=style, fontcolor=color, color=color, tooltip=edge_tip) + + shape = "ellipse" + fillcolor = "white" + color = "black" + if not v.active: + fillcolor = "grey" + + label = f"uid={v.uid}" + dot.node(v.uid, label, color=color, fillcolor=fillcolor, style="filled", shape=shape, tooltip=tooltip) + + return dot + + def _get_directory_user_vertices(self, configuration_vertex: DAGVertex, domain_name: str) -> List[DAGVertex]: + """ + Find the directory in the graph and return of list of user vertices. + """ + + domain_name = domain_name.lower() + + user_vertices: List[DAGVertex] = [] + + config_content = DiscoveryObject.get_discovery_object(configuration_vertex) + if config_content.record_type in DOMAIN_USER_CONFIGS: + config_domains = config_content.item.info.get("domains", []) + self.logger.debug(f" the provider provides domains: {config_domains}") + for config_domain in config_domains: + if config_domain.lower() == domain_name: + self.logger.debug(f" matched for {domain_name}") + for vertex in configuration_vertex.has_vertices(): + content = DiscoveryObject.get_discovery_object(vertex) + if content.record_type == PAM_USER: + user_vertices.append(vertex) + self.logger.debug(f" found {len(user_vertices)} users for {domain_name}") + return user_vertices + + self.logger.debug(" checking pam directories for users") + + for resource_vertex in configuration_vertex.has_vertices(): + content = DiscoveryObject.get_discovery_object(resource_vertex) + if content.record_type != PAM_DIRECTORY: + continue + if content.name.lower() == domain_name: + user_vertices = resource_vertex.has_vertices() + self.logger.debug(f" found {len(user_vertices)} users for {domain_name}") + break + + return user_vertices + + def _get_user_vertices(self, + infra_resource_content: DiscoveryObject, + infra_resource_vertex: DAGVertex) -> List[DAGVertex]: + + self.logger.debug(f" getting users for {infra_resource_content.name}") + + domain_name = None + if len(infra_resource_content.item.facts.directories) > 0: + domain_name = infra_resource_content.item.facts.directories[0].domain + self.logger.debug(f" joined to {domain_name}") + + user_vertices = infra_resource_vertex.has_vertices() + self.logger.debug(f" found {len(user_vertices)} local users") + if domain_name is not None: + user_vertices += self._get_directory_user_vertices( + configuration_vertex=infra_resource_vertex.belongs_to_vertices()[0], + domain_name=domain_name + ) + + self.logger.debug(f" found {len(user_vertices)} total users") + + return user_vertices + + def _connect_service_users(self, + infra_resource_content: DiscoveryObject, + infra_resource_vertex: DAGVertex, + services: List[FactsNameUser]): + + self.logger.debug(f"processing services for {infra_resource_content.description} ({infra_resource_vertex.uid})") + + lookup = {} + for service in services: + lookup[service.user.lower()] = True + + infra_user_vertices = self._get_user_vertices(infra_resource_content=infra_resource_content, + infra_resource_vertex=infra_resource_vertex) + + for infra_user_vertex in infra_user_vertices: + infra_user_content = DiscoveryObject.get_discovery_object(infra_user_vertex) + if infra_user_content.record_uid is None: + continue + if user_in_lookup( + lookup=lookup, + user=infra_user_content.item.user, + name=infra_user_content.name, + source=infra_user_content.item.source): + self.logger.debug(f" * found user for service: {infra_user_content.item.user}") + acl = self.get_acl(infra_resource_content.record_uid, infra_user_content.record_uid) + if acl is None: + acl = ServiceAcl() + acl.is_service = True + self.belongs_to( + resource_uid=infra_resource_content.record_uid, + resource_name=infra_resource_content.uid, + user_uid=infra_user_content.record_uid, + user_name=infra_user_content.uid, + acl=acl) + + def _connect_task_users(self, + infra_resource_content: DiscoveryObject, + infra_resource_vertex: DAGVertex, + tasks: List[FactsNameUser]): + + self.logger.debug(f"processing tasks for {infra_resource_content.description} ({infra_resource_vertex.uid})") + + lookup = {} + for task in tasks: + lookup[task.user.lower()] = True + + infra_user_vertices = self._get_user_vertices(infra_resource_content=infra_resource_content, + infra_resource_vertex=infra_resource_vertex) + + for infra_user_vertex in infra_user_vertices: + infra_user_content = DiscoveryObject.get_discovery_object(infra_user_vertex) + if infra_user_content.record_uid is None: + continue + if user_in_lookup( + lookup=lookup, + user=infra_user_content.item.user, + name=infra_user_content.name, + source=infra_user_content.item.source): + self.logger.debug(f" * found user for task: {infra_user_content.item.user}") + acl = self.get_acl(infra_resource_content.record_uid, infra_user_content.record_uid) + if acl is None: + acl = ServiceAcl() + acl.is_task = True + self.belongs_to( + resource_uid=infra_resource_content.record_uid, + resource_name=infra_resource_content.uid, + user_uid=infra_user_content.record_uid, + user_name=infra_user_content.uid, + acl=acl) + + def _connect_iis_pool_users(self, + infra_resource_content: DiscoveryObject, + infra_resource_vertex: DAGVertex, + iis_pools: List[FactsNameUser]): + + self.logger.debug(f"processing iis pools for " + f"{infra_resource_content.description} ({infra_resource_vertex.uid})") + + lookup = {} + for iis_pool in iis_pools: + lookup[iis_pool.user.lower()] = True + + infra_user_vertices = self._get_user_vertices(infra_resource_content=infra_resource_content, + infra_resource_vertex=infra_resource_vertex) + + for infra_user_vertex in infra_user_vertices: + infra_user_content = DiscoveryObject.get_discovery_object(infra_user_vertex) + if infra_user_content.record_uid is None: + continue + if user_in_lookup( + lookup=lookup, + user=infra_user_content.item.user, + name=infra_user_content.name, + source=infra_user_content.item.source): + self.logger.debug(f" * found user for iis pool: {infra_user_content.item.user}") + acl = self.get_acl(infra_resource_content.record_uid, infra_user_content.record_uid) + if acl is None: + acl = ServiceAcl() + acl.is_iis_pool = True + self.belongs_to( + resource_uid=infra_resource_content.record_uid, + resource_name=infra_resource_content.uid, + user_uid=infra_user_content.record_uid, + user_name=infra_user_content.uid, + acl=acl) + + def _validate_users(self, + infra_resource_content: DiscoveryObject, + infra_resource_vertex: DAGVertex): + """ + This method will check to see if a resource's users' ACL edges are still valid. + This check will check both local and directory users. + """ + self.logger.debug(f"validate existing user service edges to see if still valid to " + f"{infra_resource_content.name}") + + service_lookup = {} + for service in infra_resource_content.item.facts.services: + service_lookup[service.user.lower()] = True + + task_lookup = {} + for task in infra_resource_content.item.facts.tasks: + task_lookup[task.user.lower()] = True + + iis_pool_lookup = {} + for iss_pool in infra_resource_content.item.facts.iis_pools: + iis_pool_lookup[iss_pool.user.lower()] = True + + user_service_resource_vertex = self.dag.get_vertex(infra_resource_content.record_uid) + if user_service_resource_vertex is None: + return + + infra_dag = infra_resource_vertex.dag + + for user_service_user_vertex in user_service_resource_vertex.has_vertices(): + acl_edge = user_service_user_vertex.get_edge( + user_service_resource_vertex, edge_type=EdgeType.ACL) # type: DAGEdge + if acl_edge is None: + self.logger.info(f"User record {user_service_user_vertex.uid} does not have an ACL edge to " + f"{user_service_resource_vertex.uid} for user services.") + continue + + found_service_acl = False + found_task_acl = False + found_iis_pool_acl = False + changed = False + + acl = acl_edge.content_as_object(ServiceAcl) + + user = infra_dag.search_content({"record_type": PAM_USER, "record_uid": user_service_user_vertex.uid}) + infra_user_content = None + found_user = len(user) > 0 + if found_user: + infra_user_vertex = user[0] + if infra_user_vertex.active is False: + found_user = False + else: + infra_user_content = DiscoveryObject.get_discovery_object(infra_user_vertex) + + if not found_user: + self.disconnect_from(user_service_resource_vertex.uid, user_service_user_vertex.uid) + continue + + check_list = user_check_list( + user=infra_user_content.item.user, + name=infra_user_content.name, + source=infra_user_content.item.source + ) + + if acl.is_service: + for check_user in check_list: + if check_user in service_lookup: + found_service_acl = True + break + if not found_service_acl: + acl.is_service = False + changed = True + + if acl.is_task: + for check_user in check_list: + if check_user in task_lookup: + found_task_acl = True + break + if not found_task_acl: + acl.is_task = False + changed = True + + if acl.is_iis_pool: + for check_user in check_list: + if check_user in iis_pool_lookup: + found_iis_pool_acl = True + break + if not found_iis_pool_acl: + acl.is_iis_pool = False + changed = True + + if (found_service_acl is True or found_task_acl is True or found_iis_pool_acl is True) or changed is True: + self.logger.debug(f"user {user_service_user_vertex.uid}(US) to " + f"{user_service_resource_vertex.uid} updated") + self.belongs_to(user_service_resource_vertex.uid, user_service_user_vertex.uid, acl) + elif found_service_acl is False and found_task_acl is False and found_iis_pool_acl is False: + self.logger.debug(f"user {user_service_user_vertex.uid}(US) to " + f"{user_service_resource_vertex.uid} disconnected") + self.disconnect_from(user_service_resource_vertex.uid, user_service_user_vertex.uid) + + self.logger.debug(f"DONE validate existing user") + + def run(self, infra: Optional[Infrastructure] = None, **kwargs): + """ + Map users to services/tasks on machines. + IMPORTANT: To avoid memory leaks, pass an existing Infrastructure instance + instead of letting this method create a new one. Example: + user_service.run(infra=process.infra) + """ + + self.logger.debug("") + self.logger.debug("##########################################################################################") + self.logger.debug("# MAP USER TO MACHINE FOR SERVICE/TASKS") + self.logger.debug("") + + # If an instance of Infrastructure is not passed in. + # NOTE: Creating a new Infrastructure instance here can cause memory leaks. + # Prefer passing an existing instance via the infra parameter. + _cleanup_infra_on_exit = False + if infra is None: + self.logger.warning("Creating new Infrastructure instance - consider passing existing instance to avoid memory leaks") + + # Get ksm from the connection. + if hasattr(self.conn, "ksm"): + kwargs["ksm"] = getattr(self.conn, "ksm") + + infra = Infrastructure(record=self.record, **kwargs) + infra.load() + _cleanup_infra_on_exit = True + + infra_root_vertex = infra.get_root + infra_config_vertex = infra_root_vertex.has_vertices()[0] + + user_service_config_vertex = self.dag.get_root + + for infra_resource_vertex in infra_config_vertex.has_vertices(): + if infra_resource_vertex.active is False or infra_resource_vertex.has_data is False: + continue + infra_resource_content = DiscoveryObject.get_discovery_object(infra_resource_vertex) + if infra_resource_content.record_type == PAM_MACHINE: + + self.logger.debug(f"checking {infra_resource_content.name}") + + self._validate_users(infra_resource_content, infra_resource_vertex) + + if infra_resource_content.item.facts.has_service_items is True: + + user_service_resource_vertex = self.dag.get_vertex(infra_resource_content.record_uid) + if user_service_resource_vertex is None: + user_service_resource_vertex = self.dag.add_vertex(uid=infra_resource_content.record_uid, + name=infra_resource_content.description) + if not user_service_config_vertex.has(user_service_resource_vertex): + user_service_resource_vertex.belongs_to_root(EdgeType.LINK) + + if infra_resource_content.item.facts.has_services is True: + self._connect_service_users( + infra_resource_content, + infra_resource_vertex, + infra_resource_content.item.facts.services) + + if infra_resource_content.item.facts.has_tasks is True: + self._connect_task_users( + infra_resource_content, + infra_resource_vertex, + infra_resource_content.item.facts.tasks) + + if infra_resource_content.item.facts.has_iis_pools is True: + self._connect_iis_pool_users( + infra_resource_content, + infra_resource_vertex, + infra_resource_content.item.facts.iis_pools) + + self.save() + + if _cleanup_infra_on_exit and infra is not None: + self.logger.debug("cleaning up Infrastructure instance created in run()") + infra.close() diff --git a/keepersdk-package/src/keepersdk/helpers/pam_config_facade.py b/keepersdk-package/src/keepersdk/helpers/pam_config_facade.py new file mode 100644 index 00000000..8ad2ed38 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/pam_config_facade.py @@ -0,0 +1,97 @@ + +from typing import Any, Dict, List, Optional + +from ..vault.record_facades import TypedRecordFacade, string_getter +from ..vault import vault_record, record_types + + +class PamConfigurationRecordFacade(TypedRecordFacade): + _file_ref_getter = string_getter('_file_ref') + + def __init__(self): + super(PamConfigurationRecordFacade, self).__init__() + self._pam_resources = None + self._port_mapping = None + self._file_ref = None + + def load_typed_fields(self): + if self.record: + self._pam_resources = next((x for x in self.record.fields if x.type == 'pamResources'), None) + if not self._pam_resources: + self._pam_resources = vault_record.TypedField.create_field(field_type='pamResources', field_label='pamResources', required=False) + self.record.fields.append(self._pam_resources) + + if len(self._pam_resources.value) > 0: + if not isinstance(self._pam_resources.value[0], dict): + self._pam_resources.value.clear() + + if len(self._pam_resources.value) == 0: + if 'pamResources' in record_types.FieldTypes and isinstance(record_types.FieldTypes['pamResources'].value, dict): + value = record_types.FieldTypes['pamResources'].value.copy() + else: + value = {} + self._pam_resources.value.append(value) + + self._port_mapping = next((x for x in self.record.fields + if x.type == 'multiline' and x.label == 'portMapping'), None) + if self._port_mapping is None: + self._port_mapping = vault_record.TypedField.create_field(field_type='multiline', field_label='portMapping', required=False) + self.record.fields.append(self._port_mapping) + + self._file_ref = next((x for x in self.record.fields if x.type == 'fileRef' and x.label == 'rotationScripts'), None) + if self._file_ref is None: + self._file_ref = vault_record.TypedField.create_field(field_type='fileRef', field_label='rotationScripts', required=False) + self.record.fields.append(self._file_ref) + else: + self._pam_resources = None + self._port_mapping = None + self._file_ref = None + + super(PamConfigurationRecordFacade, self).load_typed_fields() + + def _pam_resources_dict(self) -> Optional[Dict[str, Any]]: + """pamResources stores controllerUid, folderUid, and resourceRef in value[0].""" + if not self._pam_resources or not self._pam_resources.value: + return None + v0 = self._pam_resources.value[0] + return v0 if isinstance(v0, dict) else None + + @property + def controller_uid(self) -> str: + d = self._pam_resources_dict() + if not d: + return '' + return d.get('controllerUid') or '' + + @controller_uid.setter + def controller_uid(self, value: str) -> None: + d = self._pam_resources_dict() + if d is not None: + d['controllerUid'] = value or '' + + @property + def folder_uid(self) -> str: + d = self._pam_resources_dict() + if not d: + return '' + return d.get('folderUid') or '' + + @folder_uid.setter + def folder_uid(self, value: str) -> None: + d = self._pam_resources_dict() + if d is not None: + d['folderUid'] = value or '' + + @property + def resource_ref(self) -> List[str]: + d = self._pam_resources_dict() + if not d: + return [] + refs = d.get('resourceRef') + if isinstance(refs, list): + return refs + return [] + + @property + def rotation_scripts(self): + return PamConfigurationRecordFacade._file_ref_getter(self) diff --git a/keepersdk-package/src/keepersdk/helpers/pam_user_record_facade.py b/keepersdk-package/src/keepersdk/helpers/pam_user_record_facade.py new file mode 100644 index 00000000..f6892f7b --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/pam_user_record_facade.py @@ -0,0 +1,123 @@ +from ..vault.record_facades import TypedRecordFacade, string_getter, string_setter, TypedField + +def boolean_getter(name): + def getter(obj): + field = getattr(obj, name) + if isinstance(field, TypedField): + value = field.value[0] if len(field.value) > 0 else None + if value is None: + return None + elif isinstance(value, bool) is True: + return value + + if str(value).lower() in ['true', 'yes', '1', 'on']: + return True + elif str(value).lower() in ['false', 'no', '0', 'off']: + return False + return None + return getter + +def boolean_setter(name): + def setter(obj, value): + field = getattr(obj, name) + if isinstance(field, TypedField): + if value is not None: + if isinstance(value, bool) is not True: + if str(value).lower() in ['true', 'yes', '1', 'on']: + value = True + elif str(value).lower() in ['false', 'no', '0', 'off']: + value = False + if len(field.value) > 0: + field.value[0] = value + else: + field.value.append(value) + else: + field.value.clear() + return setter + +class PamUserRecordFacade(TypedRecordFacade): + _login_getter = string_getter('_login') + _login_setter = string_setter('_login') + _password_getter = string_getter('_password') + _password_setter = string_setter('_password') + _distinguishedName_getter = string_getter('_distinguishedName') + _distinguishedName_setter = string_setter('_distinguishedName') + _connectDatabase_getter = string_getter('_connectDatabase') + _connectDatabase_setter = string_setter('_connectDatabase') + _managed_getter = boolean_getter('_managed') + _managed_setter = boolean_setter('_managed') + _oneTimeCode_getter = string_getter('_oneTimeCode') + _oneTimeCode_setter = string_setter('_oneTimeCode') + + def __init__(self): + super(PamUserRecordFacade, self).__init__() + self._login = None + self._password = None + self._distinguishedName = None + self._connectDatabase = None + self._managed = None + self._oneTimeCode = None + + @property + def login(self): + return PamUserRecordFacade._login_getter(self) + + @login.setter + def login(self, value): + PamUserRecordFacade._login_setter(self, value) + + @property + def password(self): + return PamUserRecordFacade._password_getter(self) + + @password.setter + def password(self, value): + PamUserRecordFacade._password_setter(self, value) + + @property + def distinguishedName(self): + return PamUserRecordFacade._distinguishedName_getter(self) + + @distinguishedName.setter + def distinguishedName(self, value): + PamUserRecordFacade._distinguishedName_setter(self, value) + + @property + def connectDatabase(self): + return PamUserRecordFacade._connectDatabase_getter(self) + + @connectDatabase.setter + def connectDatabase(self, value): + PamUserRecordFacade._connectDatabase_setter(self, value) + + @property + def managed(self): + return PamUserRecordFacade._connectDatabase_getter(self) + + @managed.setter + def managed(self, value): + PamUserRecordFacade._managed_setter(self, value) + + @property + def oneTimeCode(self): + return PamUserRecordFacade._oneTimeCode_getter(self) + + @oneTimeCode.setter + def oneTimeCode(self, value): + PamUserRecordFacade._oneTimeCode_setter(self, value) + + def load_typed_fields(self): + if self.record: + self.record.type_name = 'pamUser' + for attr in ["login", "password", "distinguishedName", "connectDatabase", "managed", "oneTimeCode"]: + attr_prv = f"_{attr}" + value = next((x for x in self.record.fields if x.type == attr), None) + setattr(self, attr_prv, value) + if value is None: + value = TypedField.create_field(field_type=attr, field_label='', required=False) + setattr(self, attr_prv, value) + self.record.fields.append(value) + else: + for attr in ["_login", "_password", "_distinguishedName", "_connectDatabase", "_managed", "_oneTimeCode"]: + setattr(self, attr, None) + super(PamUserRecordFacade, self).load_typed_fields() diff --git a/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py new file mode 100644 index 00000000..0ebc36c9 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py @@ -0,0 +1,609 @@ +import logging + +from . import tunnel_utils + +from ..keeper_dag.connection.commander import Connection +from ..keeper_dag.dag_types import EdgeType, RefType, PamEndpoints +from ..keeper_dag.dag import DAG +from ..keeper_dag.dag_vertex import DAGVertex +from ..keeper_dag.dag_crypto import generate_random_bytes + +from ...vault import vault_online, vault_record + +logger = logging.getLogger(__name__) + +def get_vertex_content(vertex): + return_content = None + if vertex is None: + return return_content + try: + return_content = vertex.content_as_dict + except Exception as e: + logger.debug(f"Error getting vertex content: {e}") + return_content = None + return return_content + + +# Resource meta version (int). Vault uses version >= 1 to read launch credentials from ACL. +# In set_resource_allowed: meta_version=None or 0 -> legacy (no version in meta); 1 -> v1. +# Future: add RESOURCE_META_VERSION_V2, etc. and handle them in build_resource_meta(). +RESOURCE_META_VERSION_V1 = 1 + + +def build_resource_meta_v1(allowed_settings, rotate_on_termination=False): + """ + Build DAG resource meta payload in v1 format so vault uses ACL is_launch_credential for launch. + Returns dict: {"version": , "allowedSettings": allowed_settings, "rotateOnTermination": bool}. + """ + if not isinstance(allowed_settings, dict): + allowed_settings = {} + return { + "version": int(RESOURCE_META_VERSION_V1), + "allowedSettings": dict(allowed_settings), + "rotateOnTermination": bool(rotate_on_termination), + } + +def ensure_resource_meta_v1(content): + """ + Ensure existing meta content has version 1 and rotateOnTermination (for re-writes). + Returns a copy with version= and rotateOnTermination default False if missing. + """ + if content is None: + return build_resource_meta_v1({}, False) + out = dict(content) + out["version"] = int(RESOURCE_META_VERSION_V1) + if "rotateOnTermination" not in out: + out["rotateOnTermination"] = False + # Normalize allowedSettings key if content used a different key (e.g. allowedSettings) + if "allowedSettings" not in out and "allowed_settings" in out: + out["allowedSettings"] = out.pop("allowed_settings", {}) + return out + + +class TunnelDAG: + def __init__(self, vault: vault_online.VaultOnline, encrypted_session_token, encrypted_transmission_key, record_uid: str, is_config=False): + config_uid = None + if not is_config: + config_uid = tunnel_utils.get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) + if not config_uid: + config_uid = record_uid + self.record = vault_record.PasswordRecord() + self.record.record_uid = config_uid + self.record.record_key = generate_random_bytes(32) + self.encrypted_session_token = encrypted_session_token + self.encrypted_transmission_key = encrypted_transmission_key + self.conn = Connection(vault=vault, encrypted_transmission_key=self.encrypted_transmission_key, + encrypted_session_token=self.encrypted_session_token, + use_write_protobuf=True + ) + self.linking_dag = DAG(conn=self.conn, record=self.record, graph_id=0, write_endpoint=PamEndpoints.PAM) + try: + self.linking_dag.load() + except Exception as e: + import logging + logging.debug(f"Error loading config: {e}") + + def resource_belongs_to_config(self, resource_uid): + if not self.linking_dag.has_graph: + return False + resource_vertex = self.linking_dag.get_vertex(resource_uid) + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + return resource_vertex and config_vertex.has(resource_vertex, EdgeType.LINK) + + def user_belongs_to_config(self, user_uid): + if not self.linking_dag.has_graph: + return False + user_vertex = self.linking_dag.get_vertex(user_uid) + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + res_content = False + if user_vertex and config_vertex and config_vertex.has(user_vertex, EdgeType.ACL): + acl_edge = user_vertex.get_edge(config_vertex, EdgeType.ACL) + _content = acl_edge.content_as_dict + res_content = _content.get('belongs_to', False) if _content else False + return res_content + + def check_tunneling_enabled_config(self, enable_connections=None, enable_tunneling=None, + enable_rotation=None, enable_session_recording=None, + enable_typescript_recording=None, remote_browser_isolation=None): + if not self.linking_dag.has_graph: + return False + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + content = get_vertex_content(config_vertex) + if content is None or not content.get('allowedSettings'): + return False + + allowed_settings = content['allowedSettings'] + if enable_connections and not allowed_settings.get("connections"): + return False + if enable_tunneling and not allowed_settings.get("portForwards"): + return False + if enable_rotation and not allowed_settings.get("rotation"): + return False + if allowed_settings.get("connections") and allowed_settings["connections"]: + if enable_session_recording and not allowed_settings.get("sessionRecording"): + return False + if enable_typescript_recording and not allowed_settings.get("typescriptRecording"): + return False + if remote_browser_isolation and not allowed_settings.get("remoteBrowserIsolation"): + return False + return True + + @staticmethod + def _convert_allowed_setting(value): + """Converts on/off/default|any to True/False/None""" + if value is None or isinstance(value, bool): + return value + return {"on": True, "off": False}.get(str(value).lower(), None) + + def edit_tunneling_config(self, connections=None, tunneling=None, + rotation=None, session_recording=None, + typescript_recording=None, + remote_browser_isolation=None): + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + if config_vertex is None: + config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid, vertex_type=RefType.PAM_NETWORK) + + if config_vertex.vertex_type != RefType.PAM_NETWORK: + config_vertex.vertex_type = RefType.PAM_NETWORK + content = get_vertex_content(config_vertex) + if content and content.get('allowedSettings'): + allowed_settings = dict(content['allowedSettings']) + del content['allowedSettings'] + content = {'allowedSettings': allowed_settings} + + if content is None: + content = {'allowedSettings': {}} + if 'allowedSettings' not in content: + content['allowedSettings'] = {} + + allowed_settings = content['allowedSettings'] + dirty = False + + if connections is not None: + connections = self._convert_allowed_setting(connections) + if connections != allowed_settings.get("connections", None): + dirty = True + if connections is None: + allowed_settings.pop("connections", None) + else: + allowed_settings["connections"] = connections + + if tunneling is not None: + tunneling = self._convert_allowed_setting(tunneling) + if tunneling != allowed_settings.get("portForwards", None): + dirty = True + if tunneling is None: + allowed_settings.pop("portForwards", None) + else: + allowed_settings["portForwards"] = tunneling + + if rotation is not None: + rotation = self._convert_allowed_setting(rotation) + if rotation != allowed_settings.get("rotation", None): + dirty = True + if rotation is None: + allowed_settings.pop("rotation", None) + else: + allowed_settings["rotation"] = rotation + + if session_recording is not None: + session_recording = self._convert_allowed_setting(session_recording) + if session_recording != allowed_settings.get("sessionRecording", None): + dirty = True + if session_recording is None: + allowed_settings.pop("sessionRecording", None) + else: + allowed_settings["sessionRecording"] = session_recording + + if typescript_recording is not None: + typescript_recording = self._convert_allowed_setting(typescript_recording) + if typescript_recording != allowed_settings.get("typescriptRecording", None): + dirty = True + if typescript_recording is None: + allowed_settings.pop("typescriptRecording", None) + else: + allowed_settings["typescriptRecording"] = typescript_recording + + if remote_browser_isolation is not None: + remote_browser_isolation = self._convert_allowed_setting(remote_browser_isolation) + if remote_browser_isolation != allowed_settings.get("remoteBrowserIsolation", None): + dirty = True + if remote_browser_isolation is None: + allowed_settings.pop("remoteBrowserIsolation", None) + else: + allowed_settings["remoteBrowserIsolation"] = remote_browser_isolation + + if dirty: + config_vertex.add_data(content=content, path='meta', needs_encryption=False) + self.linking_dag.save() + + def get_all_owners(self, uid): + owners = [] + if self.linking_dag.has_graph: + vertex = self.linking_dag.get_vertex(uid) + if vertex: + owners = [owner.uid for owner in vertex.belongs_to_vertices()] + return owners + + def user_belongs_to_resource(self, user_uid, resource_uid): + user_vertex = self.linking_dag.get_vertex(user_uid) + resource_vertex = self.linking_dag.get_vertex(resource_uid) + res_content = False + if user_vertex and resource_vertex and resource_vertex.has(user_vertex, EdgeType.ACL): + acl_edge = user_vertex.get_edge(resource_vertex, EdgeType.ACL) + _content = acl_edge.content_as_dict + res_content = _content.get('belongs_to', False) if _content else False + return res_content + + def get_resource_uid(self, user_uid): + if not self.linking_dag.has_graph: + return None + resources = self.get_all_owners(user_uid) + if len(resources) > 0: + for resource in resources: + if self.user_belongs_to_resource(user_uid, resource): + return resource + return None + + def link_resource_to_config(self, resource_uid): + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + if config_vertex is None: + config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid) + + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + resource_vertex = self.linking_dag.add_vertex(uid=resource_uid) + + if not config_vertex.has(resource_vertex, EdgeType.LINK): + resource_vertex.belongs_to(config_vertex, EdgeType.LINK) + self.linking_dag.save() + + def link_user_to_config(self, user_uid): + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + if config_vertex is None: + config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid) + self.link_user(user_uid, config_vertex, belongs_to=True, is_iam_user=True) + + def link_user_to_config_with_options(self, user_uid, is_admin=None, belongs_to=None, is_iam_user=None): + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + if config_vertex is None: + config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid) + + # self.link_user(user_uid, config_vertex, is_admin, belongs_to, is_iam_user) + source_vertex = config_vertex + user_vertex = self.linking_dag.get_vertex(user_uid) + if user_vertex is None: + user_vertex = self.linking_dag.add_vertex(uid=user_uid, vertex_type=RefType.PAM_USER) + + # switching to 3-state on/off/default: on/true, off/false, + # None = Keep existing, 'default' = Reset to default (remove from dict) + states = {'on': True, 'off': False, 'default': '', 'none': None} + + content = { + "belongs_to": states.get(str(belongs_to).lower()), + "is_admin": states.get(str(is_admin).lower()), + "is_iam_user": states.get(str(is_iam_user).lower()) + } + if user_vertex.vertex_type != RefType.PAM_USER: + user_vertex.vertex_type = RefType.PAM_USER + + dirty = False + if source_vertex.has(user_vertex, EdgeType.ACL): + acl_edge = user_vertex.get_edge(source_vertex, EdgeType.ACL) + existing_content = acl_edge.content_as_dict or {} + old_content = existing_content.copy() + for key in list(existing_content.keys()): + if content.get(key) is not None: + if content[key] == '': + existing_content.pop(key) + elif content[key] in (True, False): + existing_content[key] = content[key] + content = {k: v for k, v in content.items() if v not in (None, '')} + for k, v in content.items(): + existing_content.setdefault(k, v) + if existing_content != old_content: + dirty = True + + if dirty: + user_vertex.belongs_to(source_vertex, EdgeType.ACL, content=existing_content) + # user_vertex.add_data(content=existing_content, needs_encryption=False) + self.linking_dag.save() + else: + content = {k: v for k, v in content.items() if v not in (None, '')} + user_vertex.belongs_to(source_vertex, EdgeType.ACL, content=content) + self.linking_dag.save() + + def unlink_user_from_resource(self, user_uid, resource_uid) -> bool: + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None or not self.resource_belongs_to_config(resource_uid): + logger.error(f"Resource {resource_uid} does not belong to the configuration") + return False + + user_vertex = self.linking_dag.get_vertex(user_uid) + if user_vertex is None or user_vertex.vertex_type != RefType.PAM_USER: + return False + + if resource_vertex.has(user_vertex, EdgeType.ACL): + acl_edge = user_vertex.get_edge(resource_vertex, EdgeType.ACL) + edge_content = acl_edge.content_as_dict or {} + link_keys = ('belongs_to', 'is_admin') # "is_iam_user" + dirty = any(key in link_keys for key in edge_content) + if dirty: + for link_key in link_keys: + edge_content.pop(link_key, None) + user_vertex.belongs_to(resource_vertex, EdgeType.ACL, content=edge_content) + self.linking_dag.save() + return True + + return False + + def link_user_to_resource(self, user_uid, resource_uid, is_admin=None, belongs_to=None): + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None or not self.resource_belongs_to_config(resource_uid): + logger.error(f"Resource {resource_uid} does not belong to the configuration") + return False + self.link_user(user_uid, resource_vertex, is_admin, belongs_to) + return None + + def link_user(self, user_uid, source_vertex: DAGVertex, is_admin=None, belongs_to=None, is_iam_user=None): + + user_vertex = self.linking_dag.get_vertex(user_uid) + if user_vertex is None: + user_vertex = self.linking_dag.add_vertex(uid=user_uid, vertex_type=RefType.PAM_USER) + + content = {} + dirty = False + if belongs_to is not None: + content["belongs_to"] = bool(belongs_to) + if is_admin is not None: + content["is_admin"] = bool(is_admin) + if is_iam_user is not None: + content["is_iam_user"] = bool(is_iam_user) + + if user_vertex.vertex_type != RefType.PAM_USER: + user_vertex.vertex_type = RefType.PAM_USER + + if source_vertex.has(user_vertex, EdgeType.ACL): + acl_edge = user_vertex.get_edge(source_vertex, EdgeType.ACL) + existing_content = acl_edge.content_as_dict or {} + for key in existing_content: + if key not in content: + content[key] = existing_content[key] + if content != existing_content: + dirty = True + + if dirty: + user_vertex.belongs_to(source_vertex, EdgeType.ACL, content=content) + self.linking_dag.save() + else: + user_vertex.belongs_to(source_vertex, EdgeType.ACL, content=content) + self.linking_dag.save() + + def get_all_admins(self): + if not self.linking_dag.has_graph: + return [] + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + if config_vertex is None: + return [] + admins = [] + for user_vertex in config_vertex.has_vertices(EdgeType.ACL): + acl_edge = user_vertex.get_edge(config_vertex, EdgeType.ACL) + if acl_edge: + content = acl_edge.content_as_dict + if content.get('is_admin'): + admins.append(user_vertex.uid) + return admins + + def check_if_resource_has_admin(self, resource_uid): + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + return False + for user_vertex in resource_vertex.has_vertices(EdgeType.ACL): + acl_edge = user_vertex.get_edge(resource_vertex, EdgeType.ACL) + if acl_edge: + content = acl_edge.content_as_dict + if content.get('is_admin'): + return user_vertex.uid + return False + + def check_if_resource_allowed(self, resource_uid, setting): + resource_vertex = self.linking_dag.get_vertex(resource_uid) + content = get_vertex_content(resource_vertex) + return content.get('allowedSettings', {}).get(setting, False) if content else False + + def get_resource_setting(self, resource_uid: str, settings_name: str, setting: str) -> str: + # Settings are tri-state (on|off|default) mapped to true|false|missing in JSON + # When set to "default" (missing from JSON) that means look higher up the hierarchy + # ex. rotation: user -> machine -> pam_config -> Gobal Default settings + # Note: Different clients (even different client versions) + # may have different view on these defaults (Commander, Web Vault, etc.) + resource_vertex = self.linking_dag.get_vertex(resource_uid) + content = get_vertex_content(resource_vertex) + res = '' + if content and isinstance(content, dict): + if settings_name in content and isinstance(content[settings_name], dict): + if setting in content[settings_name]: + value = content[settings_name][setting] + if isinstance(value, bool): + res = {True: 'on', False: 'off'}[value] + else: + res = str(value) + else: + res = 'default' + + return res + + def set_resource_allowed(self, resource_uid, tunneling=None, connections=None, rotation=None, + session_recording=None, typescript_recording=None, remote_browser_isolation=None, + allowed_settings_name='allowedSettings', is_config=False, + v_type: RefType=str(RefType.PAM_MACHINE)): + v_type = RefType(v_type) + allowed_ref_types = [RefType.PAM_MACHINE, RefType.PAM_DATABASE, RefType.PAM_DIRECTORY, RefType.PAM_BROWSER] + if v_type not in allowed_ref_types: + # default to machine + v_type = RefType.PAM_MACHINE + + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + resource_vertex = self.linking_dag.add_vertex(uid=resource_uid, vertex_type=v_type) + + if resource_vertex.vertex_type not in allowed_ref_types: + resource_vertex.vertex_type = v_type + if is_config: + resource_vertex.vertex_type = RefType.PAM_NETWORK + dirty = False + content = get_vertex_content(resource_vertex) + if content is None: + content = {allowed_settings_name: {}} + dirty = True + if allowed_settings_name not in content: + content[allowed_settings_name] = {} + dirty = True + + settings = content[allowed_settings_name] + + # When no value in allowedSettings: client will substitute with default + # rotation defaults to True, everything else defaults to False + + # switching to 3-state on/off/default: on/true, off/false, + # None = Keep existing, 'default' = Reset to default (remove from dict) + if connections is not None: + connections = self._convert_allowed_setting(connections) + if connections != settings.get("connections", None): + dirty = True + if connections is None: + settings.pop("connections", None) + else: + settings["connections"] = connections + + if tunneling is not None: + tunneling = self._convert_allowed_setting(tunneling) + if tunneling != settings.get("portForwards", None): + dirty = True + if tunneling is None: + settings.pop("portForwards", None) + else: + settings["portForwards"] = tunneling + + if rotation is not None: + rotation = self._convert_allowed_setting(rotation) + if rotation != settings.get("rotation", None): + dirty = True + if rotation is None: + settings.pop("rotation", None) + else: + settings["rotation"] = rotation + + if session_recording is not None: + session_recording = self._convert_allowed_setting(session_recording) + if session_recording != settings.get("sessionRecording", None): + dirty = True + if session_recording is None: + settings.pop("sessionRecording", None) + else: + settings["sessionRecording"] = session_recording + + if typescript_recording is not None: + typescript_recording = self._convert_allowed_setting(typescript_recording) + if typescript_recording != settings.get("typescriptRecording", None): + dirty = True + if typescript_recording is None: + settings.pop("typescriptRecording", None) + else: + settings["typescriptRecording"] = typescript_recording + + if remote_browser_isolation is not None: + remote_browser_isolation = self._convert_allowed_setting(remote_browser_isolation) + if remote_browser_isolation != settings.get("remoteBrowserIsolation", None): + dirty = True + if remote_browser_isolation is None: + settings.pop("remoteBrowserIsolation", None) + else: + settings["remoteBrowserIsolation"] = remote_browser_isolation + + if dirty: + resource_vertex.add_data(content=content, path='meta', needs_encryption=False) + self.linking_dag.save() + + def clear_launch_credential_for_resource(self, resource_uid, exclude_user_uid=None): + """Remove is_launch_credential from all users on a resource except exclude_user_uid.""" + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + return + dirty = False + for user_vertex in resource_vertex.has_vertices(EdgeType.ACL): + if exclude_user_uid and user_vertex.uid == exclude_user_uid: + continue + acl_edge = user_vertex.get_edge(resource_vertex, EdgeType.ACL) + if not acl_edge: + continue + edge_content = acl_edge.content_as_dict + if edge_content and edge_content.get('is_launch_credential'): + edge_content = dict(edge_content) + edge_content.pop('is_launch_credential') + user_vertex.belongs_to(resource_vertex, EdgeType.ACL, content=edge_content) + dirty = True + if dirty: + self.linking_dag.save() + + def upgrade_resource_meta_to_v1(self, resource_uid): + """Ensure resource vertex meta has version >= 1 so vault reads ACL launch credentials.""" + resource_vertex = self.linking_dag.get_vertex(resource_uid) + if resource_vertex is None: + return + content = get_vertex_content(resource_vertex) + if content and content.get('version', 0) >= RESOURCE_META_VERSION_V1: + return + upgraded = ensure_resource_meta_v1(content) + resource_vertex.add_data(content=upgraded, path='meta', needs_encryption=False) + self.linking_dag.save() + + def is_tunneling_config_set_up(self, resource_uid): + if not self.linking_dag.has_graph: + return False + resource_vertex = self.linking_dag.get_vertex(resource_uid) + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + return resource_vertex and config_vertex and config_vertex in resource_vertex.belongs_to_vertices() + + def remove_from_dag(self, uid): + if not self.linking_dag.has_graph: + return True + + vertex = self.linking_dag.get_vertex(uid) + if vertex is None: + return True + + vertex.delete() + self.linking_dag.save(confirm=True) + return None + + def print_tunneling_config(self, record_uid, pam_settings=None, config_uid=None): + if not pam_settings and not config_uid: + return + self.linking_dag.load() + vertex = self.linking_dag.get_vertex(record_uid) + content = get_vertex_content(vertex) + config_id = config_uid if config_uid else pam_settings.value[0].get('configUid') if pam_settings else None + if content and content.get('allowedSettings'): + allowed_settings = content['allowedSettings'] + logger.info(f"Settings configured for {record_uid}") + port_forwarding = f"Enabled" if allowed_settings.get('portForwards') else \ + "Disabled" + rotation = "Disabled" if (allowed_settings.get('rotation') and not allowed_settings['rotation']) else "Enabled" + logger.info(f"\tRotation: {rotation}") + logger.info(f"\tTunneling: {port_forwarding}") + + logger.info(f"Configuration: {config_id}") + if config_id is not None: + config_vertex = self.linking_dag.get_vertex(self.record.record_uid) + config_content = get_vertex_content(config_vertex) + if config_content and config_content.get('allowedSettings'): + config_allowed_settings = config_content['allowedSettings'] + config_port_forwarding = "Enabled" if ( + config_allowed_settings.get('portForwards')) else \ + "Disabled" + config_rotation = "Disabled" if (config_allowed_settings.get('rotation') and + not config_allowed_settings['rotation']) else \ + "Enabled" + logger.info(f"\tRotation: {config_rotation}") + logger.info(f"\tTunneling: {config_port_forwarding}") diff --git a/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_utils.py b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_utils.py new file mode 100644 index 00000000..a7aa7429 --- /dev/null +++ b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_utils.py @@ -0,0 +1,83 @@ +import json +import os +import logging +import requests + +from typing import Any, Dict, List, Optional + +from ... import errors, utils, crypto +from ...vault import vault_online +from ..keeper_dag.dag_crypto import generate_random_bytes +from ...authentication import endpoint + + +logger = logging.getLogger(__name__) + + +VERIFY_SSL = bool(os.environ.get("VERIFY_SSL", "TRUE") == "TRUE") + + +def get_config_uid(vault: vault_online.VaultOnline, encrypted_session_token: bytes, encrypted_transmission_key: bytes, record_uid: str) -> Optional[str]: + try: + rs = get_dag_leafs(vault, encrypted_session_token, encrypted_transmission_key, record_uid) + if not rs: + return None + else: + return rs[0].get('value', '') + except Exception as e: + logger.error(f"Error getting configuration: {e}") + return None + + +def get_dag_leafs(vault: vault_online.VaultOnline, encrypted_session_token: bytes, encrypted_transmission_key: bytes, record_id: str) -> Optional[List[Dict[str, Any]]]: + """ + POST a stringified JSON object to /api/dag/get_leafs on the KRouter + The object is: + { + vertex: string, + graphId: number + } + """ + krouter_host = f"https://{vault.keeper_auth.keeper_endpoint.get_router_server()}" + path = '/api/user/get_leafs' + + payload = { + 'vertex': record_id, + 'graphId': 0 + } + + try: + rs = requests.request('post', + krouter_host + path, + verify=VERIFY_SSL, + headers={ + 'TransmissionKey': utils.base64_url_encode(encrypted_transmission_key), + 'Authorization': f'KeeperUser {utils.base64_url_encode(encrypted_session_token)}' + }, + data=json.dumps(payload).encode('utf-8') + ) + except ConnectionError as e: + raise errors.KeeperApiError("-1", f"KRouter is not reachable on '{krouter_host}'. Error: ${e}") + except Exception as ex: + raise ex + + if rs.status_code == 200: + logger.debug("Found right host") + return rs.json() + else: + logger.warning("Looks like there is no such controller connected to the router.") + return None + + +def get_keeper_tokens(vault: vault_online.VaultOnline): + transmission_key = generate_random_bytes(32) + server_public_key = endpoint.SERVER_PUBLIC_KEYS[vault.keeper_auth.keeper_endpoint.server_key_id] + + if vault.keeper_auth.keeper_endpoint.server_key_id < 7: + encrypted_transmission_key = crypto.encrypt_rsa(transmission_key, server_public_key) + else: + encrypted_transmission_key = crypto.encrypt_ec(transmission_key, server_public_key) + encrypted_session_token = crypto.encrypt_aes_v2( + vault.keeper_auth.auth_context.session_token, transmission_key) + + return encrypted_session_token, encrypted_transmission_key, transmission_key diff --git a/keepersdk-package/src/keepersdk/plugins/pam/__init__.py b/keepersdk-package/src/keepersdk/plugins/pam/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keepersdk-package/src/keepersdk/plugins/pam/pam_plugin.py b/keepersdk-package/src/keepersdk/plugins/pam/pam_plugin.py new file mode 100644 index 00000000..d3b3ff4e --- /dev/null +++ b/keepersdk-package/src/keepersdk/plugins/pam/pam_plugin.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import abc +from typing import Dict, List, Tuple + +from . import pam_storage, pam_types +from ... import utils +from ...authentication.keeper_auth import KeeperAuth +from ...enterprise import enterprise_loader, sqlite_enterprise_storage +from ...proto import SyncDown_pb2, pam_pb2 +from ...storage import in_memory, storage_types + + +def _pam_rows_from_proto(c: pam_pb2.PAMController) -> Tuple[pam_storage.PamStorageController, pam_types.PamController]: + kwargs = dict( + controller_uid=utils.base64_url_encode(c.controllerUid) if c.controllerUid else '', + controller_name=c.controllerName or '', + device_token=c.deviceToken or '', + device_name=c.deviceName or '', + node_id=c.nodeId, + created=c.created, + last_modified=c.lastModified, + application_uid=utils.base64_url_encode(c.applicationUid) if c.applicationUid else '', + app_client_type=c.appClientType, + is_initialized=c.isInitialized, + ) + return pam_storage.PamStorageController(**kwargs), pam_types.PamController(**kwargs) + + +def _pam_rotation_to_domain(row: pam_storage.PamRecordRotation) -> pam_types.PamRecordRotationInfo: + return pam_types.PamRecordRotationInfo( + record_uid=row.record_uid, + revision=row.revision, + configuration_uid=row.configuration_uid, + schedule=row.schedule, + pwd_complexity=row.pwd_complexity, + disabled=row.disabled, + resource_uid=row.resource_uid, + last_rotation=row.last_rotation, + last_rotation_status=row.last_rotation_status, + ) + + +class IPamPlugin(abc.ABC): + @abc.abstractmethod + def sync_down(self, *, reload: bool = False) -> None: + pass + + @abc.abstractmethod + def sync_record_rotations_from_vault(self) -> None: + pass + + @property + @abc.abstractmethod + def controllers(self) -> storage_types.IEntityReader[pam_types.PamController, str]: + pass + + @property + @abc.abstractmethod + def record_rotations(self) -> storage_types.IEntityReader[pam_types.PamRecordRotationInfo, str]: + pass + + +class PamPlugin(IPamPlugin): + def __init__(self, loader: enterprise_loader.EnterpriseLoader): + assert loader.keeper_auth.auth_context.enterprise_id + assert loader.keeper_auth.auth_context.is_enterprise_admin + self._enterprise_id = loader.keeper_auth.auth_context.enterprise_id + self.enterprise_uid: str = utils.base64_url_encode(self._enterprise_id.to_bytes(16, byteorder='big')) + loader_storage = loader.storage + self.storage: pam_storage.IPamStorage + if isinstance(loader_storage, sqlite_enterprise_storage.SqliteEnterpriseStorage): + self.storage = pam_storage.SqlitePamStorage(loader_storage.get_connection, self._enterprise_id) + else: + self.storage = pam_storage.MemoryPamStorage() + self.loader = loader + self._controllers = in_memory.InMemoryEntityStorage[pam_types.PamController, str]() + self._record_rotations = in_memory.InMemoryEntityStorage[pam_types.PamRecordRotationInfo, str]() + self.logger = utils.get_logger() + + @property + def controllers(self) -> storage_types.IEntityReader[pam_types.PamController, str]: + return self._controllers + + @property + def record_rotations(self) -> storage_types.IEntityReader[pam_types.PamRecordRotationInfo, str]: + return self._record_rotations + + def _get_all_gateways(self, auth: KeeperAuth) -> List[pam_pb2.PAMController]: + rs = auth.execute_auth_rest( + 'pam/get_controllers', + None, + response_type=pam_pb2.PAMControllersResponse, + ) + if rs: + return list(rs.controllers) + return [] + + def sync_record_rotations_from_vault(self) -> None: + + self._sync_record_rotations_from_vault_auth(self.loader.keeper_auth) + + def _sync_record_rotations_from_vault_auth(self, auth: KeeperAuth) -> None: + + merged: Dict[str, pam_storage.PamRecordRotation] = {} + rq = SyncDown_pb2.SyncDownRequest() + token = b'' + done = False + while not done: + rq.continuationToken = token + response = auth.execute_auth_rest( + 'vault/sync_down', rq, response_type=SyncDown_pb2.SyncDownResponse) + if response is None: + break + done = not response.hasMore + token = response.continuationToken or b'' + for rr in response.recordRotations: + row = pam_storage.pam_record_rotation_from_proto(rr) + if row.record_uid: + merged[row.record_uid] = row + if not merged: + return + rows = list(merged.values()) + self.storage.record_rotations.put_entities(rows) + self._record_rotations.put_entities(_pam_rotation_to_domain(r) for r in rows) + + def sync_down(self, *, reload: bool = False) -> None: + _ = reload + self.storage.reset() + self._controllers.clear() + self._record_rotations.clear() + + auth = self.loader.keeper_auth + all_controllers = self._get_all_gateways(auth) + storage_rows = [] + domain_rows = [] + for c in all_controllers: + s_row, d_row = _pam_rows_from_proto(c) + storage_rows.append(s_row) + domain_rows.append(d_row) + if storage_rows: + self.storage.controllers.put_entities(storage_rows) + if domain_rows: + self._controllers.put_entities(domain_rows) + + try: + self._sync_record_rotations_from_vault_auth(auth) + except Exception as e: + self.logger.warning('PAM: loading record rotations from vault/sync_down failed: %s', e) diff --git a/keepersdk-package/src/keepersdk/plugins/pam/pam_storage.py b/keepersdk-package/src/keepersdk/plugins/pam/pam_storage.py new file mode 100644 index 00000000..3ffdb675 --- /dev/null +++ b/keepersdk-package/src/keepersdk/plugins/pam/pam_storage.py @@ -0,0 +1,127 @@ +import abc +import sqlite3 +from typing import Callable + +import attrs + +from ... import sqlite_dao +from ...proto import enterprise_pb2 +from ...storage import in_memory, sqlite, storage_types +from ... import utils +from ...proto import SyncDown_pb2 + + +@attrs.define(kw_only=True) +class PamStorageController(storage_types.IUid[str]): + controller_uid: str = '' + controller_name: str = '' + device_token: str = '' + device_name: str = '' + node_id: int = 0 + created: int = 0 + last_modified: int = 0 + application_uid: str = '' + app_client_type: enterprise_pb2.AppClientType = enterprise_pb2.NOT_USED + is_initialized: bool = False + + def uid(self) -> str: + return self.controller_uid + +@attrs.define(kw_only=True) +class PamRecordRotation(storage_types.IUid[str]): + """Mirrors ``Vault.RecordRotation``; UIDs are URL-safe base64 strings.""" + + record_uid: str = '' + revision: int = 0 + configuration_uid: str = '' + schedule: str = '' + pwd_complexity: bytes = b'' + disabled: bool = False + resource_uid: str = '' + last_rotation: int = 0 + last_rotation_status: int = 0 + + def uid(self) -> str: + return self.record_uid + + +def pam_record_rotation_from_proto(rr: SyncDown_pb2.RecordRotation) -> PamRecordRotation: + return PamRecordRotation( + record_uid=utils.base64_url_encode(rr.recordUid) if rr.recordUid else '', + revision=int(rr.revision), + configuration_uid=utils.base64_url_encode(rr.configurationUid) if rr.configurationUid else '', + schedule=rr.schedule or '', + pwd_complexity=rr.pwdComplexity or b'', + disabled=bool(rr.disabled), + resource_uid=utils.base64_url_encode(rr.resourceUid) if rr.resourceUid else '', + last_rotation=int(rr.lastRotation), + last_rotation_status=int(rr.lastRotationStatus), + ) + + +class IPamStorage(abc.ABC): + @property + @abc.abstractmethod + def controllers(self) -> storage_types.IEntityReaderStorage[PamStorageController, str]: + pass + + @property + @abc.abstractmethod + def record_rotations(self) -> storage_types.IEntityReaderStorage[PamRecordRotation, str]: + pass + + @abc.abstractmethod + def reset(self): + pass + + +class MemoryPamStorage(IPamStorage): + def __init__(self): + self._controllers = in_memory.InMemoryEntityStorage[PamStorageController, str]() + self._record_rotations = in_memory.InMemoryEntityStorage[PamRecordRotation, str]() + + @property + def controllers(self) -> storage_types.IEntityReaderStorage[PamStorageController, str]: + return self._controllers + + @property + def record_rotations(self) -> storage_types.IEntityReaderStorage[PamRecordRotation, str]: + return self._record_rotations + + def reset(self): + self._controllers.clear() + self._record_rotations.clear() + + +class SqlitePamStorage(IPamStorage): + def __init__(self, get_connection: Callable[[], sqlite3.Connection], enterprise_id: int) -> None: + self.get_connection = get_connection + self.enterprise_id = enterprise_id + self.owner_column = 'enterprise_id' + controller_schema = sqlite_dao.TableSchema.load_schema( + PamStorageController, 'controller_uid', owner_column=self.owner_column, owner_type=int) + rotation_schema = sqlite_dao.TableSchema.load_schema( + PamRecordRotation, + 'record_uid', + owner_column=self.owner_column, + owner_type=int, + indexes={'configuration_uid': ['configuration_uid']}, + ) + sqlite_dao.verify_database(self.get_connection(), (controller_schema, rotation_schema)) + self._controllers = sqlite.SqliteEntityStorage( + self.get_connection, controller_schema, owner=enterprise_id) + self._record_rotations = sqlite.SqliteEntityStorage( + self.get_connection, rotation_schema, owner=enterprise_id) + + @property + def controllers(self) -> storage_types.IEntityReaderStorage[PamStorageController, str]: + return self._controllers + + @property + def record_rotations(self) -> storage_types.IEntityReaderStorage[PamRecordRotation, str]: + return self._record_rotations + + def reset(self): + self._controllers.delete_all() + self._record_rotations.delete_all() + diff --git a/keepersdk-package/src/keepersdk/plugins/pam/pam_types.py b/keepersdk-package/src/keepersdk/plugins/pam/pam_types.py new file mode 100644 index 00000000..48f6b399 --- /dev/null +++ b/keepersdk-package/src/keepersdk/plugins/pam/pam_types.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import attrs + +from ...proto import enterprise_pb2 +from ...storage import storage_types + + +@attrs.define(kw_only=True, frozen=True) +class PamController(storage_types.IUid[str]): + """PAM Controller information.""" + controller_uid: str + controller_name: str + device_token: str + device_name: str + node_id: int + created: int + last_modified: int + application_uid: str + app_client_type: enterprise_pb2.AppClientType + is_initialized: bool + + def uid(self) -> str: + return self.controller_uid + + +@attrs.define(kw_only=True, frozen=True) +class PamRecordRotationInfo(storage_types.IUid[str]): + """Vault record rotation row exposed by :class:`~keepersdk.plugins.pam.pam_plugin.PamPlugin`.""" + + record_uid: str + revision: int + configuration_uid: str + schedule: str + pwd_complexity: bytes + disabled: bool + resource_uid: str + last_rotation: int + last_rotation_status: int + + def uid(self) -> str: + return self.record_uid diff --git a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py index 54b85407..39de1cd0 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: APIRequest.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'APIRequest.proto' ) @@ -25,7 +25,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\"{\n\rQrcMessageKey\x12\x19\n\x11\x63lientEcPublicKey\x18\x01 \x01(\x0c\x12\x1c\n\x14mlKemEncapsulatedKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x12\n\nmsgVersion\x18\x04 \x01(\x05\x12\x0f\n\x07\x65\x63KeyId\x18\x05 \x01(\x05\"\xe6\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\x12\x34\n\rqrcMessageKey\x18\x08 \x01(\x0b\x32\x1d.Authentication.QrcMessageKey\"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\"v\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\x12\x15\n\rprofilePicUrl\x18\x05 \x01(\t\"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\"\xc5\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\x12\x10\n\x08username\x18\x06 \x01(\t\"\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') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x41PIRequest.proto\x12\x0e\x41uthentication\x1a\x10\x65nterprise.proto\"{\n\rQrcMessageKey\x12\x19\n\x11\x63lientEcPublicKey\x18\x01 \x01(\x0c\x12\x1c\n\x14mlKemEncapsulatedKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x12\n\nmsgVersion\x18\x04 \x01(\x05\x12\x0f\n\x07\x65\x63KeyId\x18\x05 \x01(\x05\"\xe6\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\x12\x34\n\rqrcMessageKey\x18\x08 \x01(\x0b\x32\x1d.Authentication.QrcMessageKey\"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\"v\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\x12\x15\n\rprofilePicUrl\x18\x05 \x01(\t\"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\"\xb7\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\x12\x16\n\x0eparentThreadId\x18\x07 \x01(\t\"\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\"\xc5\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\x12\x10\n\x08username\x18\x06 \x01(\t\"\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*r\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\x12\x19\n\x15LICENSE_SEAT_EXCEEDED\x10\x04*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,66 +33,66 @@ 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=21515 - _globals['_SUPPORTEDLANGUAGE']._serialized_end=21854 - _globals['_LOGINTYPE']._serialized_start=21856 - _globals['_LOGINTYPE']._serialized_end=21963 - _globals['_DEVICESTATUS']._serialized_start=21965 - _globals['_DEVICESTATUS']._serialized_end=22078 - _globals['_LICENSESTATUS']._serialized_start=22080 - _globals['_LICENSESTATUS']._serialized_end=22145 - _globals['_ACCOUNTTYPE']._serialized_start=22147 - _globals['_ACCOUNTTYPE']._serialized_end=22202 - _globals['_SESSIONTOKENTYPE']._serialized_start=22205 - _globals['_SESSIONTOKENTYPE']._serialized_end=22492 - _globals['_VERSION']._serialized_start=22494 - _globals['_VERSION']._serialized_end=22565 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=22567 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=22622 - _globals['_LOGINMETHOD']._serialized_start=22624 - _globals['_LOGINMETHOD']._serialized_end=22732 - _globals['_LOGINSTATE']._serialized_start=22735 - _globals['_LOGINSTATE']._serialized_end=23309 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=23311 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=23418 - _globals['_PASSWORDMETHOD']._serialized_start=23420 - _globals['_PASSWORDMETHOD']._serialized_end=23465 - _globals['_TWOFACTORPUSHTYPE']._serialized_start=23468 - _globals['_TWOFACTORPUSHTYPE']._serialized_end=23653 - _globals['_TWOFACTORVALUETYPE']._serialized_start=23656 - _globals['_TWOFACTORVALUETYPE']._serialized_end=23851 - _globals['_TWOFACTORCHANNELTYPE']._serialized_start=23854 - _globals['_TWOFACTORCHANNELTYPE']._serialized_end=24079 - _globals['_TWOFACTOREXPIRATION']._serialized_start=24082 - _globals['_TWOFACTOREXPIRATION']._serialized_end=24253 - _globals['_LICENSETYPE']._serialized_start=24255 - _globals['_LICENSETYPE']._serialized_end=24319 - _globals['_OBJECTTYPES']._serialized_start=24321 - _globals['_OBJECTTYPES']._serialized_end=24426 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=24429 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=24718 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=24720 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=24797 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=24799 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=24895 - _globals['_THROTTLETYPE']._serialized_start=24898 - _globals['_THROTTLETYPE']._serialized_end=25180 - _globals['_REGION']._serialized_start=25182 - _globals['_REGION']._serialized_end=25254 - _globals['_APPLICATIONSHARETYPE']._serialized_start=25256 - _globals['_APPLICATIONSHARETYPE']._serialized_end=25324 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=25327 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=25491 - _globals['_BACKUPKEYTYPE']._serialized_start=25493 - _globals['_BACKUPKEYTYPE']._serialized_end=25553 - _globals['_GENERICSTATUS']._serialized_start=25555 - _globals['_GENERICSTATUS']._serialized_end=25642 - _globals['_AUTHENTICATORATTACHMENT']._serialized_start=25644 - _globals['_AUTHENTICATORATTACHMENT']._serialized_end=25722 - _globals['_PASSKEYPURPOSE']._serialized_start=25724 - _globals['_PASSKEYPURPOSE']._serialized_end=25769 - _globals['_CLIENTFORMFACTOR']._serialized_start=25771 - _globals['_CLIENTFORMFACTOR']._serialized_end=25846 + _globals['_SUPPORTEDLANGUAGE']._serialized_start=21539 + _globals['_SUPPORTEDLANGUAGE']._serialized_end=21878 + _globals['_LOGINTYPE']._serialized_start=21880 + _globals['_LOGINTYPE']._serialized_end=21987 + _globals['_DEVICESTATUS']._serialized_start=21989 + _globals['_DEVICESTATUS']._serialized_end=22102 + _globals['_LICENSESTATUS']._serialized_start=22104 + _globals['_LICENSESTATUS']._serialized_end=22169 + _globals['_ACCOUNTTYPE']._serialized_start=22171 + _globals['_ACCOUNTTYPE']._serialized_end=22226 + _globals['_SESSIONTOKENTYPE']._serialized_start=22229 + _globals['_SESSIONTOKENTYPE']._serialized_end=22516 + _globals['_VERSION']._serialized_start=22518 + _globals['_VERSION']._serialized_end=22589 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=22591 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=22646 + _globals['_LOGINMETHOD']._serialized_start=22648 + _globals['_LOGINMETHOD']._serialized_end=22756 + _globals['_LOGINSTATE']._serialized_start=22759 + _globals['_LOGINSTATE']._serialized_end=23333 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=23335 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=23442 + _globals['_PASSWORDMETHOD']._serialized_start=23444 + _globals['_PASSWORDMETHOD']._serialized_end=23489 + _globals['_TWOFACTORPUSHTYPE']._serialized_start=23492 + _globals['_TWOFACTORPUSHTYPE']._serialized_end=23677 + _globals['_TWOFACTORVALUETYPE']._serialized_start=23680 + _globals['_TWOFACTORVALUETYPE']._serialized_end=23875 + _globals['_TWOFACTORCHANNELTYPE']._serialized_start=23878 + _globals['_TWOFACTORCHANNELTYPE']._serialized_end=24103 + _globals['_TWOFACTOREXPIRATION']._serialized_start=24106 + _globals['_TWOFACTOREXPIRATION']._serialized_end=24277 + _globals['_LICENSETYPE']._serialized_start=24279 + _globals['_LICENSETYPE']._serialized_end=24343 + _globals['_OBJECTTYPES']._serialized_start=24345 + _globals['_OBJECTTYPES']._serialized_end=24450 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=24453 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=24742 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=24744 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=24821 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=24823 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=24919 + _globals['_THROTTLETYPE']._serialized_start=24922 + _globals['_THROTTLETYPE']._serialized_end=25204 + _globals['_REGION']._serialized_start=25206 + _globals['_REGION']._serialized_end=25278 + _globals['_APPLICATIONSHARETYPE']._serialized_start=25280 + _globals['_APPLICATIONSHARETYPE']._serialized_end=25348 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=25351 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=25515 + _globals['_BACKUPKEYTYPE']._serialized_start=25517 + _globals['_BACKUPKEYTYPE']._serialized_end=25577 + _globals['_GENERICSTATUS']._serialized_start=25579 + _globals['_GENERICSTATUS']._serialized_end=25693 + _globals['_AUTHENTICATORATTACHMENT']._serialized_start=25695 + _globals['_AUTHENTICATORATTACHMENT']._serialized_end=25773 + _globals['_PASSKEYPURPOSE']._serialized_start=25775 + _globals['_PASSKEYPURPOSE']._serialized_end=25820 + _globals['_CLIENTFORMFACTOR']._serialized_start=25822 + _globals['_CLIENTFORMFACTOR']._serialized_end=25897 _globals['_QRCMESSAGEKEY']._serialized_start=54 _globals['_QRCMESSAGEKEY']._serialized_end=177 _globals['_APIREQUEST']._serialized_start=180 @@ -238,199 +238,199 @@ _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_start=10588 _globals['_NODEENFORCEMENTREMOVEREQUEST']._serialized_end=10655 _globals['_APIREQUESTBYKEY']._serialized_start=10658 - _globals['_APIREQUESTBYKEY']._serialized_end=10817 - _globals['_APIREQUESTBYKATOKAKEY']._serialized_start=10820 - _globals['_APIREQUESTBYKATOKAKEY']._serialized_end=11019 - _globals['_MEMCACHEREQUEST']._serialized_start=11021 - _globals['_MEMCACHEREQUEST']._serialized_end=11067 - _globals['_MEMCACHERESPONSE']._serialized_start=11069 - _globals['_MEMCACHERESPONSE']._serialized_end=11115 - _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_start=11117 - _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_end=11236 - _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_start=11238 - _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_end=11330 - _globals['_DEVICEREGISTRATIONREQUEST']._serialized_start=11333 - _globals['_DEVICEREGISTRATIONREQUEST']._serialized_end=11530 - _globals['_DEVICEVERIFICATIONREQUEST']._serialized_start=11533 - _globals['_DEVICEVERIFICATIONREQUEST']._serialized_end=11687 - _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_start=11690 - _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_end=11868 - _globals['_DEVICEAPPROVALREQUEST']._serialized_start=11871 - _globals['_DEVICEAPPROVALREQUEST']._serialized_end=12071 - _globals['_DEVICEAPPROVALRESPONSE']._serialized_start=12073 - _globals['_DEVICEAPPROVALRESPONSE']._serialized_end=12130 - _globals['_APPROVEDEVICEREQUEST']._serialized_start=12132 - _globals['_APPROVEDEVICEREQUEST']._serialized_end=12258 - _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_start=12260 - _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_end=12329 - _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_start=12331 - _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_end=12420 - _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_start=12422 - _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_end=12541 - _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_start=12543 - _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_end=12615 - _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_start=12617 - _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_end=12711 - _globals['_DEVICE']._serialized_start=12713 - _globals['_DEVICE']._serialized_end=12751 - _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_start=12753 - _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_end=12845 - _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_start=12847 - _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_end=12957 - _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_start=12960 - _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_end=13123 - _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_start=13125 - _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_end=13214 - _globals['_GLOBALUSERACCOUNT']._serialized_start=13216 - _globals['_GLOBALUSERACCOUNT']._serialized_end=13293 - _globals['_ACCOUNTUSERNAME']._serialized_start=13295 - _globals['_ACCOUNTUSERNAME']._serialized_end=13350 - _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_start=13352 - _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_end=13432 - _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_start=13434 - _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_end=13531 - _globals['_USERSETTINGREQUEST']._serialized_start=13533 - _globals['_USERSETTINGREQUEST']._serialized_end=13585 - _globals['_THROTTLESTATE']._serialized_start=13587 - _globals['_THROTTLESTATE']._serialized_end=13689 - _globals['_THROTTLESTATE2']._serialized_start=13692 - _globals['_THROTTLESTATE2']._serialized_end=13873 - _globals['_DEVICEINFORMATION']._serialized_start=13876 - _globals['_DEVICEINFORMATION']._serialized_end=14027 - _globals['_USERSETTING']._serialized_start=14029 - _globals['_USERSETTING']._serialized_end=14071 - _globals['_USERDATAKEYREQUEST']._serialized_start=14073 - _globals['_USERDATAKEYREQUEST']._serialized_end=14119 - _globals['_USERDATAKEYBYNODEREQUEST']._serialized_start=14121 - _globals['_USERDATAKEYBYNODEREQUEST']._serialized_end=14164 - _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_start=14167 - _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_end=14295 - _globals['_USERDATAKEY']._serialized_start=14298 - _globals['_USERDATAKEY']._serialized_end=14447 - _globals['_USERDATAKEYRESPONSE']._serialized_start=14449 - _globals['_USERDATAKEYRESPONSE']._serialized_end=14571 - _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_start=14573 - _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_end=14645 - _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_start=14647 - _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_end=14732 - _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_start=14734 - _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_end=14848 - _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_start=14850 - _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_end=14960 - _globals['_PASSWORDRULES']._serialized_start=14962 - _globals['_PASSWORDRULES']._serialized_end=15080 - _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_start=15083 - _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_end=15412 - _globals['_GETPUBLICKEYSREQUEST']._serialized_start=15414 - _globals['_GETPUBLICKEYSREQUEST']._serialized_end=15455 - _globals['_PUBLICKEYRESPONSE']._serialized_start=15457 - _globals['_PUBLICKEYRESPONSE']._serialized_end=15571 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=15573 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=15653 - _globals['_SETECCKEYPAIRREQUEST']._serialized_start=15655 - _globals['_SETECCKEYPAIRREQUEST']._serialized_end=15725 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=15727 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=15800 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=15802 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=15884 - _globals['_TEAMECCKEYPAIR']._serialized_start=15886 - _globals['_TEAMECCKEYPAIR']._serialized_end=15967 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=15969 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=16057 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=16059 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=16127 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=16129 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=16214 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=16216 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=16305 - _globals['_ADDAPPSHARESREQUEST']._serialized_start=16307 - _globals['_ADDAPPSHARESREQUEST']._serialized_end=16395 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=16397 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=16459 - _globals['_APPSHAREADD']._serialized_start=16462 - _globals['_APPSHAREADD']._serialized_end=16597 - _globals['_APPSHARE']._serialized_start=16600 - _globals['_APPSHARE']._serialized_end=16737 - _globals['_ADDAPPCLIENTREQUEST']._serialized_start=16740 - _globals['_ADDAPPCLIENTREQUEST']._serialized_end=16957 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=16959 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=17023 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=17026 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=17196 - _globals['_APPCLIENT']._serialized_start=17199 - _globals['_APPCLIENT']._serialized_end=17474 - _globals['_GETAPPINFOREQUEST']._serialized_start=17476 - _globals['_GETAPPINFOREQUEST']._serialized_end=17517 - _globals['_APPINFO']._serialized_start=17520 - _globals['_APPINFO']._serialized_end=17662 - _globals['_GETAPPINFORESPONSE']._serialized_start=17664 - _globals['_GETAPPINFORESPONSE']._serialized_end=17726 - _globals['_APPLICATIONSUMMARY']._serialized_start=17729 - _globals['_APPLICATIONSUMMARY']._serialized_end=17942 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=17944 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=18040 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=18042 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=18089 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=18091 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=18157 - _globals['_SENDSHAREINVITEREQUEST']._serialized_start=18159 - _globals['_SENDSHAREINVITEREQUEST']._serialized_end=18198 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=18201 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=18398 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=18400 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=18455 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=18458 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=18706 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=18708 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=18751 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=18753 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=18856 - _globals['_DOWNLOAD']._serialized_start=18858 - _globals['_DOWNLOAD']._serialized_end=18926 - _globals['_DELETEUSERREQUEST']._serialized_start=18928 - _globals['_DELETEUSERREQUEST']._serialized_end=18963 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=18966 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=19098 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=19100 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=19161 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=19163 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=19252 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=19255 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=19427 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=19429 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=19473 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=19476 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=19657 - _globals['_USERTEAMKEY']._serialized_start=19660 - _globals['_USERTEAMKEY']._serialized_end=19838 - _globals['_GENERICREQUESTRESPONSE']._serialized_start=19840 - _globals['_GENERICREQUESTRESPONSE']._serialized_end=19881 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=19883 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=19985 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=19987 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=20067 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=20070 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=20202 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=20205 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=20512 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=20515 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=20654 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=20657 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=20848 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=20850 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=20923 - _globals['_UPDATEPASSKEYREQUEST']._serialized_start=20925 - _globals['_UPDATEPASSKEYREQUEST']._serialized_end=21029 - _globals['_PASSKEYLISTREQUEST']._serialized_start=21031 - _globals['_PASSKEYLISTREQUEST']._serialized_end=21076 - _globals['_PASSKEYINFO']._serialized_start=21079 - _globals['_PASSKEYINFO']._serialized_end=21243 - _globals['_PASSKEYLISTRESPONSE']._serialized_start=21245 - _globals['_PASSKEYLISTRESPONSE']._serialized_end=21316 - _globals['_TRANSLATIONINFO']._serialized_start=21318 - _globals['_TRANSLATIONINFO']._serialized_end=21385 - _globals['_TRANSLATIONREQUEST']._serialized_start=21387 - _globals['_TRANSLATIONREQUEST']._serialized_end=21431 - _globals['_TRANSLATIONRESPONSE']._serialized_start=21433 - _globals['_TRANSLATIONRESPONSE']._serialized_end=21512 + _globals['_APIREQUESTBYKEY']._serialized_end=10841 + _globals['_APIREQUESTBYKATOKAKEY']._serialized_start=10844 + _globals['_APIREQUESTBYKATOKAKEY']._serialized_end=11043 + _globals['_MEMCACHEREQUEST']._serialized_start=11045 + _globals['_MEMCACHEREQUEST']._serialized_end=11091 + _globals['_MEMCACHERESPONSE']._serialized_start=11093 + _globals['_MEMCACHERESPONSE']._serialized_end=11139 + _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_start=11141 + _globals['_MASTERPASSWORDREENTRYREQUEST']._serialized_end=11260 + _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_start=11262 + _globals['_MASTERPASSWORDREENTRYRESPONSE']._serialized_end=11354 + _globals['_DEVICEREGISTRATIONREQUEST']._serialized_start=11357 + _globals['_DEVICEREGISTRATIONREQUEST']._serialized_end=11554 + _globals['_DEVICEVERIFICATIONREQUEST']._serialized_start=11557 + _globals['_DEVICEVERIFICATIONREQUEST']._serialized_end=11711 + _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_start=11714 + _globals['_DEVICEVERIFICATIONRESPONSE']._serialized_end=11892 + _globals['_DEVICEAPPROVALREQUEST']._serialized_start=11895 + _globals['_DEVICEAPPROVALREQUEST']._serialized_end=12095 + _globals['_DEVICEAPPROVALRESPONSE']._serialized_start=12097 + _globals['_DEVICEAPPROVALRESPONSE']._serialized_end=12154 + _globals['_APPROVEDEVICEREQUEST']._serialized_start=12156 + _globals['_APPROVEDEVICEREQUEST']._serialized_end=12282 + _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_start=12284 + _globals['_ENTERPRISEUSERALIASREQUEST']._serialized_end=12353 + _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_start=12355 + _globals['_ENTERPRISEUSERADDALIASREQUEST']._serialized_end=12444 + _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_start=12446 + _globals['_ENTERPRISEUSERADDALIASREQUESTV2']._serialized_end=12565 + _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_start=12567 + _globals['_ENTERPRISEUSERADDALIASSTATUS']._serialized_end=12639 + _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_start=12641 + _globals['_ENTERPRISEUSERADDALIASRESPONSE']._serialized_end=12735 + _globals['_DEVICE']._serialized_start=12737 + _globals['_DEVICE']._serialized_end=12775 + _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_start=12777 + _globals['_REGISTERDEVICEDATAKEYREQUEST']._serialized_end=12869 + _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_start=12871 + _globals['_VALIDATECREATEUSERVERIFICATIONCODEREQUEST']._serialized_end=12981 + _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_start=12984 + _globals['_VALIDATEDEVICEVERIFICATIONCODEREQUEST']._serialized_end=13147 + _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_start=13149 + _globals['_SENDSESSIONMESSAGEREQUEST']._serialized_end=13238 + _globals['_GLOBALUSERACCOUNT']._serialized_start=13240 + _globals['_GLOBALUSERACCOUNT']._serialized_end=13317 + _globals['_ACCOUNTUSERNAME']._serialized_start=13319 + _globals['_ACCOUNTUSERNAME']._serialized_end=13374 + _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_start=13376 + _globals['_SSOSERVICEPROVIDERREQUEST']._serialized_end=13456 + _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_start=13458 + _globals['_SSOSERVICEPROVIDERRESPONSE']._serialized_end=13555 + _globals['_USERSETTINGREQUEST']._serialized_start=13557 + _globals['_USERSETTINGREQUEST']._serialized_end=13609 + _globals['_THROTTLESTATE']._serialized_start=13611 + _globals['_THROTTLESTATE']._serialized_end=13713 + _globals['_THROTTLESTATE2']._serialized_start=13716 + _globals['_THROTTLESTATE2']._serialized_end=13897 + _globals['_DEVICEINFORMATION']._serialized_start=13900 + _globals['_DEVICEINFORMATION']._serialized_end=14051 + _globals['_USERSETTING']._serialized_start=14053 + _globals['_USERSETTING']._serialized_end=14095 + _globals['_USERDATAKEYREQUEST']._serialized_start=14097 + _globals['_USERDATAKEYREQUEST']._serialized_end=14143 + _globals['_USERDATAKEYBYNODEREQUEST']._serialized_start=14145 + _globals['_USERDATAKEYBYNODEREQUEST']._serialized_end=14188 + _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_start=14191 + _globals['_ENTERPRISEUSERIDDATAKEYPAIR']._serialized_end=14319 + _globals['_USERDATAKEY']._serialized_start=14322 + _globals['_USERDATAKEY']._serialized_end=14471 + _globals['_USERDATAKEYRESPONSE']._serialized_start=14473 + _globals['_USERDATAKEYRESPONSE']._serialized_end=14595 + _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_start=14597 + _globals['_MASTERPASSWORDRECOVERYVERIFICATIONREQUEST']._serialized_end=14669 + _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_start=14671 + _globals['_GETSECURITYQUESTIONV3REQUEST']._serialized_end=14756 + _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_start=14758 + _globals['_GETSECURITYQUESTIONV3RESPONSE']._serialized_end=14872 + _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_start=14874 + _globals['_GETDATAKEYBACKUPV3REQUEST']._serialized_end=14984 + _globals['_PASSWORDRULES']._serialized_start=14986 + _globals['_PASSWORDRULES']._serialized_end=15104 + _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_start=15107 + _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_end=15436 + _globals['_GETPUBLICKEYSREQUEST']._serialized_start=15438 + _globals['_GETPUBLICKEYSREQUEST']._serialized_end=15479 + _globals['_PUBLICKEYRESPONSE']._serialized_start=15481 + _globals['_PUBLICKEYRESPONSE']._serialized_end=15595 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=15597 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=15677 + _globals['_SETECCKEYPAIRREQUEST']._serialized_start=15679 + _globals['_SETECCKEYPAIRREQUEST']._serialized_end=15749 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=15751 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=15824 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=15826 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=15908 + _globals['_TEAMECCKEYPAIR']._serialized_start=15910 + _globals['_TEAMECCKEYPAIR']._serialized_end=15991 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=15993 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=16081 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=16083 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=16151 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=16153 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=16238 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=16240 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=16329 + _globals['_ADDAPPSHARESREQUEST']._serialized_start=16331 + _globals['_ADDAPPSHARESREQUEST']._serialized_end=16419 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=16421 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=16483 + _globals['_APPSHAREADD']._serialized_start=16486 + _globals['_APPSHAREADD']._serialized_end=16621 + _globals['_APPSHARE']._serialized_start=16624 + _globals['_APPSHARE']._serialized_end=16761 + _globals['_ADDAPPCLIENTREQUEST']._serialized_start=16764 + _globals['_ADDAPPCLIENTREQUEST']._serialized_end=16981 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=16983 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=17047 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=17050 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=17220 + _globals['_APPCLIENT']._serialized_start=17223 + _globals['_APPCLIENT']._serialized_end=17498 + _globals['_GETAPPINFOREQUEST']._serialized_start=17500 + _globals['_GETAPPINFOREQUEST']._serialized_end=17541 + _globals['_APPINFO']._serialized_start=17544 + _globals['_APPINFO']._serialized_end=17686 + _globals['_GETAPPINFORESPONSE']._serialized_start=17688 + _globals['_GETAPPINFORESPONSE']._serialized_end=17750 + _globals['_APPLICATIONSUMMARY']._serialized_start=17753 + _globals['_APPLICATIONSUMMARY']._serialized_end=17966 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=17968 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=18064 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=18066 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=18113 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=18115 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=18181 + _globals['_SENDSHAREINVITEREQUEST']._serialized_start=18183 + _globals['_SENDSHAREINVITEREQUEST']._serialized_end=18222 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=18225 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=18422 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=18424 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=18479 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=18482 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=18730 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=18732 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=18775 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=18777 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=18880 + _globals['_DOWNLOAD']._serialized_start=18882 + _globals['_DOWNLOAD']._serialized_end=18950 + _globals['_DELETEUSERREQUEST']._serialized_start=18952 + _globals['_DELETEUSERREQUEST']._serialized_end=18987 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=18990 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=19122 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=19124 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=19185 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=19187 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=19276 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=19279 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=19451 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=19453 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=19497 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=19500 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=19681 + _globals['_USERTEAMKEY']._serialized_start=19684 + _globals['_USERTEAMKEY']._serialized_end=19862 + _globals['_GENERICREQUESTRESPONSE']._serialized_start=19864 + _globals['_GENERICREQUESTRESPONSE']._serialized_end=19905 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=19907 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=20009 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=20011 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=20091 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=20094 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=20226 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=20229 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=20536 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=20539 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=20678 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=20681 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=20872 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=20874 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=20947 + _globals['_UPDATEPASSKEYREQUEST']._serialized_start=20949 + _globals['_UPDATEPASSKEYREQUEST']._serialized_end=21053 + _globals['_PASSKEYLISTREQUEST']._serialized_start=21055 + _globals['_PASSKEYLISTREQUEST']._serialized_end=21100 + _globals['_PASSKEYINFO']._serialized_start=21103 + _globals['_PASSKEYINFO']._serialized_end=21267 + _globals['_PASSKEYLISTRESPONSE']._serialized_start=21269 + _globals['_PASSKEYLISTRESPONSE']._serialized_end=21340 + _globals['_TRANSLATIONINFO']._serialized_start=21342 + _globals['_TRANSLATIONINFO']._serialized_end=21409 + _globals['_TRANSLATIONREQUEST']._serialized_start=21411 + _globals['_TRANSLATIONREQUEST']._serialized_end=21455 + _globals['_TRANSLATIONRESPONSE']._serialized_start=21457 + _globals['_TRANSLATIONRESPONSE']._serialized_end=21536 # @@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 149d8093..f12b21e6 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi @@ -3,7 +3,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 @@ -263,6 +264,7 @@ class GenericStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): INVALID_OBJECT: _ClassVar[GenericStatus] ALREADY_EXISTS: _ClassVar[GenericStatus] ACCESS_DENIED: _ClassVar[GenericStatus] + LICENSE_SEAT_EXCEEDED: _ClassVar[GenericStatus] class AuthenticatorAttachment(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -457,6 +459,7 @@ SUCCESS: GenericStatus INVALID_OBJECT: GenericStatus ALREADY_EXISTS: GenericStatus ACCESS_DENIED: GenericStatus +LICENSE_SEAT_EXCEEDED: GenericStatus CROSS_PLATFORM: AuthenticatorAttachment PLATFORM: AuthenticatorAttachment ALL_SUPPORTED: AuthenticatorAttachment @@ -559,7 +562,7 @@ class NewUserMinimumParams(_message.Message): isEnterpriseDomain: bool enterpriseEccPublicKey: bytes forbidKeyType2: bool - def __init__(self, minimumIterations: _Optional[int] = ..., passwordMatchRegex: _Optional[_Iterable[str]] = ..., passwordMatchDescription: _Optional[_Iterable[str]] = ..., isEnterpriseDomain: bool = ..., enterpriseEccPublicKey: _Optional[bytes] = ..., forbidKeyType2: bool = ...) -> None: ... + def __init__(self, minimumIterations: _Optional[int] = ..., passwordMatchRegex: _Optional[_Iterable[str]] = ..., passwordMatchDescription: _Optional[_Iterable[str]] = ..., isEnterpriseDomain: _Optional[bool] = ..., enterpriseEccPublicKey: _Optional[bytes] = ..., forbidKeyType2: _Optional[bool] = ...) -> None: ... class PreLoginRequest(_message.Message): __slots__ = ("authRequest", "loginType", "twoFactorToken") @@ -647,7 +650,7 @@ class StartLoginRequest(_message.Message): v2TwoFactorToken: str accountUid: bytes 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: ... + 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: _Optional[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") @@ -697,7 +700,7 @@ class SwitchListElement(_message.Message): authRequired: bool isLinked: bool profilePicUrl: str - def __init__(self, username: _Optional[str] = ..., fullName: _Optional[str] = ..., authRequired: bool = ..., isLinked: bool = ..., profilePicUrl: _Optional[str] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., fullName: _Optional[str] = ..., authRequired: _Optional[bool] = ..., isLinked: _Optional[bool] = ..., profilePicUrl: _Optional[str] = ...) -> None: ... class SwitchListResponse(_message.Message): __slots__ = ("elements",) @@ -885,7 +888,7 @@ class License(_message.Message): licenseStatus: LicenseStatus paid: bool message: str - def __init__(self, created: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseStatus: _Optional[_Union[LicenseStatus, str]] = ..., paid: bool = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, created: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseStatus: _Optional[_Union[LicenseStatus, str]] = ..., paid: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... class OwnerlessRecord(_message.Message): __slots__ = ("recordUid", "recordKey", "status") @@ -1043,7 +1046,7 @@ class EmailVerificationLinkResponse(_message.Message): __slots__ = ("emailVerified",) EMAILVERIFIED_FIELD_NUMBER: _ClassVar[int] emailVerified: bool - def __init__(self, emailVerified: bool = ...) -> None: ... + def __init__(self, emailVerified: _Optional[bool] = ...) -> None: ... class SecurityData(_message.Message): __slots__ = ("uid", "data") @@ -1115,7 +1118,7 @@ class SecurityReport(_message.Message): securityReportIncrementalData: _containers.RepeatedCompositeFieldContainer[SecurityReportIncrementalData] userId: int hasOldEncryption: bool - 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: ... + 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: _Optional[bool] = ...) -> None: ... class SecurityReportSaveRequest(_message.Message): __slots__ = ("securityReport", "continuationToken") @@ -1149,7 +1152,7 @@ class SecurityReportResponse(_message.Message): complete: bool enterpriseEccPrivateKey: bytes 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: ... + def __init__(self, enterprisePrivateKey: _Optional[bytes] = ..., securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ..., asOfRevision: _Optional[int] = ..., fromPage: _Optional[int] = ..., toPage: _Optional[int] = ..., complete: _Optional[bool] = ..., enterpriseEccPrivateKey: _Optional[bytes] = ..., hasIncrementalData: _Optional[bool] = ...) -> None: ... class IncrementalSecurityDataRequest(_message.Message): __slots__ = ("continuationToken",) @@ -1227,7 +1230,7 @@ class GetChangeKeyTypesRequest(_message.Message): includeRecommended: bool includeKeys: bool includeAllowedKeyTypes: bool - def __init__(self, onlyTheseObjects: _Optional[_Iterable[_Union[EncryptedObjectType, str]]] = ..., limit: _Optional[int] = ..., includeRecommended: bool = ..., includeKeys: bool = ..., includeAllowedKeyTypes: bool = ...) -> None: ... + def __init__(self, onlyTheseObjects: _Optional[_Iterable[_Union[EncryptedObjectType, str]]] = ..., limit: _Optional[int] = ..., includeRecommended: _Optional[bool] = ..., includeKeys: _Optional[bool] = ..., includeAllowedKeyTypes: _Optional[bool] = ...) -> None: ... class GetChangeKeyTypesResponse(_message.Message): __slots__ = ("keys", "allowedKeyTypes") @@ -1354,20 +1357,22 @@ class NodeEnforcementRemoveRequest(_message.Message): def __init__(self, nodeId: _Optional[int] = ..., enforcement: _Optional[str] = ...) -> None: ... class ApiRequestByKey(_message.Message): - __slots__ = ("keyId", "payload", "username", "locale", "supportedLanguage", "type") + __slots__ = ("keyId", "payload", "username", "locale", "supportedLanguage", "type", "parentThreadId") KEYID_FIELD_NUMBER: _ClassVar[int] PAYLOAD_FIELD_NUMBER: _ClassVar[int] USERNAME_FIELD_NUMBER: _ClassVar[int] LOCALE_FIELD_NUMBER: _ClassVar[int] SUPPORTEDLANGUAGE_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] + PARENTTHREADID_FIELD_NUMBER: _ClassVar[int] keyId: int payload: bytes username: str locale: str supportedLanguage: SupportedLanguage type: int - def __init__(self, keyId: _Optional[int] = ..., payload: _Optional[bytes] = ..., username: _Optional[str] = ..., locale: _Optional[str] = ..., supportedLanguage: _Optional[_Union[SupportedLanguage, str]] = ..., type: _Optional[int] = ...) -> None: ... + parentThreadId: str + def __init__(self, keyId: _Optional[int] = ..., payload: _Optional[bytes] = ..., username: _Optional[str] = ..., locale: _Optional[str] = ..., supportedLanguage: _Optional[_Union[SupportedLanguage, str]] = ..., type: _Optional[int] = ..., parentThreadId: _Optional[str] = ...) -> None: ... class ApiRequestByKAtoKAKey(_message.Message): __slots__ = ("sourceRegion", "payload", "supportedLanguage", "destinationRegion") @@ -1491,7 +1496,7 @@ class ApproveDeviceRequest(_message.Message): encryptedDeviceDataKey: bytes denyApproval: bool linkDevice: bool - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: bool = ..., linkDevice: bool = ...) -> None: ... + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: _Optional[bool] = ..., linkDevice: _Optional[bool] = ...) -> None: ... class EnterpriseUserAliasRequest(_message.Message): __slots__ = ("enterpriseUserId", "alias") @@ -1509,7 +1514,7 @@ class EnterpriseUserAddAliasRequest(_message.Message): enterpriseUserId: int alias: str primary: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., alias: _Optional[str] = ..., primary: bool = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., alias: _Optional[str] = ..., primary: _Optional[bool] = ...) -> None: ... class EnterpriseUserAddAliasRequestV2(_message.Message): __slots__ = ("enterpriseUserAddAliasRequest",) @@ -1617,7 +1622,7 @@ class SsoServiceProviderResponse(_message.Message): spUrl: str isCloud: bool clientVersion: str - def __init__(self, name: _Optional[str] = ..., spUrl: _Optional[str] = ..., isCloud: bool = ..., clientVersion: _Optional[str] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., spUrl: _Optional[str] = ..., isCloud: _Optional[bool] = ..., clientVersion: _Optional[str] = ...) -> None: ... class UserSettingRequest(_message.Message): __slots__ = ("setting", "value") @@ -1637,7 +1642,7 @@ class ThrottleState(_message.Message): key: str value: str state: bool - def __init__(self, type: _Optional[_Union[ThrottleType, str]] = ..., key: _Optional[str] = ..., value: _Optional[str] = ..., state: bool = ...) -> None: ... + def __init__(self, type: _Optional[_Union[ThrottleType, str]] = ..., key: _Optional[str] = ..., value: _Optional[str] = ..., state: _Optional[bool] = ...) -> None: ... class ThrottleState2(_message.Message): __slots__ = ("key", "keyDescription", "value", "valueDescription", "identifier", "locked", "includedInAllClear", "expireSeconds") @@ -1657,7 +1662,7 @@ class ThrottleState2(_message.Message): locked: bool includedInAllClear: bool expireSeconds: int - def __init__(self, key: _Optional[str] = ..., keyDescription: _Optional[str] = ..., value: _Optional[str] = ..., valueDescription: _Optional[str] = ..., identifier: _Optional[str] = ..., locked: bool = ..., includedInAllClear: bool = ..., expireSeconds: _Optional[int] = ...) -> None: ... + def __init__(self, key: _Optional[str] = ..., keyDescription: _Optional[str] = ..., value: _Optional[str] = ..., valueDescription: _Optional[str] = ..., identifier: _Optional[str] = ..., locked: _Optional[bool] = ..., includedInAllClear: _Optional[bool] = ..., expireSeconds: _Optional[int] = ...) -> None: ... class DeviceInformation(_message.Message): __slots__ = ("deviceId", "deviceName", "clientVersion", "lastLogin", "deviceStatus") @@ -1679,7 +1684,7 @@ class UserSetting(_message.Message): VALUE_FIELD_NUMBER: _ClassVar[int] name: str value: bool - def __init__(self, name: _Optional[str] = ..., value: bool = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., value: _Optional[bool] = ...) -> None: ... class UserDataKeyRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -1775,7 +1780,7 @@ class PasswordRules(_message.Message): description: str minimum: int value: str - def __init__(self, ruleType: _Optional[str] = ..., match: bool = ..., pattern: _Optional[str] = ..., description: _Optional[str] = ..., minimum: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... + def __init__(self, ruleType: _Optional[str] = ..., match: _Optional[bool] = ..., pattern: _Optional[str] = ..., description: _Optional[str] = ..., minimum: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... class GetDataKeyBackupV3Response(_message.Message): __slots__ = ("dataKeyBackup", "dataKeyBackupDate", "publicKey", "encryptedPrivateKey", "clientKey", "encryptedSessionToken", "passwordRules", "passwordRulesIntro", "minimumPbkdf2Iterations", "keyType") @@ -1915,7 +1920,7 @@ class AppShareAdd(_message.Message): shareType: ApplicationShareType encryptedSecretKey: bytes editable: bool - def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., encryptedSecretKey: _Optional[bytes] = ..., editable: bool = ...) -> None: ... + def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., encryptedSecretKey: _Optional[bytes] = ..., editable: _Optional[bool] = ...) -> None: ... class AppShare(_message.Message): __slots__ = ("secretUid", "shareType", "editable", "createdOn", "data") @@ -1929,7 +1934,7 @@ class AppShare(_message.Message): editable: bool createdOn: int data: bytes - def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., editable: bool = ..., createdOn: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... + def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., editable: _Optional[bool] = ..., createdOn: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... class AddAppClientRequest(_message.Message): __slots__ = ("appRecordUid", "encryptedAppKey", "clientId", "lockIp", "firstAccessExpireOn", "accessExpireOn", "id", "appClientType") @@ -1949,7 +1954,7 @@ class AddAppClientRequest(_message.Message): accessExpireOn: int id: str appClientType: _enterprise_pb2.AppClientType - def __init__(self, appRecordUid: _Optional[bytes] = ..., encryptedAppKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., lockIp: bool = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., encryptedAppKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., lockIp: _Optional[bool] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ...) -> None: ... class RemoveAppClientsRequest(_message.Message): __slots__ = ("appRecordUid", "clients") @@ -1975,7 +1980,7 @@ class AddExternalShareRequest(_message.Message): id: str isSelfDestruct: bool 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: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedRecordKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., isSelfDestruct: _Optional[bool] = ..., isEditable: _Optional[bool] = ...) -> None: ... class AppClient(_message.Message): __slots__ = ("id", "clientId", "createdOn", "firstAccess", "lastAccess", "publicKey", "lockIp", "ipAddress", "firstAccessExpireOn", "accessExpireOn", "appClientType", "canEdit") @@ -2003,7 +2008,7 @@ class AppClient(_message.Message): accessExpireOn: int appClientType: _enterprise_pb2.AppClientType 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: ... + def __init__(self, id: _Optional[str] = ..., clientId: _Optional[bytes] = ..., createdOn: _Optional[int] = ..., firstAccess: _Optional[int] = ..., lastAccess: _Optional[int] = ..., publicKey: _Optional[bytes] = ..., lockIp: _Optional[bool] = ..., ipAddress: _Optional[str] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., canEdit: _Optional[bool] = ...) -> None: ... class GetAppInfoRequest(_message.Message): __slots__ = ("appRecordUid",) @@ -2021,7 +2026,7 @@ class AppInfo(_message.Message): shares: _containers.RepeatedCompositeFieldContainer[AppShare] clients: _containers.RepeatedCompositeFieldContainer[AppClient] isExternalShare: bool - def __init__(self, appRecordUid: _Optional[bytes] = ..., shares: _Optional[_Iterable[_Union[AppShare, _Mapping]]] = ..., clients: _Optional[_Iterable[_Union[AppClient, _Mapping]]] = ..., isExternalShare: bool = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., shares: _Optional[_Iterable[_Union[AppShare, _Mapping]]] = ..., clients: _Optional[_Iterable[_Union[AppClient, _Mapping]]] = ..., isExternalShare: _Optional[bool] = ...) -> None: ... class GetAppInfoResponse(_message.Message): __slots__ = ("appInfo",) @@ -2153,7 +2158,7 @@ class ChangeMasterPasswordRequest(_message.Message): encryptionParams: bytes fromServiceProvider: bool iterationsChange: bool - def __init__(self, authVerifier: _Optional[bytes] = ..., encryptionParams: _Optional[bytes] = ..., fromServiceProvider: bool = ..., iterationsChange: bool = ...) -> None: ... + def __init__(self, authVerifier: _Optional[bytes] = ..., encryptionParams: _Optional[bytes] = ..., fromServiceProvider: _Optional[bool] = ..., iterationsChange: _Optional[bool] = ...) -> None: ... class ChangeMasterPasswordResponse(_message.Message): __slots__ = ("encryptedSessionToken",) @@ -2291,7 +2296,7 @@ class PasskeyValidationResponse(_message.Message): ENCRYPTEDLOGINTOKEN_FIELD_NUMBER: _ClassVar[int] isValid: bool encryptedLoginToken: bytes - def __init__(self, isValid: bool = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... + def __init__(self, isValid: _Optional[bool] = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... class UpdatePasskeyRequest(_message.Message): __slots__ = ("userId", "credentialId", "friendlyName") @@ -2307,7 +2312,7 @@ class PasskeyListRequest(_message.Message): __slots__ = ("includeDisabled",) INCLUDEDISABLED_FIELD_NUMBER: _ClassVar[int] includeDisabled: bool - def __init__(self, includeDisabled: bool = ...) -> None: ... + def __init__(self, includeDisabled: _Optional[bool] = ...) -> None: ... class PasskeyInfo(_message.Message): __slots__ = ("userId", "credentialId", "friendlyName", "AAGUID", "createdAtMillis", "lastUsedMillis", "disabledAtMillis") diff --git a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py index b3f598a9..aa9d944b 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: AccountSummary.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'AccountSummary.proto' ) @@ -25,7 +25,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\"\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') +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\"\xb0\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\x12\x16\n\x0e\x66orbidKeyType1\x18\x13 \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\"\xf4\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\x12\x15\n\rmaxIpDistance\x18( \x01(\x05\x12\x1e\n\x16maxIpDistanceEffective\x18) \x01(\x05\"&\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) @@ -36,39 +36,39 @@ _globals['_ACCOUNTSUMMARYREQUEST']._serialized_start=58 _globals['_ACCOUNTSUMMARYREQUEST']._serialized_end=136 _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_start=139 - _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_end=803 - _globals['_DEVICEINFO']._serialized_start=806 - _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 + _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_end=827 + _globals['_DEVICEINFO']._serialized_start=830 + _globals['_DEVICEINFO']._serialized_end=1225 + _globals['_KEYSINFO']._serialized_start=1228 + _globals['_KEYSINFO']._serialized_end=1421 + _globals['_SYNCLOG']._serialized_start=1424 + _globals['_SYNCLOG']._serialized_end=1553 + _globals['_LICENSE']._serialized_start=1556 + _globals['_LICENSE']._serialized_end=2449 + _globals['_ADDON']._serialized_start=2452 + _globals['_ADDON']._serialized_end=2615 + _globals['_SETTINGS']._serialized_start=2618 + _globals['_SETTINGS']._serialized_end=3886 + _globals['_KEYVALUE']._serialized_start=3888 + _globals['_KEYVALUE']._serialized_end=3926 + _globals['_KEYVALUEBOOLEAN']._serialized_start=3928 + _globals['_KEYVALUEBOOLEAN']._serialized_end=3973 + _globals['_KEYVALUELONG']._serialized_start=3975 + _globals['_KEYVALUELONG']._serialized_end=4017 + _globals['_RESULT']._serialized_start=4019 + _globals['_RESULT']._serialized_end=4080 + _globals['_ENFORCEMENTS']._serialized_start=4083 + _globals['_ENFORCEMENTS']._serialized_end=4277 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_start=4279 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_end=4339 + _globals['_PASSWORDRULE']._serialized_start=4341 + _globals['_PASSWORDRULE']._serialized_end=4458 + _globals['_SECURITYKEY']._serialized_start=4461 + _globals['_SECURITYKEY']._serialized_end=4612 + _globals['_DEVICEREGISTRATION']._serialized_start=4614 + _globals['_DEVICEREGISTRATION']._serialized_end=4735 + _globals['_GROUP']._serialized_start=4737 + _globals['_GROUP']._serialized_end=4844 + _globals['_ADMINISTRATOR']._serialized_start=4847 + _globals['_ADMINISTRATOR']._serialized_end=5039 # @@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 271a2ad1..1e1b67d4 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi @@ -2,7 +2,8 @@ import APIRequest_pb2 as _APIRequest_pb2 from google.protobuf.internal import containers as _containers 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 @@ -12,10 +13,10 @@ class AccountSummaryRequest(_message.Message): INCLUDERECENTACTIVITY_FIELD_NUMBER: _ClassVar[int] summaryVersion: int includeRecentActivity: bool - def __init__(self, summaryVersion: _Optional[int] = ..., includeRecentActivity: bool = ...) -> None: ... + def __init__(self, summaryVersion: _Optional[int] = ..., includeRecentActivity: _Optional[bool] = ...) -> None: ... class AccountSummaryElements(_message.Message): - __slots__ = ("clientKey", "settings", "keysInfo", "syncLogs", "isEnterpriseAdmin", "license", "group", "Enforcements", "Images", "personalLicense", "fixSharedFolderRecords", "usernames", "devices", "isShareAdmin", "accountRecovery", "accountRecoveryPrompt", "minMasterPasswordLengthNoPrompt", "forbidKeyType2") + __slots__ = ("clientKey", "settings", "keysInfo", "syncLogs", "isEnterpriseAdmin", "license", "group", "Enforcements", "Images", "personalLicense", "fixSharedFolderRecords", "usernames", "devices", "isShareAdmin", "accountRecovery", "accountRecoveryPrompt", "minMasterPasswordLengthNoPrompt", "forbidKeyType2", "forbidKeyType1") CLIENTKEY_FIELD_NUMBER: _ClassVar[int] SETTINGS_FIELD_NUMBER: _ClassVar[int] KEYSINFO_FIELD_NUMBER: _ClassVar[int] @@ -34,6 +35,7 @@ class AccountSummaryElements(_message.Message): ACCOUNTRECOVERYPROMPT_FIELD_NUMBER: _ClassVar[int] MINMASTERPASSWORDLENGTHNOPROMPT_FIELD_NUMBER: _ClassVar[int] FORBIDKEYTYPE2_FIELD_NUMBER: _ClassVar[int] + FORBIDKEYTYPE1_FIELD_NUMBER: _ClassVar[int] clientKey: bytes settings: Settings keysInfo: KeysInfo @@ -52,7 +54,8 @@ class AccountSummaryElements(_message.Message): accountRecoveryPrompt: bool minMasterPasswordLengthNoPrompt: int forbidKeyType2: bool - 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: ... + forbidKeyType1: bool + def __init__(self, clientKey: _Optional[bytes] = ..., settings: _Optional[_Union[Settings, _Mapping]] = ..., keysInfo: _Optional[_Union[KeysInfo, _Mapping]] = ..., syncLogs: _Optional[_Iterable[_Union[SyncLog, _Mapping]]] = ..., isEnterpriseAdmin: _Optional[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: _Optional[bool] = ..., usernames: _Optional[_Iterable[str]] = ..., devices: _Optional[_Iterable[_Union[DeviceInfo, _Mapping]]] = ..., isShareAdmin: _Optional[bool] = ..., accountRecovery: _Optional[bool] = ..., accountRecoveryPrompt: _Optional[bool] = ..., minMasterPasswordLengthNoPrompt: _Optional[int] = ..., forbidKeyType2: _Optional[bool] = ..., forbidKeyType1: _Optional[bool] = ...) -> None: ... class DeviceInfo(_message.Message): __slots__ = ("encryptedDeviceToken", "deviceName", "deviceStatus", "devicePublicKey", "encryptedDataKeyDoNotUse", "clientVersion", "username", "ipAddress", "approveRequestTime", "encryptedDataKeyPresent", "groupId", "devicePlatform", "clientFormFactor") @@ -82,7 +85,7 @@ class DeviceInfo(_message.Message): groupId: int 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: ... + 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: _Optional[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") @@ -188,7 +191,7 @@ class License(_message.Message): pendingEnterprise: bool 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: ... + 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: _Optional[bool] = ..., accountType: _Optional[int] = ..., uploadsRemaining: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., chatEnabled: _Optional[bool] = ..., auditAndReportingEnabled: _Optional[bool] = ..., breachWatchFeatureDisable: _Optional[bool] = ..., accountUid: _Optional[bytes] = ..., allowPersonalLicense: _Optional[bool] = ..., licensedBy: _Optional[str] = ..., email: _Optional[str] = ..., breachWatchEnabled: _Optional[bool] = ..., breachWatchScanned: _Optional[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: _Optional[bool] = ..., isPamEnabled: _Optional[bool] = ..., isKsmEnabled: _Optional[bool] = ...) -> None: ... class AddOn(_message.Message): __slots__ = ("licenseKeyId", "name", "expirationDate", "createdDate", "isTrial", "enabled", "scanned", "featureDisable") @@ -208,10 +211,10 @@ class AddOn(_message.Message): enabled: bool scanned: bool featureDisable: bool - def __init__(self, licenseKeyId: _Optional[int] = ..., name: _Optional[str] = ..., expirationDate: _Optional[int] = ..., createdDate: _Optional[int] = ..., isTrial: bool = ..., enabled: bool = ..., scanned: bool = ..., featureDisable: bool = ...) -> None: ... + def __init__(self, licenseKeyId: _Optional[int] = ..., name: _Optional[str] = ..., expirationDate: _Optional[int] = ..., createdDate: _Optional[int] = ..., isTrial: _Optional[bool] = ..., enabled: _Optional[bool] = ..., scanned: _Optional[bool] = ..., featureDisable: _Optional[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", "personalUsernames") + __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", "maxIpDistance", "maxIpDistanceEffective") AUDIT_FIELD_NUMBER: _ClassVar[int] MUSTPERFORMACCOUNTSHAREBY_FIELD_NUMBER: _ClassVar[int] SHAREACCOUNTTO_FIELD_NUMBER: _ClassVar[int] @@ -251,6 +254,8 @@ class Settings(_message.Message): SECURITYKEYSNOUSERVERIFY_FIELD_NUMBER: _ClassVar[int] CHANNELS_FIELD_NUMBER: _ClassVar[int] PERSONALUSERNAMES_FIELD_NUMBER: _ClassVar[int] + MAXIPDISTANCE_FIELD_NUMBER: _ClassVar[int] + MAXIPDISTANCEEFFECTIVE_FIELD_NUMBER: _ClassVar[int] audit: bool mustPerformAccountShareBy: int shareAccountTo: _containers.RepeatedCompositeFieldContainer[MissingAccountShareKey] @@ -290,7 +295,9 @@ class Settings(_message.Message): securityKeysNoUserVerify: bool channels: _containers.RepeatedScalarFieldContainer[_APIRequest_pb2.TwoFactorChannelType] 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: ... + maxIpDistance: int + maxIpDistanceEffective: int + def __init__(self, audit: _Optional[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: _Optional[bool] = ..., emailVerified: _Optional[bool] = ..., masterPasswordLastModified: _Optional[float] = ..., accountFolderKey: _Optional[bytes] = ..., securityKeys: _Optional[_Iterable[_Union[SecurityKey, _Mapping]]] = ..., keyValues: _Optional[_Iterable[_Union[KeyValue, _Mapping]]] = ..., ssoUser: _Optional[bool] = ..., onlineAccessOnly: _Optional[bool] = ..., masterPasswordExpiry: _Optional[int] = ..., twoFactorRequired: _Optional[bool] = ..., disallowExport: _Optional[bool] = ..., restrictFiles: _Optional[bool] = ..., restrictAllSharing: _Optional[bool] = ..., restrictSharing: _Optional[bool] = ..., restrictSharingIncomingAll: _Optional[bool] = ..., restrictSharingIncomingEnterprise: _Optional[bool] = ..., logoutTimer: _Optional[int] = ..., persistentLogin: _Optional[bool] = ..., ipDisableAutoApprove: _Optional[bool] = ..., shareDataKeyWithEccPublicKey: _Optional[bool] = ..., shareDataKeyWithDevicePublicKey: _Optional[bool] = ..., RecordTypesCounter: _Optional[int] = ..., RecordTypesEnterpriseCounter: _Optional[int] = ..., recordTypesEnabled: _Optional[bool] = ..., canManageRecordTypes: _Optional[bool] = ..., recordTypesPAMCounter: _Optional[int] = ..., logoutTimerMinutes: _Optional[int] = ..., securityKeysNoUserVerify: _Optional[bool] = ..., channels: _Optional[_Iterable[_Union[_APIRequest_pb2.TwoFactorChannelType, str]]] = ..., personalUsernames: _Optional[_Iterable[str]] = ..., maxIpDistance: _Optional[int] = ..., maxIpDistanceEffective: _Optional[int] = ...) -> None: ... class KeyValue(_message.Message): __slots__ = ("key", "value") @@ -306,7 +313,7 @@ class KeyValueBoolean(_message.Message): VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: bool - def __init__(self, key: _Optional[str] = ..., value: bool = ...) -> None: ... + def __init__(self, key: _Optional[str] = ..., value: _Optional[bool] = ...) -> None: ... class KeyValueLong(_message.Message): __slots__ = ("key", "value") @@ -360,7 +367,7 @@ class PasswordRule(_message.Message): minimum: int description: str value: str - def __init__(self, ruleType: _Optional[str] = ..., pattern: _Optional[str] = ..., match: bool = ..., minimum: _Optional[int] = ..., description: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + def __init__(self, ruleType: _Optional[str] = ..., pattern: _Optional[str] = ..., match: _Optional[bool] = ..., minimum: _Optional[int] = ..., description: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... class SecurityKey(_message.Message): __slots__ = ("deviceId", "deviceName", "dateAdded", "isValid", "deviceRegistration") @@ -374,7 +381,7 @@ class SecurityKey(_message.Message): dateAdded: int isValid: bool deviceRegistration: DeviceRegistration - def __init__(self, deviceId: _Optional[int] = ..., deviceName: _Optional[str] = ..., dateAdded: _Optional[int] = ..., isValid: bool = ..., deviceRegistration: _Optional[_Union[DeviceRegistration, _Mapping]] = ...) -> None: ... + def __init__(self, deviceId: _Optional[int] = ..., deviceName: _Optional[str] = ..., dateAdded: _Optional[int] = ..., isValid: _Optional[bool] = ..., deviceRegistration: _Optional[_Union[DeviceRegistration, _Mapping]] = ...) -> None: ... class DeviceRegistration(_message.Message): __slots__ = ("keyHandle", "publicKey", "attestationCert", "counter", "compromised") @@ -388,7 +395,7 @@ class DeviceRegistration(_message.Message): attestationCert: str counter: int compromised: bool - def __init__(self, keyHandle: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., attestationCert: _Optional[str] = ..., counter: _Optional[int] = ..., compromised: bool = ...) -> None: ... + def __init__(self, keyHandle: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., attestationCert: _Optional[str] = ..., counter: _Optional[int] = ..., compromised: _Optional[bool] = ...) -> None: ... class Group(_message.Message): __slots__ = ("admin", "groupVerificationCode", "administrator") @@ -398,7 +405,7 @@ class Group(_message.Message): admin: bool groupVerificationCode: str administrator: Administrator - def __init__(self, admin: bool = ..., groupVerificationCode: _Optional[str] = ..., administrator: _Optional[_Union[Administrator, _Mapping]] = ...) -> None: ... + def __init__(self, admin: _Optional[bool] = ..., groupVerificationCode: _Optional[str] = ..., administrator: _Optional[_Union[Administrator, _Mapping]] = ...) -> None: ... class Administrator(_message.Message): __slots__ = ("firstName", "lastName", "email", "currentNumberOfUsers", "numberOfUsers", "subscriptionCode", "expirationDate", "purchaseDate") diff --git a/keepersdk-package/src/keepersdk/proto/BI_pb2.py b/keepersdk-package/src/keepersdk/proto/BI_pb2.py index 09b2ff51..c1960027 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: BI.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'BI.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -17,7 +25,7 @@ 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\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') +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\".\n\rEventResponse\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x0e\n\x06status\x18\x02 \x01(\x08\"5\n\x0e\x45ventsResponse\x12#\n\x08response\x18\x01 \x03(\x0b\x32\x11.BI.EventResponse\"\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\"\x96\x01\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\x12\x1a\n\x12purchaseIdentifier\x18\x06 \x01(\t\"k\n\x0fPurchaseOptions\x12\x16\n\tinConsole\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10\x65xternalCheckout\x18\x02 \x01(\x08H\x01\x88\x01\x01\x42\x0c\n\n_inConsoleB\x13\n\x11_externalCheckout\"\xae\x06\n\x14\x41\x64\x64onPurchaseOptions\x12)\n\x07storage\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x00\x88\x01\x01\x12\'\n\x05\x61udit\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x01\x88\x01\x01\x12-\n\x0b\x62reachwatch\x18\x03 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x02\x88\x01\x01\x12&\n\x04\x63hat\x18\x04 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x03\x88\x01\x01\x12,\n\ncompliance\x18\x05 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x04\x88\x01\x01\x12<\n\x1aprofessionalServicesSilver\x18\x06 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x05\x88\x01\x01\x12>\n\x1cprofessionalServicesPlatinum\x18\x07 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x06\x88\x01\x01\x12%\n\x03pam\x18\x08 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x07\x88\x01\x01\x12%\n\x03\x65pm\x18\t \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x08\x88\x01\x01\x12\x30\n\x0esecretsManager\x18\n \x01(\x0b\x32\x13.BI.PurchaseOptionsH\t\x88\x01\x01\x12\x33\n\x11\x63onnectionManager\x18\x0b \x01(\x0b\x32\x13.BI.PurchaseOptionsH\n\x88\x01\x01\x12\x38\n\x16remoteBrowserIsolation\x18\x0c \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x0b\x88\x01\x01\x42\n\n\x08_storageB\x08\n\x06_auditB\x0e\n\x0c_breachwatchB\x07\n\x05_chatB\r\n\x0b_complianceB\x1d\n\x1b_professionalServicesSilverB\x1f\n\x1d_professionalServicesPlatinumB\x06\n\x04_pamB\x06\n\x04_epmB\x11\n\x0f_secretsManagerB\x14\n\x12_connectionManagerB\x19\n\x17_remoteBrowserIsolation\"\x8f\x01\n\x18\x41vailablePurchaseOptions\x12%\n\x08\x62\x61sePlan\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12\"\n\x05users\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12(\n\x06\x61\x64\x64ons\x18\x03 \x01(\x0b\x32\x18.BI.AddonPurchaseOptions\"\x1d\n\x1bUpgradeLicenseStatusRequest\"\x91\x01\n\x1cUpgradeLicenseStatusResponse\x12 \n\x18\x61llowPurchaseFromConsole\x18\x01 \x01(\x08\x12\x35\n\x0fpurchaseOptions\x18\x02 \x01(\x0b\x32\x1c.BI.AvailablePurchaseOptions\x12\x18\n\x05\x65rror\x18\x03 \x01(\x0b\x32\t.BI.Error\"r\n\"UpgradeLicenseQuotePurchaseRequest\x12,\n\x0bproductType\x18\x01 \x01(\x0e\x32\x17.BI.PurchaseProductType\x12\x10\n\x08quantity\x18\x02 \x01(\x05\x12\x0c\n\x04tier\x18\x03 \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\"\x9f\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\x12\x0c\n\x04tier\x18\x04 \x01(\x05\"\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\"J\n\x18SingularDeviceIdentifier\x12\n\n\x02id\x18\x01 \x01(\t\x12\"\n\x06idType\x18\x02 \x01(\x0e\x32\x12.BI.IdentifierType\"\xac\x01\n\x12SingularSharedData\x12\x10\n\x08platform\x18\x01 \x01(\t\x12\x11\n\tosVersion\x18\x02 \x01(\t\x12\x0c\n\x04make\x18\x03 \x01(\t\x12\r\n\x05model\x18\x04 \x01(\t\x12\x0e\n\x06locale\x18\x05 \x01(\t\x12\r\n\x05\x62uild\x18\x06 \x01(\t\x12\x15\n\rappIdentifier\x18\x07 \x01(\t\x12\x1e\n\x16\x61ttAuthorizationStatus\x18\x08 \x01(\x05\"\x8b\x03\n\x16SingularSessionRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x1a\n\x12\x61pplicationVersion\x18\x03 \x01(\t\x12\x0f\n\x07install\x18\x04 \x01(\x08\x12\x13\n\x0binstallTime\x18\x05 \x01(\x03\x12\x12\n\nupdateTime\x18\x06 \x01(\x03\x12\x15\n\rinstallSource\x18\x07 \x01(\t\x12\x16\n\x0einstallReceipt\x18\x08 \x01(\t\x12\x0f\n\x07openuri\x18\t \x01(\t\x12\x12\n\nddlEnabled\x18\n \x01(\x08\x12#\n\x1bsingularLinkResolveRequired\x18\x0b \x01(\x08\x12\x12\n\ninstallRef\x18\x0c \x01(\t\x12\x0f\n\x07metaRef\x18\r \x01(\t\x12\x18\n\x10\x61ttributionToken\x18\x0e \x01(\t\"\x8e\x01\n\x14SingularEventRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x11\n\teventName\x18\x03 \x01(\t\"-\n\x15\x41\x63tivePamCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\"*\n\x16\x41\x63tivePamCountResponse\x12\x10\n\x08pamCount\x18\x01 \x01(\x05*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*\xd5\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\t\x12\x0b\n\x07\x61\x64\x64KEPM\x10\n*\xe0\x01\n\x0eIdentifierType\x12\x1b\n\x17UNKNOWN_IDENTIFIER_TYPE\x10\x00\x12\n\n\x06IOS_ID\x10\x01\x12\x1a\n\x16\x41NDROID_GOOGLE_PLAY_ID\x10\x02\x12\x16\n\x12\x41NDROID_APP_SET_ID\x10\x03\x12\x0e\n\nANDROID_ID\x10\x04\x12\x19\n\x15\x41MAZON_ADVERTISING_ID\x10\x05\x12\x17\n\x13OPEN_ADVERTISING_ID\x10\x06\x12\x16\n\x12SINGULAR_DEVICE_ID\x10\x07\x12\x15\n\x11\x43LIENT_DEFINED_ID\x10\x08\x42\x1e\n\x18\x63om.keepersecurity.protoB\x02\x42Ib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -27,14 +35,16 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\002BI' _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['_CURRENCY']._serialized_start=8918 + _globals['_CURRENCY']._serialized_end=8995 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_start=8997 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_end=9080 + _globals['_EVENTTYPE']._serialized_start=9083 + _globals['_EVENTTYPE']._serialized_end=9306 + _globals['_PURCHASEPRODUCTTYPE']._serialized_start=9309 + _globals['_PURCHASEPRODUCTTYPE']._serialized_end=9522 + _globals['_IDENTIFIERTYPE']._serialized_start=9525 + _globals['_IDENTIFIERTYPE']._serialized_end=9749 _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_start=46 _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_end=148 _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_start=151 @@ -159,34 +169,56 @@ _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 + _globals['_EVENTRESPONSE']._serialized_start=5221 + _globals['_EVENTRESPONSE']._serialized_end=5267 + _globals['_EVENTSRESPONSE']._serialized_start=5269 + _globals['_EVENTSRESPONSE']._serialized_end=5322 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_start=5325 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_end=5494 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_start=5496 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_end=5521 + _globals['_ERROR']._serialized_start=5523 + _globals['_ERROR']._serialized_end=5647 + _globals['_ERROR_EXTRASENTRY']._serialized_start=5602 + _globals['_ERROR_EXTRASENTRY']._serialized_end=5647 + _globals['_QUOTEPURCHASE']._serialized_start=5650 + _globals['_QUOTEPURCHASE']._serialized_end=5800 + _globals['_PURCHASEOPTIONS']._serialized_start=5802 + _globals['_PURCHASEOPTIONS']._serialized_end=5909 + _globals['_ADDONPURCHASEOPTIONS']._serialized_start=5912 + _globals['_ADDONPURCHASEOPTIONS']._serialized_end=6726 + _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_start=6729 + _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_end=6872 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_start=6874 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_end=6903 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_start=6906 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_end=7051 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_start=7053 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_end=7167 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_start=7170 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_end=7317 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_start=7320 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_end=7479 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_start=7482 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_end=7630 + _globals['_ENTERPRISEBASEPLAN']._serialized_start=7633 + _globals['_ENTERPRISEBASEPLAN']._serialized_end=7846 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_start=7754 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_end=7846 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_start=7848 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_end=7886 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_start=7889 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_end=8031 + _globals['_SINGULARDEVICEIDENTIFIER']._serialized_start=8033 + _globals['_SINGULARDEVICEIDENTIFIER']._serialized_end=8107 + _globals['_SINGULARSHAREDDATA']._serialized_start=8110 + _globals['_SINGULARSHAREDDATA']._serialized_end=8282 + _globals['_SINGULARSESSIONREQUEST']._serialized_start=8285 + _globals['_SINGULARSESSIONREQUEST']._serialized_end=8680 + _globals['_SINGULAREVENTREQUEST']._serialized_start=8683 + _globals['_SINGULAREVENTREQUEST']._serialized_end=8825 + _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_start=8827 + _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_end=8872 + _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_start=8874 + _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_end=8916 # @@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 b8e4b319..39f992b6 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi @@ -3,7 +3,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 @@ -47,6 +48,19 @@ class PurchaseProductType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): addPAM: _ClassVar[PurchaseProductType] addSilverSupport: _ClassVar[PurchaseProductType] addPlatinumSupport: _ClassVar[PurchaseProductType] + addKEPM: _ClassVar[PurchaseProductType] + +class IdentifierType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN_IDENTIFIER_TYPE: _ClassVar[IdentifierType] + IOS_ID: _ClassVar[IdentifierType] + ANDROID_GOOGLE_PLAY_ID: _ClassVar[IdentifierType] + ANDROID_APP_SET_ID: _ClassVar[IdentifierType] + ANDROID_ID: _ClassVar[IdentifierType] + AMAZON_ADVERTISING_ID: _ClassVar[IdentifierType] + OPEN_ADVERTISING_ID: _ClassVar[IdentifierType] + SINGULAR_DEVICE_ID: _ClassVar[IdentifierType] + CLIENT_DEFINED_ID: _ClassVar[IdentifierType] UNKNOWN: Currency USD: Currency GBP: Currency @@ -76,6 +90,16 @@ addChat: PurchaseProductType addPAM: PurchaseProductType addSilverSupport: PurchaseProductType addPlatinumSupport: PurchaseProductType +addKEPM: PurchaseProductType +UNKNOWN_IDENTIFIER_TYPE: IdentifierType +IOS_ID: IdentifierType +ANDROID_GOOGLE_PLAY_ID: IdentifierType +ANDROID_APP_SET_ID: IdentifierType +ANDROID_ID: IdentifierType +AMAZON_ADVERTISING_ID: IdentifierType +OPEN_ADVERTISING_ID: IdentifierType +SINGULAR_DEVICE_ID: IdentifierType +CLIENT_DEFINED_ID: IdentifierType class ValidateSessionTokenRequest(_message.Message): __slots__ = ("encryptedSessionToken", "returnMcEnterpiseIds", "ip") @@ -85,7 +109,7 @@ class ValidateSessionTokenRequest(_message.Message): encryptedSessionToken: bytes returnMcEnterpiseIds: bool ip: str - def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., returnMcEnterpiseIds: bool = ..., ip: _Optional[str] = ...) -> None: ... + def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., returnMcEnterpiseIds: _Optional[bool] = ..., ip: _Optional[str] = ...) -> None: ... class ValidateSessionTokenResponse(_message.Message): __slots__ = ("username", "userId", "enterpriseUserId", "status", "statusMessage", "mcEnterpriseIds", "hasMSPPermission", "deletedMcEnterpriseIds") @@ -117,7 +141,7 @@ class ValidateSessionTokenResponse(_message.Message): mcEnterpriseIds: _containers.RepeatedScalarFieldContainer[int] hasMSPPermission: bool deletedMcEnterpriseIds: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, username: _Optional[str] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., status: _Optional[_Union[ValidateSessionTokenResponse.Status, str]] = ..., statusMessage: _Optional[str] = ..., mcEnterpriseIds: _Optional[_Iterable[int]] = ..., hasMSPPermission: bool = ..., deletedMcEnterpriseIds: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., status: _Optional[_Union[ValidateSessionTokenResponse.Status, str]] = ..., statusMessage: _Optional[str] = ..., mcEnterpriseIds: _Optional[_Iterable[int]] = ..., hasMSPPermission: _Optional[bool] = ..., deletedMcEnterpriseIds: _Optional[_Iterable[int]] = ...) -> None: ... class SubscriptionStatusRequest(_message.Message): __slots__ = () @@ -149,7 +173,7 @@ class SubscriptionStatusResponse(_message.Message): gradientLastSyncDate: str gradientNextSyncDate: str isGradientMappingPending: bool - def __init__(self, autoRenewal: _Optional[_Union[AutoRenewal, _Mapping]] = ..., currentPaymentMethod: _Optional[_Union[PaymentMethod, _Mapping]] = ..., checkoutLink: _Optional[str] = ..., licenseCreateDate: _Optional[int] = ..., isDistributor: bool = ..., isLegacyMsp: bool = ..., licenseStats: _Optional[_Iterable[_Union[LicenseStats, _Mapping]]] = ..., gradientStatus: _Optional[_Union[GradientIntegrationStatus, str]] = ..., hideTrialBanner: bool = ..., gradientLastSyncDate: _Optional[str] = ..., gradientNextSyncDate: _Optional[str] = ..., isGradientMappingPending: bool = ...) -> None: ... + def __init__(self, autoRenewal: _Optional[_Union[AutoRenewal, _Mapping]] = ..., currentPaymentMethod: _Optional[_Union[PaymentMethod, _Mapping]] = ..., checkoutLink: _Optional[str] = ..., licenseCreateDate: _Optional[int] = ..., isDistributor: _Optional[bool] = ..., isLegacyMsp: _Optional[bool] = ..., licenseStats: _Optional[_Iterable[_Union[LicenseStats, _Mapping]]] = ..., gradientStatus: _Optional[_Union[GradientIntegrationStatus, str]] = ..., hideTrialBanner: _Optional[bool] = ..., gradientLastSyncDate: _Optional[str] = ..., gradientNextSyncDate: _Optional[str] = ..., isGradientMappingPending: _Optional[bool] = ...) -> None: ... class LicenseStats(_message.Message): __slots__ = ("type", "available", "used") @@ -189,7 +213,7 @@ class AutoRenewal(_message.Message): nextOn: int daysLeft: int isTrial: bool - def __init__(self, nextOn: _Optional[int] = ..., daysLeft: _Optional[int] = ..., isTrial: bool = ...) -> None: ... + def __init__(self, nextOn: _Optional[int] = ..., daysLeft: _Optional[int] = ..., isTrial: _Optional[bool] = ...) -> None: ... class PaymentMethod(_message.Message): __slots__ = ("type", "card", "sepa", "paypal", "failedBilling", "vendor", "purchaseOrder") @@ -248,7 +272,7 @@ class PaymentMethod(_message.Message): failedBilling: bool vendor: PaymentMethod.Vendor purchaseOrder: PaymentMethod.PurchaseOrder - def __init__(self, type: _Optional[_Union[PaymentMethod.Type, str]] = ..., card: _Optional[_Union[PaymentMethod.Card, _Mapping]] = ..., sepa: _Optional[_Union[PaymentMethod.Sepa, _Mapping]] = ..., paypal: _Optional[_Union[PaymentMethod.Paypal, _Mapping]] = ..., failedBilling: bool = ..., vendor: _Optional[_Union[PaymentMethod.Vendor, _Mapping]] = ..., purchaseOrder: _Optional[_Union[PaymentMethod.PurchaseOrder, _Mapping]] = ...) -> None: ... + def __init__(self, type: _Optional[_Union[PaymentMethod.Type, str]] = ..., card: _Optional[_Union[PaymentMethod.Card, _Mapping]] = ..., sepa: _Optional[_Union[PaymentMethod.Sepa, _Mapping]] = ..., paypal: _Optional[_Union[PaymentMethod.Paypal, _Mapping]] = ..., failedBilling: _Optional[bool] = ..., vendor: _Optional[_Union[PaymentMethod.Vendor, _Mapping]] = ..., purchaseOrder: _Optional[_Union[PaymentMethod.PurchaseOrder, _Mapping]] = ...) -> None: ... class SubscriptionMspPricingRequest(_message.Message): __slots__ = () @@ -340,7 +364,7 @@ class InvoiceSearchRequest(_message.Message): size: int startingAfterId: int allInvoicesUnfiltered: bool - def __init__(self, size: _Optional[int] = ..., startingAfterId: _Optional[int] = ..., allInvoicesUnfiltered: bool = ...) -> None: ... + def __init__(self, size: _Optional[int] = ..., startingAfterId: _Optional[int] = ..., allInvoicesUnfiltered: _Optional[bool] = ...) -> None: ... class InvoiceSearchResponse(_message.Message): __slots__ = ("invoices",) @@ -518,7 +542,7 @@ class GradientValidateKeyResponse(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] success: bool message: str - def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... class GradientSaveRequest(_message.Message): __slots__ = ("gradientKey", "enterpriseUserId") @@ -536,7 +560,7 @@ class GradientSaveResponse(_message.Message): success: bool status: GradientIntegrationStatus message: str - def __init__(self, success: bool = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... class GradientRemoveRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -550,7 +574,7 @@ class GradientRemoveResponse(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] success: bool message: str - def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... class GradientSyncRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -566,7 +590,7 @@ class GradientSyncResponse(_message.Message): success: bool status: GradientIntegrationStatus message: str - def __init__(self, success: bool = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... class NetPromoterScoreSurveySubmissionRequest(_message.Message): __slots__ = ("survey_score", "notes") @@ -588,7 +612,7 @@ class NetPromoterScorePopupScheduleResponse(_message.Message): __slots__ = ("show_popup",) SHOW_POPUP_FIELD_NUMBER: _ClassVar[int] show_popup: bool - def __init__(self, show_popup: bool = ...) -> None: ... + def __init__(self, show_popup: _Optional[bool] = ...) -> None: ... class NetPromoterScorePopupDismissalRequest(_message.Message): __slots__ = () @@ -628,6 +652,20 @@ class EventsRequest(_message.Message): event: _containers.RepeatedCompositeFieldContainer[EventRequest] def __init__(self, event: _Optional[_Iterable[_Union[EventRequest, _Mapping]]] = ...) -> None: ... +class EventResponse(_message.Message): + __slots__ = ("index", "status") + INDEX_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + index: int + status: bool + def __init__(self, index: _Optional[int] = ..., status: _Optional[bool] = ...) -> None: ... + +class EventsResponse(_message.Message): + __slots__ = ("response",) + RESPONSE_FIELD_NUMBER: _ClassVar[int] + response: _containers.RepeatedCompositeFieldContainer[EventResponse] + def __init__(self, response: _Optional[_Iterable[_Union[EventResponse, _Mapping]]] = ...) -> None: ... + class CustomerCaptureRequest(_message.Message): __slots__ = ("pageUrl", "tree", "hash", "image", "pageLoadTime", "keyId", "test", "issueType", "notes") PAGEURL_FIELD_NUMBER: _ClassVar[int] @@ -648,7 +686,7 @@ class CustomerCaptureRequest(_message.Message): 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: ... + def __init__(self, pageUrl: _Optional[str] = ..., tree: _Optional[str] = ..., hash: _Optional[str] = ..., image: _Optional[str] = ..., pageLoadTime: _Optional[str] = ..., keyId: _Optional[str] = ..., test: _Optional[bool] = ..., issueType: _Optional[str] = ..., notes: _Optional[str] = ...) -> None: ... class CustomerCaptureResponse(_message.Message): __slots__ = () @@ -672,40 +710,90 @@ class Error(_message.Message): 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") + __slots__ = ("quoteTotal", "includedTax", "includedOtherAddons", "taxAmount", "taxLabel", "purchaseIdentifier") 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] + PURCHASEIDENTIFIER_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: ... + purchaseIdentifier: str + def __init__(self, quoteTotal: _Optional[float] = ..., includedTax: _Optional[bool] = ..., includedOtherAddons: _Optional[bool] = ..., taxAmount: _Optional[float] = ..., taxLabel: _Optional[str] = ..., purchaseIdentifier: _Optional[str] = ...) -> None: ... + +class PurchaseOptions(_message.Message): + __slots__ = ("inConsole", "externalCheckout") + INCONSOLE_FIELD_NUMBER: _ClassVar[int] + EXTERNALCHECKOUT_FIELD_NUMBER: _ClassVar[int] + inConsole: bool + externalCheckout: bool + def __init__(self, inConsole: _Optional[bool] = ..., externalCheckout: _Optional[bool] = ...) -> None: ... + +class AddonPurchaseOptions(_message.Message): + __slots__ = ("storage", "audit", "breachwatch", "chat", "compliance", "professionalServicesSilver", "professionalServicesPlatinum", "pam", "epm", "secretsManager", "connectionManager", "remoteBrowserIsolation") + STORAGE_FIELD_NUMBER: _ClassVar[int] + AUDIT_FIELD_NUMBER: _ClassVar[int] + BREACHWATCH_FIELD_NUMBER: _ClassVar[int] + CHAT_FIELD_NUMBER: _ClassVar[int] + COMPLIANCE_FIELD_NUMBER: _ClassVar[int] + PROFESSIONALSERVICESSILVER_FIELD_NUMBER: _ClassVar[int] + PROFESSIONALSERVICESPLATINUM_FIELD_NUMBER: _ClassVar[int] + PAM_FIELD_NUMBER: _ClassVar[int] + EPM_FIELD_NUMBER: _ClassVar[int] + SECRETSMANAGER_FIELD_NUMBER: _ClassVar[int] + CONNECTIONMANAGER_FIELD_NUMBER: _ClassVar[int] + REMOTEBROWSERISOLATION_FIELD_NUMBER: _ClassVar[int] + storage: PurchaseOptions + audit: PurchaseOptions + breachwatch: PurchaseOptions + chat: PurchaseOptions + compliance: PurchaseOptions + professionalServicesSilver: PurchaseOptions + professionalServicesPlatinum: PurchaseOptions + pam: PurchaseOptions + epm: PurchaseOptions + secretsManager: PurchaseOptions + connectionManager: PurchaseOptions + remoteBrowserIsolation: PurchaseOptions + def __init__(self, storage: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., audit: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., breachwatch: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., chat: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., compliance: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., professionalServicesSilver: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., professionalServicesPlatinum: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., pam: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., epm: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., secretsManager: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., connectionManager: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., remoteBrowserIsolation: _Optional[_Union[PurchaseOptions, _Mapping]] = ...) -> None: ... + +class AvailablePurchaseOptions(_message.Message): + __slots__ = ("basePlan", "users", "addons") + BASEPLAN_FIELD_NUMBER: _ClassVar[int] + USERS_FIELD_NUMBER: _ClassVar[int] + ADDONS_FIELD_NUMBER: _ClassVar[int] + basePlan: PurchaseOptions + users: PurchaseOptions + addons: AddonPurchaseOptions + def __init__(self, basePlan: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., users: _Optional[_Union[PurchaseOptions, _Mapping]] = ..., addons: _Optional[_Union[AddonPurchaseOptions, _Mapping]] = ...) -> None: ... class UpgradeLicenseStatusRequest(_message.Message): __slots__ = () def __init__(self) -> None: ... class UpgradeLicenseStatusResponse(_message.Message): - __slots__ = ("allowPurchaseFromConsole", "checkoutLink", "error") + __slots__ = ("allowPurchaseFromConsole", "purchaseOptions", "error") ALLOWPURCHASEFROMCONSOLE_FIELD_NUMBER: _ClassVar[int] - CHECKOUTLINK_FIELD_NUMBER: _ClassVar[int] + PURCHASEOPTIONS_FIELD_NUMBER: _ClassVar[int] ERROR_FIELD_NUMBER: _ClassVar[int] allowPurchaseFromConsole: bool - checkoutLink: str + purchaseOptions: AvailablePurchaseOptions error: Error - def __init__(self, allowPurchaseFromConsole: bool = ..., checkoutLink: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... + def __init__(self, allowPurchaseFromConsole: _Optional[bool] = ..., purchaseOptions: _Optional[_Union[AvailablePurchaseOptions, _Mapping]] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... class UpgradeLicenseQuotePurchaseRequest(_message.Message): - __slots__ = ("productType", "quantity") + __slots__ = ("productType", "quantity", "tier") PRODUCTTYPE_FIELD_NUMBER: _ClassVar[int] QUANTITY_FIELD_NUMBER: _ClassVar[int] + TIER_FIELD_NUMBER: _ClassVar[int] productType: PurchaseProductType quantity: int - def __init__(self, productType: _Optional[_Union[PurchaseProductType, str]] = ..., quantity: _Optional[int] = ...) -> None: ... + tier: int + def __init__(self, productType: _Optional[_Union[PurchaseProductType, str]] = ..., quantity: _Optional[int] = ..., tier: _Optional[int] = ...) -> None: ... class UpgradeLicenseQuotePurchaseResponse(_message.Message): __slots__ = ("success", "quotePurchase", "viewSummaryLink", "error") @@ -717,17 +805,19 @@ class UpgradeLicenseQuotePurchaseResponse(_message.Message): 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: ... + def __init__(self, success: _Optional[bool] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ..., viewSummaryLink: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... class UpgradeLicenseCompletePurchaseRequest(_message.Message): - __slots__ = ("productType", "quantity", "quotePurchase") + __slots__ = ("productType", "quantity", "quotePurchase", "tier") PRODUCTTYPE_FIELD_NUMBER: _ClassVar[int] QUANTITY_FIELD_NUMBER: _ClassVar[int] QUOTEPURCHASE_FIELD_NUMBER: _ClassVar[int] + TIER_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: ... + tier: int + def __init__(self, productType: _Optional[_Union[PurchaseProductType, str]] = ..., quantity: _Optional[int] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ..., tier: _Optional[int] = ...) -> None: ... class UpgradeLicenseCompletePurchaseResponse(_message.Message): __slots__ = ("success", "invoiceNumber", "error", "quotePurchase") @@ -739,7 +829,7 @@ class UpgradeLicenseCompletePurchaseResponse(_message.Message): 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: ... + def __init__(self, success: _Optional[bool] = ..., invoiceNumber: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ...) -> None: ... class EnterpriseBasePlan(_message.Message): __slots__ = ("baseplanVersion", "cost") @@ -772,3 +862,85 @@ class SubscriptionEnterprisePricingResponse(_message.Message): 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: ... + +class SingularDeviceIdentifier(_message.Message): + __slots__ = ("id", "idType") + ID_FIELD_NUMBER: _ClassVar[int] + IDTYPE_FIELD_NUMBER: _ClassVar[int] + id: str + idType: IdentifierType + def __init__(self, id: _Optional[str] = ..., idType: _Optional[_Union[IdentifierType, str]] = ...) -> None: ... + +class SingularSharedData(_message.Message): + __slots__ = ("platform", "osVersion", "make", "model", "locale", "build", "appIdentifier", "attAuthorizationStatus") + PLATFORM_FIELD_NUMBER: _ClassVar[int] + OSVERSION_FIELD_NUMBER: _ClassVar[int] + MAKE_FIELD_NUMBER: _ClassVar[int] + MODEL_FIELD_NUMBER: _ClassVar[int] + LOCALE_FIELD_NUMBER: _ClassVar[int] + BUILD_FIELD_NUMBER: _ClassVar[int] + APPIDENTIFIER_FIELD_NUMBER: _ClassVar[int] + ATTAUTHORIZATIONSTATUS_FIELD_NUMBER: _ClassVar[int] + platform: str + osVersion: str + make: str + model: str + locale: str + build: str + appIdentifier: str + attAuthorizationStatus: int + def __init__(self, platform: _Optional[str] = ..., osVersion: _Optional[str] = ..., make: _Optional[str] = ..., model: _Optional[str] = ..., locale: _Optional[str] = ..., build: _Optional[str] = ..., appIdentifier: _Optional[str] = ..., attAuthorizationStatus: _Optional[int] = ...) -> None: ... + +class SingularSessionRequest(_message.Message): + __slots__ = ("deviceIdentifiers", "sharedData", "applicationVersion", "install", "installTime", "updateTime", "installSource", "installReceipt", "openuri", "ddlEnabled", "singularLinkResolveRequired", "installRef", "metaRef", "attributionToken") + DEVICEIDENTIFIERS_FIELD_NUMBER: _ClassVar[int] + SHAREDDATA_FIELD_NUMBER: _ClassVar[int] + APPLICATIONVERSION_FIELD_NUMBER: _ClassVar[int] + INSTALL_FIELD_NUMBER: _ClassVar[int] + INSTALLTIME_FIELD_NUMBER: _ClassVar[int] + UPDATETIME_FIELD_NUMBER: _ClassVar[int] + INSTALLSOURCE_FIELD_NUMBER: _ClassVar[int] + INSTALLRECEIPT_FIELD_NUMBER: _ClassVar[int] + OPENURI_FIELD_NUMBER: _ClassVar[int] + DDLENABLED_FIELD_NUMBER: _ClassVar[int] + SINGULARLINKRESOLVEREQUIRED_FIELD_NUMBER: _ClassVar[int] + INSTALLREF_FIELD_NUMBER: _ClassVar[int] + METAREF_FIELD_NUMBER: _ClassVar[int] + ATTRIBUTIONTOKEN_FIELD_NUMBER: _ClassVar[int] + deviceIdentifiers: _containers.RepeatedCompositeFieldContainer[SingularDeviceIdentifier] + sharedData: SingularSharedData + applicationVersion: str + install: bool + installTime: int + updateTime: int + installSource: str + installReceipt: str + openuri: str + ddlEnabled: bool + singularLinkResolveRequired: bool + installRef: str + metaRef: str + attributionToken: str + def __init__(self, deviceIdentifiers: _Optional[_Iterable[_Union[SingularDeviceIdentifier, _Mapping]]] = ..., sharedData: _Optional[_Union[SingularSharedData, _Mapping]] = ..., applicationVersion: _Optional[str] = ..., install: _Optional[bool] = ..., installTime: _Optional[int] = ..., updateTime: _Optional[int] = ..., installSource: _Optional[str] = ..., installReceipt: _Optional[str] = ..., openuri: _Optional[str] = ..., ddlEnabled: _Optional[bool] = ..., singularLinkResolveRequired: _Optional[bool] = ..., installRef: _Optional[str] = ..., metaRef: _Optional[str] = ..., attributionToken: _Optional[str] = ...) -> None: ... + +class SingularEventRequest(_message.Message): + __slots__ = ("deviceIdentifiers", "sharedData", "eventName") + DEVICEIDENTIFIERS_FIELD_NUMBER: _ClassVar[int] + SHAREDDATA_FIELD_NUMBER: _ClassVar[int] + EVENTNAME_FIELD_NUMBER: _ClassVar[int] + deviceIdentifiers: _containers.RepeatedCompositeFieldContainer[SingularDeviceIdentifier] + sharedData: SingularSharedData + eventName: str + def __init__(self, deviceIdentifiers: _Optional[_Iterable[_Union[SingularDeviceIdentifier, _Mapping]]] = ..., sharedData: _Optional[_Union[SingularSharedData, _Mapping]] = ..., eventName: _Optional[str] = ...) -> None: ... + +class ActivePamCountRequest(_message.Message): + __slots__ = ("enterpriseId",) + ENTERPRISEID_FIELD_NUMBER: _ClassVar[int] + enterpriseId: int + def __init__(self, enterpriseId: _Optional[int] = ...) -> None: ... + +class ActivePamCountResponse(_message.Message): + __slots__ = ("pamCount",) + PAMCOUNT_FIELD_NUMBER: _ClassVar[int] + pamCount: int + def __init__(self, pamCount: _Optional[int] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py index 1599095b..e05a8dc1 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: GraphSync.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'GraphSync.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi index 594a4eec..26c41dc3 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi @@ -2,7 +2,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 @@ -137,7 +138,7 @@ class GraphSyncResult(_message.Message): syncPoint: int data: _containers.RepeatedCompositeFieldContainer[GraphSyncDataPlus] hasMore: bool - def __init__(self, streamId: _Optional[bytes] = ..., syncPoint: _Optional[int] = ..., data: _Optional[_Iterable[_Union[GraphSyncDataPlus, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... + def __init__(self, streamId: _Optional[bytes] = ..., syncPoint: _Optional[int] = ..., data: _Optional[_Iterable[_Union[GraphSyncDataPlus, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... class GraphSyncMultiQuery(_message.Message): __slots__ = ("queries",) diff --git a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py index 40ab71b1..b10a1c8e 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: NotificationCenter.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'NotificationCenter.proto' ) @@ -25,7 +25,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\"\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\"e\n(NotificationsApprovalStatusUpdateRequest\x12\x39\n\x07updates\x18\x01 \x03(\x0b\x32(.NotificationCenter.ApprovalStatusUpdate*\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*\xb7\x03\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\x12\x1f\n\x1bNT_DEVICE_APPROVAL_APPROVED\x10\x0e\x12\x1d\n\x19NT_DEVICE_APPROVAL_DENIED\x10\x0f\x12\x15\n\x11NT_ACCOUNT_CREATE\x10\x10*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\"2\n\x15NotificationParameter\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xa1\x03\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\x12=\n\nparameters\x18\x08 \x03(\x0b\x32).NotificationCenter.NotificationParameter\"\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\"\xd6\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\x12\x1a\n\rpredefinedUid\x18\x05 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_predefinedUid\"^\n\x18NotificationsSendRequest\x12\x42\n\rnotifications\x18\x01 \x03(\x0b\x32+.NotificationCenter.NotificationSendRequest\",\n\x17NotificationSyncRequest\x12\x11\n\tsyncPoint\x18\x01 \x01(\x03\"9\n\x10SentNotification\x12\x0c\n\x04user\x18\x01 \x01(\x05\x12\x17\n\x0fnotificationUid\x18\x02 \x01(\x0c\"\xa7\x01\n(NotificationsApprovalStatusUpdateRequest\x12>\n\x06status\x18\x01 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\x12;\n\rnotifications\x18\x02 \x03(\x0b\x32$.NotificationCenter.SentNotification*\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*\x9e\x04\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\x12\x1f\n\x1bNT_DEVICE_APPROVAL_APPROVED\x10\x0e\x12\x1d\n\x19NT_DEVICE_APPROVAL_DENIED\x10\x0f\x12\x16\n\x12NT_ACCOUNT_CREATED\x10\x10\x12\x12\n\x0eNT_2FA_ENABLED\x10\x11\x12\x13\n\x0fNT_2FA_DISABLED\x10\x12\x12\x1c\n\x18NT_SECURITY_KEYS_ENABLED\x10\x13\x12\x1d\n\x19NT_SECURITY_KEYS_DISABLED\x10\x14*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) @@ -35,38 +35,42 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\022NotificationCenter' _globals['_NOTIFICATION'].fields_by_name['category']._loaded_options = None _globals['_NOTIFICATION'].fields_by_name['category']._serialized_options = b'\030\001' - _globals['_NOTIFICATIONCATEGORY']._serialized_start=1876 - _globals['_NOTIFICATIONCATEGORY']._serialized_end=2035 - _globals['_NOTIFICATIONTYPE']._serialized_start=2038 - _globals['_NOTIFICATIONTYPE']._serialized_end=2477 - _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2479 - _globals['_NOTIFICATIONREADSTATUS']._serialized_end=2568 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=2571 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=2705 + _globals['_NOTIFICATIONCATEGORY']._serialized_start=2163 + _globals['_NOTIFICATIONCATEGORY']._serialized_end=2322 + _globals['_NOTIFICATIONTYPE']._serialized_start=2325 + _globals['_NOTIFICATIONTYPE']._serialized_end=2867 + _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2869 + _globals['_NOTIFICATIONREADSTATUS']._serialized_end=2958 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=2961 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=3095 _globals['_ENCRYPTEDDATA']._serialized_start=65 _globals['_ENCRYPTEDDATA']._serialized_end=111 - _globals['_NOTIFICATION']._serialized_start=114 - _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 - _globals['_NOTIFICATIONSAPPROVALSTATUSUPDATEREQUEST']._serialized_start=1772 - _globals['_NOTIFICATIONSAPPROVALSTATUSUPDATEREQUEST']._serialized_end=1873 + _globals['_NOTIFICATIONPARAMETER']._serialized_start=113 + _globals['_NOTIFICATIONPARAMETER']._serialized_end=163 + _globals['_NOTIFICATION']._serialized_start=166 + _globals['_NOTIFICATION']._serialized_end=583 + _globals['_NOTIFICATIONREADMARK']._serialized_start=586 + _globals['_NOTIFICATIONREADMARK']._serialized_end=737 + _globals['_NOTIFICATIONCONTENT']._serialized_start=740 + _globals['_NOTIFICATIONCONTENT']._serialized_end=1034 + _globals['_NOTIFICATIONWRAPPER']._serialized_start=1036 + _globals['_NOTIFICATIONWRAPPER']._serialized_end=1147 + _globals['_NOTIFICATIONSYNC']._serialized_start=1149 + _globals['_NOTIFICATIONSYNC']._serialized_end=1258 + _globals['_READSTATUSUPDATE']._serialized_start=1260 + _globals['_READSTATUSUPDATE']._serialized_end=1363 + _globals['_APPROVALSTATUSUPDATE']._serialized_start=1365 + _globals['_APPROVALSTATUSUPDATE']._serialized_end=1476 + _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_start=1478 + _globals['_PROCESSMARKREADEVENTSREQUEST']._serialized_end=1572 + _globals['_NOTIFICATIONSENDREQUEST']._serialized_start=1575 + _globals['_NOTIFICATIONSENDREQUEST']._serialized_end=1789 + _globals['_NOTIFICATIONSSENDREQUEST']._serialized_start=1791 + _globals['_NOTIFICATIONSSENDREQUEST']._serialized_end=1885 + _globals['_NOTIFICATIONSYNCREQUEST']._serialized_start=1887 + _globals['_NOTIFICATIONSYNCREQUEST']._serialized_end=1931 + _globals['_SENTNOTIFICATION']._serialized_start=1933 + _globals['_SENTNOTIFICATION']._serialized_end=1990 + _globals['_NOTIFICATIONSAPPROVALSTATUSUPDATEREQUEST']._serialized_start=1993 + _globals['_NOTIFICATIONSAPPROVALSTATUSUPDATEREQUEST']._serialized_end=2160 # @@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 fd5d287d..dfcecea3 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi @@ -3,7 +3,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 @@ -36,7 +37,11 @@ class NotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NT_SHARE_APPROVAL_DENIED: _ClassVar[NotificationType] NT_DEVICE_APPROVAL_APPROVED: _ClassVar[NotificationType] NT_DEVICE_APPROVAL_DENIED: _ClassVar[NotificationType] - NT_ACCOUNT_CREATE: _ClassVar[NotificationType] + NT_ACCOUNT_CREATED: _ClassVar[NotificationType] + NT_2FA_ENABLED: _ClassVar[NotificationType] + NT_2FA_DISABLED: _ClassVar[NotificationType] + NT_SECURITY_KEYS_ENABLED: _ClassVar[NotificationType] + NT_SECURITY_KEYS_DISABLED: _ClassVar[NotificationType] class NotificationReadStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -76,7 +81,11 @@ NT_2FA_CONFIGURED: NotificationType NT_SHARE_APPROVAL_DENIED: NotificationType NT_DEVICE_APPROVAL_APPROVED: NotificationType NT_DEVICE_APPROVAL_DENIED: NotificationType -NT_ACCOUNT_CREATE: NotificationType +NT_ACCOUNT_CREATED: NotificationType +NT_2FA_ENABLED: NotificationType +NT_2FA_DISABLED: NotificationType +NT_SECURITY_KEYS_ENABLED: NotificationType +NT_SECURITY_KEYS_DISABLED: NotificationType NRS_UNSPECIFIED: NotificationReadStatus NRS_LAST: NotificationReadStatus NRS_READ: NotificationReadStatus @@ -95,8 +104,16 @@ class EncryptedData(_message.Message): data: bytes def __init__(self, version: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... +class NotificationParameter(_message.Message): + __slots__ = ("key", "data") + KEY_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + key: str + data: bytes + def __init__(self, key: _Optional[str] = ..., data: _Optional[bytes] = ...) -> None: ... + class Notification(_message.Message): - __slots__ = ("type", "category", "sender", "senderFullName", "encryptedData", "refs", "categories") + __slots__ = ("type", "category", "sender", "senderFullName", "encryptedData", "refs", "categories", "parameters") TYPE_FIELD_NUMBER: _ClassVar[int] CATEGORY_FIELD_NUMBER: _ClassVar[int] SENDER_FIELD_NUMBER: _ClassVar[int] @@ -104,6 +121,7 @@ class Notification(_message.Message): ENCRYPTEDDATA_FIELD_NUMBER: _ClassVar[int] REFS_FIELD_NUMBER: _ClassVar[int] CATEGORIES_FIELD_NUMBER: _ClassVar[int] + PARAMETERS_FIELD_NUMBER: _ClassVar[int] type: NotificationType category: NotificationCategory sender: _GraphSync_pb2.GraphSyncRef @@ -111,7 +129,8 @@ class Notification(_message.Message): encryptedData: EncryptedData refs: _containers.RepeatedCompositeFieldContainer[_GraphSync_pb2.GraphSyncRef] 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: ... + parameters: _containers.RepeatedCompositeFieldContainer[NotificationParameter] + 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]]] = ..., parameters: _Optional[_Iterable[_Union[NotificationParameter, _Mapping]]] = ...) -> None: ... class NotificationReadMark(_message.Message): __slots__ = ("uid", "notification_edge_id", "mark_edge_id", "readStatus") @@ -139,7 +158,7 @@ class NotificationContent(_message.Message): 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]] = ..., trimmingPoint: bool = ..., 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: _Optional[bool] = ..., clientTypeIDs: _Optional[_Iterable[int]] = ..., deviceIDs: _Optional[_Iterable[int]] = ...) -> None: ... class NotificationWrapper(_message.Message): __slots__ = ("uid", "content", "timestamp") @@ -159,7 +178,7 @@ class NotificationSync(_message.Message): data: _containers.RepeatedCompositeFieldContainer[NotificationWrapper] syncPoint: int hasMore: bool - def __init__(self, data: _Optional[_Iterable[_Union[NotificationWrapper, _Mapping]]] = ..., syncPoint: _Optional[int] = ..., hasMore: bool = ...) -> None: ... + def __init__(self, data: _Optional[_Iterable[_Union[NotificationWrapper, _Mapping]]] = ..., syncPoint: _Optional[int] = ..., hasMore: _Optional[bool] = ...) -> None: ... class ReadStatusUpdate(_message.Message): __slots__ = ("notificationUid", "status") @@ -184,16 +203,18 @@ class ProcessMarkReadEventsRequest(_message.Message): def __init__(self, readStatusUpdate: _Optional[_Iterable[_Union[ReadStatusUpdate, _Mapping]]] = ...) -> None: ... class NotificationSendRequest(_message.Message): - __slots__ = ("recipients", "notification", "clientTypeIDs", "deviceIDs") + __slots__ = ("recipients", "notification", "clientTypeIDs", "deviceIDs", "predefinedUid") RECIPIENTS_FIELD_NUMBER: _ClassVar[int] NOTIFICATION_FIELD_NUMBER: _ClassVar[int] CLIENTTYPEIDS_FIELD_NUMBER: _ClassVar[int] DEVICEIDS_FIELD_NUMBER: _ClassVar[int] + PREDEFINEDUID_FIELD_NUMBER: _ClassVar[int] recipients: _containers.RepeatedCompositeFieldContainer[_GraphSync_pb2.GraphSyncRef] notification: Notification clientTypeIDs: _containers.RepeatedScalarFieldContainer[int] deviceIDs: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, recipients: _Optional[_Iterable[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]]] = ..., notification: _Optional[_Union[Notification, _Mapping]] = ..., clientTypeIDs: _Optional[_Iterable[int]] = ..., deviceIDs: _Optional[_Iterable[int]] = ...) -> None: ... + predefinedUid: bytes + def __init__(self, recipients: _Optional[_Iterable[_Union[_GraphSync_pb2.GraphSyncRef, _Mapping]]] = ..., notification: _Optional[_Union[Notification, _Mapping]] = ..., clientTypeIDs: _Optional[_Iterable[int]] = ..., deviceIDs: _Optional[_Iterable[int]] = ..., predefinedUid: _Optional[bytes] = ...) -> None: ... class NotificationsSendRequest(_message.Message): __slots__ = ("notifications",) @@ -207,8 +228,18 @@ class NotificationSyncRequest(_message.Message): syncPoint: int def __init__(self, syncPoint: _Optional[int] = ...) -> None: ... +class SentNotification(_message.Message): + __slots__ = ("user", "notificationUid") + USER_FIELD_NUMBER: _ClassVar[int] + NOTIFICATIONUID_FIELD_NUMBER: _ClassVar[int] + user: int + notificationUid: bytes + def __init__(self, user: _Optional[int] = ..., notificationUid: _Optional[bytes] = ...) -> None: ... + class NotificationsApprovalStatusUpdateRequest(_message.Message): - __slots__ = ("updates",) - UPDATES_FIELD_NUMBER: _ClassVar[int] - updates: _containers.RepeatedCompositeFieldContainer[ApprovalStatusUpdate] - def __init__(self, updates: _Optional[_Iterable[_Union[ApprovalStatusUpdate, _Mapping]]] = ...) -> None: ... + __slots__ = ("status", "notifications") + STATUS_FIELD_NUMBER: _ClassVar[int] + NOTIFICATIONS_FIELD_NUMBER: _ClassVar[int] + status: NotificationApprovalStatus + notifications: _containers.RepeatedCompositeFieldContainer[SentNotification] + def __init__(self, status: _Optional[_Union[NotificationApprovalStatus, str]] = ..., notifications: _Optional[_Iterable[_Union[SentNotification, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py index 8887cf3a..a1d6e25b 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: SyncDown.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'SyncDown.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi index 3a60cf8b..933b685d 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi @@ -7,7 +7,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 @@ -133,7 +134,7 @@ class SyncDownResponse(_message.Message): removedUsers: _containers.RepeatedScalarFieldContainer[bytes] securityScoreData: _containers.RepeatedCompositeFieldContainer[SecurityScoreData] notificationSync: _containers.RepeatedCompositeFieldContainer[_NotificationCenter_pb2.NotificationWrapper] - def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., userFolders: _Optional[_Iterable[_Union[UserFolder, _Mapping]]] = ..., sharedFolders: _Optional[_Iterable[_Union[SharedFolder, _Mapping]]] = ..., userFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., sharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., records: _Optional[_Iterable[_Union[Record, _Mapping]]] = ..., recordMetaData: _Optional[_Iterable[_Union[RecordMetaData, _Mapping]]] = ..., nonSharedData: _Optional[_Iterable[_Union[NonSharedData, _Mapping]]] = ..., recordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., userFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., sharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., sharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., sharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., sharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., recordAddAuditData: _Optional[_Iterable[bytes]] = ..., teams: _Optional[_Iterable[_Union[Team, _Mapping]]] = ..., sharingChanges: _Optional[_Iterable[_Union[SharingChange, _Mapping]]] = ..., profile: _Optional[_Union[Profile, _Mapping]] = ..., profilePic: _Optional[_Union[ProfilePic, _Mapping]] = ..., pendingTeamMembers: _Optional[_Iterable[_Union[PendingTeamMember, _Mapping]]] = ..., breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., userAuths: _Optional[_Iterable[_Union[UserAuth, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., reusedPasswords: _Optional[_Union[ReusedPasswords, _Mapping]] = ..., removedUserFolders: _Optional[_Iterable[bytes]] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., removedUserFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., removedSharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., removedRecords: _Optional[_Iterable[bytes]] = ..., removedRecordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., removedUserFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., removedSharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., removedSharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., removedSharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., removedSharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., removedTeams: _Optional[_Iterable[bytes]] = ..., ksmAppShares: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., ksmAppClients: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., shareInvitations: _Optional[_Iterable[_Union[ShareInvitation, _Mapping]]] = ..., diagnostics: _Optional[_Union[SyncDiagnostics, _Mapping]] = ..., recordRotations: _Optional[_Iterable[_Union[RecordRotation, _Mapping]]] = ..., users: _Optional[_Iterable[_Union[User, _Mapping]]] = ..., removedUsers: _Optional[_Iterable[bytes]] = ..., securityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ..., notificationSync: _Optional[_Iterable[_Union[_NotificationCenter_pb2.NotificationWrapper, _Mapping]]] = ...) -> None: ... + def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., userFolders: _Optional[_Iterable[_Union[UserFolder, _Mapping]]] = ..., sharedFolders: _Optional[_Iterable[_Union[SharedFolder, _Mapping]]] = ..., userFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., sharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., records: _Optional[_Iterable[_Union[Record, _Mapping]]] = ..., recordMetaData: _Optional[_Iterable[_Union[RecordMetaData, _Mapping]]] = ..., nonSharedData: _Optional[_Iterable[_Union[NonSharedData, _Mapping]]] = ..., recordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., userFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., sharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., sharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., sharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., sharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., recordAddAuditData: _Optional[_Iterable[bytes]] = ..., teams: _Optional[_Iterable[_Union[Team, _Mapping]]] = ..., sharingChanges: _Optional[_Iterable[_Union[SharingChange, _Mapping]]] = ..., profile: _Optional[_Union[Profile, _Mapping]] = ..., profilePic: _Optional[_Union[ProfilePic, _Mapping]] = ..., pendingTeamMembers: _Optional[_Iterable[_Union[PendingTeamMember, _Mapping]]] = ..., breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., userAuths: _Optional[_Iterable[_Union[UserAuth, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., reusedPasswords: _Optional[_Union[ReusedPasswords, _Mapping]] = ..., removedUserFolders: _Optional[_Iterable[bytes]] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., removedUserFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., removedSharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., removedRecords: _Optional[_Iterable[bytes]] = ..., removedRecordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., removedUserFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., removedSharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., removedSharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., removedSharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., removedSharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., removedTeams: _Optional[_Iterable[bytes]] = ..., ksmAppShares: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., ksmAppClients: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., shareInvitations: _Optional[_Iterable[_Union[ShareInvitation, _Mapping]]] = ..., diagnostics: _Optional[_Union[SyncDiagnostics, _Mapping]] = ..., recordRotations: _Optional[_Iterable[_Union[RecordRotation, _Mapping]]] = ..., users: _Optional[_Iterable[_Union[User, _Mapping]]] = ..., removedUsers: _Optional[_Iterable[bytes]] = ..., securityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ..., notificationSync: _Optional[_Iterable[_Union[_NotificationCenter_pb2.NotificationWrapper, _Mapping]]] = ...) -> None: ... class UserFolder(_message.Message): __slots__ = ("folderUid", "parentUid", "userFolderKey", "keyType", "revision", "data") @@ -179,7 +180,7 @@ class SharedFolder(_message.Message): owner: str ownerAccountUid: bytes name: bytes - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., sharedFolderKey: _Optional[bytes] = ..., keyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., data: _Optional[bytes] = ..., defaultManageRecords: bool = ..., defaultManageUsers: bool = ..., defaultCanEdit: bool = ..., defaultCanReshare: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., owner: _Optional[str] = ..., ownerAccountUid: _Optional[bytes] = ..., name: _Optional[bytes] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., sharedFolderKey: _Optional[bytes] = ..., keyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., data: _Optional[bytes] = ..., defaultManageRecords: _Optional[bool] = ..., defaultManageUsers: _Optional[bool] = ..., defaultCanEdit: _Optional[bool] = ..., defaultCanReshare: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., owner: _Optional[str] = ..., ownerAccountUid: _Optional[bytes] = ..., name: _Optional[bytes] = ...) -> None: ... class UserFolderSharedFolder(_message.Message): __slots__ = ("folderUid", "sharedFolderUid", "revision") @@ -245,7 +246,7 @@ class Team(_message.Message): sharedFolderKeys: _containers.RepeatedCompositeFieldContainer[SharedFolderKey] teamEccPrivateKey: bytes teamEccPublicKey: bytes - def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., teamKey: _Optional[bytes] = ..., teamKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., teamPrivateKey: _Optional[bytes] = ..., restrictEdit: bool = ..., restrictShare: bool = ..., restrictView: bool = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., sharedFolderKeys: _Optional[_Iterable[_Union[SharedFolderKey, _Mapping]]] = ..., teamEccPrivateKey: _Optional[bytes] = ..., teamEccPublicKey: _Optional[bytes] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., teamKey: _Optional[bytes] = ..., teamKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., teamPrivateKey: _Optional[bytes] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ..., restrictView: _Optional[bool] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., sharedFolderKeys: _Optional[_Iterable[_Union[SharedFolderKey, _Mapping]]] = ..., teamEccPrivateKey: _Optional[bytes] = ..., teamEccPublicKey: _Optional[bytes] = ...) -> None: ... class Record(_message.Message): __slots__ = ("recordUid", "revision", "version", "shared", "clientModifiedTime", "data", "extra", "udata", "fileSize", "thumbnailSize") @@ -269,7 +270,7 @@ class Record(_message.Message): udata: str fileSize: int thumbnailSize: int - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: bool = ..., clientModifiedTime: _Optional[int] = ..., data: _Optional[bytes] = ..., extra: _Optional[bytes] = ..., udata: _Optional[str] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: _Optional[bool] = ..., clientModifiedTime: _Optional[int] = ..., data: _Optional[bytes] = ..., extra: _Optional[bytes] = ..., udata: _Optional[str] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ...) -> None: ... class RecordLink(_message.Message): __slots__ = ("parentRecordUid", "childRecordUid", "recordKey", "revision") @@ -335,7 +336,7 @@ class RecordMetaData(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType ownerUsername: str - def __init__(self, recordUid: _Optional[bytes] = ..., owner: bool = ..., recordKey: _Optional[bytes] = ..., recordKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., canShare: bool = ..., canEdit: bool = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., owner: _Optional[bool] = ..., recordKey: _Optional[bytes] = ..., recordKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., canShare: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ...) -> None: ... class SharingChange(_message.Message): __slots__ = ("recordUid", "shared") @@ -343,7 +344,7 @@ class SharingChange(_message.Message): SHARED_FIELD_NUMBER: _ClassVar[int] recordUid: bytes shared: bool - def __init__(self, recordUid: _Optional[bytes] = ..., shared: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., shared: _Optional[bool] = ...) -> None: ... class Profile(_message.Message): __slots__ = ("data", "profileName", "revision") @@ -409,7 +410,7 @@ class UserAuth(_message.Message): encryptedClientKey: bytes revision: int name: str - def __init__(self, uid: _Optional[bytes] = ..., loginType: _Optional[_Union[_APIRequest_pb2.LoginType, str]] = ..., deleted: bool = ..., iterations: _Optional[int] = ..., salt: _Optional[bytes] = ..., encryptedClientKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., name: _Optional[str] = ...) -> None: ... + def __init__(self, uid: _Optional[bytes] = ..., loginType: _Optional[_Union[_APIRequest_pb2.LoginType, str]] = ..., deleted: _Optional[bool] = ..., iterations: _Optional[int] = ..., salt: _Optional[bytes] = ..., encryptedClientKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., name: _Optional[str] = ...) -> None: ... class BreachWatchSecurityData(_message.Message): __slots__ = ("recordUid", "revision", "removed") @@ -419,7 +420,7 @@ class BreachWatchSecurityData(_message.Message): recordUid: bytes revision: int removed: bool - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., removed: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., removed: _Optional[bool] = ...) -> None: ... class ReusedPasswords(_message.Message): __slots__ = ("count", "revision") @@ -453,7 +454,7 @@ class SharedFolderRecord(_message.Message): expirationNotificationType: _record_pb2.TimerNotificationType ownerUsername: str rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., canShare: bool = ..., canEdit: bool = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., owner: bool = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., canShare: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., owner: _Optional[bool] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderUser(_message.Message): __slots__ = ("sharedFolderUid", "username", "manageRecords", "manageUsers", "accountUid", "expiration", "expirationNotificationType", "rotateOnExpiration") @@ -473,7 +474,7 @@ class SharedFolderUser(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., username: _Optional[str] = ..., manageRecords: bool = ..., manageUsers: bool = ..., accountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., username: _Optional[str] = ..., manageRecords: _Optional[bool] = ..., manageUsers: _Optional[bool] = ..., accountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderTeam(_message.Message): __slots__ = ("sharedFolderUid", "teamUid", "name", "manageRecords", "manageUsers", "expiration", "expirationNotificationType", "rotateOnExpiration") @@ -493,7 +494,7 @@ class SharedFolderTeam(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., manageRecords: bool = ..., manageUsers: bool = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., manageRecords: _Optional[bool] = ..., manageUsers: _Optional[bool] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class KsmChange(_message.Message): __slots__ = ("appRecordUid", "detailId", "removed", "appClientType", "expiration") @@ -507,7 +508,7 @@ class KsmChange(_message.Message): removed: bool appClientType: _enterprise_pb2.AppClientType expiration: int - def __init__(self, appRecordUid: _Optional[bytes] = ..., detailId: _Optional[bytes] = ..., removed: bool = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., expiration: _Optional[int] = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., detailId: _Optional[bytes] = ..., removed: _Optional[bool] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., expiration: _Optional[int] = ...) -> None: ... class ShareInvitation(_message.Message): __slots__ = ("username",) @@ -557,7 +558,7 @@ class RecordRotation(_message.Message): resourceUid: bytes lastRotation: int lastRotationStatus: RecordRotationStatus - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: bool = ..., resourceUid: _Optional[bytes] = ..., lastRotation: _Optional[int] = ..., lastRotationStatus: _Optional[_Union[RecordRotationStatus, str]] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., resourceUid: _Optional[bytes] = ..., lastRotation: _Optional[int] = ..., lastRotationStatus: _Optional[_Union[RecordRotationStatus, str]] = ...) -> None: ... class SecurityScoreData(_message.Message): __slots__ = ("recordUid", "data", "revision") diff --git a/keepersdk-package/src/keepersdk/proto/automator_pb2.py b/keepersdk-package/src/keepersdk/proto/automator_pb2.py index 8258c6e9..b17ba298 100644 --- a/keepersdk-package/src/keepersdk/proto/automator_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/automator_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: automator.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'automator.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -19,7 +27,7 @@ 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') +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\"\x8f\x04\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\x42\x02\x18\x01\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\x12\x39\n\x12sslCertificateInfo\x18\x0f \x03(\x0b\x32\x1d.Automator.SSLCertificateInfo\" \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\"\x93\x01\n\x12SSLCertificateInfo\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x04\x12\x0f\n\x07hostUrl\x18\x02 \x01(\t\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\x0e\n\x06issuer\x18\x04 \x01(\t\x12\x10\n\x08issuedOn\x18\x05 \x01(\t\x12\x11\n\texpiresOn\x18\x06 \x01(\t\x12\x11\n\tcheckedOn\x18\x07 \x01(\t*I\n\x1dSsoAuthenticationProtocolType\x12\x14\n\x10UNKNOWN_PROTOCOL\x10\x00\x12\t\n\x05SAML2\x10\x01\x12\x07\n\x03JWT\x10\x02*<\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') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -27,14 +35,16 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\tAutomator' - _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_start=7193 - _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_end=7257 - _globals['_CERTIFICATEFORMAT']._serialized_start=7259 - _globals['_CERTIFICATEFORMAT']._serialized_end=7319 - _globals['_SKILLTYPE']._serialized_start=7321 - _globals['_SKILLTYPE']._serialized_end=7424 - _globals['_AUTOMATORSTATE']._serialized_start=7427 - _globals['_AUTOMATORSTATE']._serialized_end=7562 + _globals['_STATUSRESPONSE'].fields_by_name['sslCertificateExpiration']._loaded_options = None + _globals['_STATUSRESPONSE'].fields_by_name['sslCertificateExpiration']._serialized_options = b'\030\001' + _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_start=7406 + _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_end=7479 + _globals['_CERTIFICATEFORMAT']._serialized_start=7481 + _globals['_CERTIFICATEFORMAT']._serialized_end=7541 + _globals['_SKILLTYPE']._serialized_start=7543 + _globals['_SKILLTYPE']._serialized_end=7646 + _globals['_AUTOMATORSTATE']._serialized_start=7649 + _globals['_AUTOMATORSTATE']._serialized_end=7784 _globals['_AUTOMATORSETTINGVALUE']._serialized_start=80 _globals['_AUTOMATORSETTINGVALUE']._serialized_end=399 _globals['_APPROVEDEVICEREQUEST']._serialized_start=402 @@ -52,59 +62,61 @@ _globals['_APPROVEDEVICERESPONSE']._serialized_start=2557 _globals['_APPROVEDEVICERESPONSE']._serialized_end=2709 _globals['_STATUSRESPONSE']._serialized_start=2712 - _globals['_STATUSRESPONSE']._serialized_end=3176 - _globals['_ERRORRESPONSE']._serialized_start=3178 - _globals['_ERRORRESPONSE']._serialized_end=3210 - _globals['_LOGENTRY']._serialized_start=3212 - _globals['_LOGENTRY']._serialized_end=3300 - _globals['_ADMINRESPONSE']._serialized_start=3302 - _globals['_ADMINRESPONSE']._serialized_end=3400 - _globals['_AUTOMATORINFO']._serialized_start=3403 - _globals['_AUTOMATORINFO']._serialized_end=3769 - _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_start=3771 - _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_end=3872 - _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_start=3874 - _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_end=3924 - _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_start=3926 - _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_end=3975 - _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_start=3977 - _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_end=4039 - _globals['_ADMINGETAUTOMATORREQUEST']._serialized_start=4041 - _globals['_ADMINGETAUTOMATORREQUEST']._serialized_end=4088 - _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_start=4090 - _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_end=4157 - _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_start=4160 - _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_end=4360 - _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_start=4363 - _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_end=4615 - _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_start=4618 - _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_end=4784 - _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_start=4786 - _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_end=4836 - _globals['_AUTOMATORSKILL']._serialized_start=4838 - _globals['_AUTOMATORSKILL']._serialized_end=4933 - _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_start=4935 - _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_end=5051 - _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_start=5053 - _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_end=5102 - _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_start=5104 - _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_end=5158 - _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_start=5160 - _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_end=5207 - _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_start=5209 - _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_end=5261 - _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_start=5264 - _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_end=5619 - _globals['_TEAMDESCRIPTION']._serialized_start=5622 - _globals['_TEAMDESCRIPTION']._serialized_end=5760 - _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_start=5763 - _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_end=5916 - _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_start=5919 - _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_end=6218 - _globals['_APPROVETEAMSREQUEST']._serialized_start=6221 - _globals['_APPROVETEAMSREQUEST']._serialized_end=6520 - _globals['_APPROVETEAMSRESPONSE']._serialized_start=6522 - _globals['_APPROVETEAMSRESPONSE']._serialized_end=6646 - _globals['_APPROVEONETEAMRESPONSE']._serialized_start=6649 - _globals['_APPROVEONETEAMRESPONSE']._serialized_end=7191 + _globals['_STATUSRESPONSE']._serialized_end=3239 + _globals['_ERRORRESPONSE']._serialized_start=3241 + _globals['_ERRORRESPONSE']._serialized_end=3273 + _globals['_LOGENTRY']._serialized_start=3275 + _globals['_LOGENTRY']._serialized_end=3363 + _globals['_ADMINRESPONSE']._serialized_start=3365 + _globals['_ADMINRESPONSE']._serialized_end=3463 + _globals['_AUTOMATORINFO']._serialized_start=3466 + _globals['_AUTOMATORINFO']._serialized_end=3832 + _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_start=3834 + _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_end=3935 + _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_start=3937 + _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_end=3987 + _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_start=3989 + _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_end=4038 + _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_start=4040 + _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_end=4102 + _globals['_ADMINGETAUTOMATORREQUEST']._serialized_start=4104 + _globals['_ADMINGETAUTOMATORREQUEST']._serialized_end=4151 + _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_start=4153 + _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_end=4220 + _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_start=4223 + _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_end=4423 + _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_start=4426 + _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_end=4678 + _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_start=4681 + _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_end=4847 + _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_start=4849 + _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_end=4899 + _globals['_AUTOMATORSKILL']._serialized_start=4901 + _globals['_AUTOMATORSKILL']._serialized_end=4996 + _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_start=4998 + _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_end=5114 + _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_start=5116 + _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_end=5165 + _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_start=5167 + _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_end=5221 + _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_start=5223 + _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_end=5270 + _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_start=5272 + _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_end=5324 + _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_start=5327 + _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_end=5682 + _globals['_TEAMDESCRIPTION']._serialized_start=5685 + _globals['_TEAMDESCRIPTION']._serialized_end=5823 + _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_start=5826 + _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_end=5979 + _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_start=5982 + _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_end=6281 + _globals['_APPROVETEAMSREQUEST']._serialized_start=6284 + _globals['_APPROVETEAMSREQUEST']._serialized_end=6583 + _globals['_APPROVETEAMSRESPONSE']._serialized_start=6585 + _globals['_APPROVETEAMSRESPONSE']._serialized_end=6709 + _globals['_APPROVEONETEAMRESPONSE']._serialized_start=6712 + _globals['_APPROVEONETEAMRESPONSE']._serialized_end=7254 + _globals['_SSLCERTIFICATEINFO']._serialized_start=7257 + _globals['_SSLCERTIFICATEINFO']._serialized_end=7404 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi b/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi index dea1cbc2..024af8a7 100644 --- a/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi @@ -5,7 +5,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 @@ -13,6 +14,7 @@ class SsoAuthenticationProtocolType(int, metaclass=_enum_type_wrapper.EnumTypeWr __slots__ = () UNKNOWN_PROTOCOL: _ClassVar[SsoAuthenticationProtocolType] SAML2: _ClassVar[SsoAuthenticationProtocolType] + JWT: _ClassVar[SsoAuthenticationProtocolType] class CertificateFormat(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -37,6 +39,7 @@ class AutomatorState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NEEDS_CRYPTO_STEP_2: _ClassVar[AutomatorState] UNKNOWN_PROTOCOL: SsoAuthenticationProtocolType SAML2: SsoAuthenticationProtocolType +JWT: SsoAuthenticationProtocolType UNKNOWN_FORMAT: CertificateFormat PKCS12: CertificateFormat JKS: CertificateFormat @@ -81,7 +84,7 @@ class AutomatorSettingValue(_message.Message): translated: bool userVisible: bool required: bool - def __init__(self, settingId: _Optional[int] = ..., settingTypeId: _Optional[int] = ..., settingTag: _Optional[str] = ..., settingName: _Optional[str] = ..., settingValue: _Optional[str] = ..., dataType: _Optional[_Union[_ssocloud_pb2.DataType, str]] = ..., lastModified: _Optional[str] = ..., fromFile: bool = ..., encrypted: bool = ..., encoded: bool = ..., editable: bool = ..., translated: bool = ..., userVisible: bool = ..., required: bool = ...) -> None: ... + def __init__(self, settingId: _Optional[int] = ..., settingTypeId: _Optional[int] = ..., settingTag: _Optional[str] = ..., settingName: _Optional[str] = ..., settingValue: _Optional[str] = ..., dataType: _Optional[_Union[_ssocloud_pb2.DataType, str]] = ..., lastModified: _Optional[str] = ..., fromFile: _Optional[bool] = ..., encrypted: _Optional[bool] = ..., encoded: _Optional[bool] = ..., editable: _Optional[bool] = ..., translated: _Optional[bool] = ..., userVisible: _Optional[bool] = ..., required: _Optional[bool] = ...) -> None: ... class ApproveDeviceRequest(_message.Message): __slots__ = ("automatorId", "ssoAuthenticationProtocolType", "authMessage", "email", "devicePublicKey", "serverEccPublicKeyId", "userEncryptedDataKey", "userEncryptedDataKeyType", "ipAddress", "isTesting", "isEccOnly") @@ -107,7 +110,7 @@ class ApproveDeviceRequest(_message.Message): ipAddress: str isTesting: bool isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., serverEccPublicKeyId: _Optional[int] = ..., userEncryptedDataKey: _Optional[bytes] = ..., userEncryptedDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., ipAddress: _Optional[str] = ..., isTesting: bool = ..., isEccOnly: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., serverEccPublicKeyId: _Optional[int] = ..., userEncryptedDataKey: _Optional[bytes] = ..., userEncryptedDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., ipAddress: _Optional[str] = ..., isTesting: _Optional[bool] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... class SetupRequest(_message.Message): __slots__ = ("automatorId", "serverEccPublicKeyId", "automatorState", "encryptedEnterprisePrivateEccKey", "encryptedEnterprisePrivateRsaKey", "automatorSkills", "encryptedTreeKey", "isEccOnly") @@ -127,7 +130,7 @@ class SetupRequest(_message.Message): automatorSkills: _containers.RepeatedCompositeFieldContainer[AutomatorSkill] encryptedTreeKey: bytes isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., encryptedEnterprisePrivateEccKey: _Optional[bytes] = ..., encryptedEnterprisePrivateRsaKey: _Optional[bytes] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., encryptedTreeKey: _Optional[bytes] = ..., isEccOnly: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., encryptedEnterprisePrivateEccKey: _Optional[bytes] = ..., encryptedEnterprisePrivateRsaKey: _Optional[bytes] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., encryptedTreeKey: _Optional[bytes] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... class StatusRequest(_message.Message): __slots__ = ("automatorId", "serverEccPublicKeyId", "isEccOnly") @@ -137,7 +140,7 @@ class StatusRequest(_message.Message): automatorId: int serverEccPublicKeyId: int isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., isEccOnly: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... class InitializeRequest(_message.Message): __slots__ = ("automatorId", "idpMetadata", "idpSigningCertificate", "ssoEntityId", "emailMapping", "firstnameMapping", "lastnameMapping", "disabled", "serverEccPublicKeyId", "config", "sslMode", "persistState", "disableSniCheck", "sslCertificateFilename", "sslCertificateFilePassword", "sslCertificateKeyPassword", "sslCertificateContents", "automatorHost", "automatorPort", "ipAllow", "ipDeny", "isEccOnly") @@ -185,7 +188,7 @@ class InitializeRequest(_message.Message): ipAllow: str ipDeny: str isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., idpMetadata: _Optional[str] = ..., idpSigningCertificate: _Optional[bytes] = ..., ssoEntityId: _Optional[str] = ..., emailMapping: _Optional[str] = ..., firstnameMapping: _Optional[str] = ..., lastnameMapping: _Optional[str] = ..., disabled: bool = ..., serverEccPublicKeyId: _Optional[int] = ..., config: _Optional[bytes] = ..., sslMode: _Optional[str] = ..., persistState: bool = ..., disableSniCheck: bool = ..., sslCertificateFilename: _Optional[str] = ..., sslCertificateFilePassword: _Optional[str] = ..., sslCertificateKeyPassword: _Optional[str] = ..., sslCertificateContents: _Optional[bytes] = ..., automatorHost: _Optional[str] = ..., automatorPort: _Optional[str] = ..., ipAllow: _Optional[str] = ..., ipDeny: _Optional[str] = ..., isEccOnly: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., idpMetadata: _Optional[str] = ..., idpSigningCertificate: _Optional[bytes] = ..., ssoEntityId: _Optional[str] = ..., emailMapping: _Optional[str] = ..., firstnameMapping: _Optional[str] = ..., lastnameMapping: _Optional[str] = ..., disabled: _Optional[bool] = ..., serverEccPublicKeyId: _Optional[int] = ..., config: _Optional[bytes] = ..., sslMode: _Optional[str] = ..., persistState: _Optional[bool] = ..., disableSniCheck: _Optional[bool] = ..., sslCertificateFilename: _Optional[str] = ..., sslCertificateFilePassword: _Optional[str] = ..., sslCertificateKeyPassword: _Optional[str] = ..., sslCertificateContents: _Optional[bytes] = ..., automatorHost: _Optional[str] = ..., automatorPort: _Optional[str] = ..., ipAllow: _Optional[str] = ..., ipDeny: _Optional[str] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... class NotInitializedResponse(_message.Message): __slots__ = ("automatorTransmissionKey", "signingCertificate", "signingCertificateFilename", "signingCertificatePassword", "signingKeyPassword", "signingCertificateFormat", "automatorPublicKey", "config") @@ -233,7 +236,7 @@ class AutomatorResponse(_message.Message): automatorState: AutomatorState automatorPublicEccKey: bytes version: _version_pb2.Version - def __init__(self, automatorId: _Optional[int] = ..., enabled: bool = ..., timestamp: _Optional[int] = ..., approveDevice: _Optional[_Union[ApproveDeviceResponse, _Mapping]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., notInitialized: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., error: _Optional[_Union[ErrorResponse, _Mapping]] = ..., approveTeamsForUser: _Optional[_Union[ApproveTeamsForUserResponse, _Mapping]] = ..., approveTeams: _Optional[_Union[ApproveTeamsResponse, _Mapping]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorPublicEccKey: _Optional[bytes] = ..., version: _Optional[_Union[_version_pb2.Version, _Mapping]] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., enabled: _Optional[bool] = ..., timestamp: _Optional[int] = ..., approveDevice: _Optional[_Union[ApproveDeviceResponse, _Mapping]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., notInitialized: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., error: _Optional[_Union[ErrorResponse, _Mapping]] = ..., approveTeamsForUser: _Optional[_Union[ApproveTeamsForUserResponse, _Mapping]] = ..., approveTeams: _Optional[_Union[ApproveTeamsResponse, _Mapping]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorPublicEccKey: _Optional[bytes] = ..., version: _Optional[_Union[_version_pb2.Version, _Mapping]] = ...) -> None: ... class ApproveDeviceResponse(_message.Message): __slots__ = ("approved", "encryptedUserDataKey", "message", "encryptedUserDataKeyType") @@ -245,10 +248,10 @@ class ApproveDeviceResponse(_message.Message): encryptedUserDataKey: bytes message: str encryptedUserDataKeyType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: bool = ..., encryptedUserDataKey: _Optional[bytes] = ..., message: _Optional[str] = ..., encryptedUserDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: _Optional[bool] = ..., encryptedUserDataKey: _Optional[bytes] = ..., message: _Optional[str] = ..., encryptedUserDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... class StatusResponse(_message.Message): - __slots__ = ("initialized", "enabledTimestamp", "initializedTimestamp", "updatedTimestamp", "numberOfDevicesApproved", "numberOfDevicesDenied", "numberOfErrors", "sslCertificateExpiration", "notInitializedResponse", "config", "numberOfTeamMembershipsApproved", "numberOfTeamMembershipsDenied", "numberOfTeamsApproved", "numberOfTeamsDenied") + __slots__ = ("initialized", "enabledTimestamp", "initializedTimestamp", "updatedTimestamp", "numberOfDevicesApproved", "numberOfDevicesDenied", "numberOfErrors", "sslCertificateExpiration", "notInitializedResponse", "config", "numberOfTeamMembershipsApproved", "numberOfTeamMembershipsDenied", "numberOfTeamsApproved", "numberOfTeamsDenied", "sslCertificateInfo") INITIALIZED_FIELD_NUMBER: _ClassVar[int] ENABLEDTIMESTAMP_FIELD_NUMBER: _ClassVar[int] INITIALIZEDTIMESTAMP_FIELD_NUMBER: _ClassVar[int] @@ -263,6 +266,7 @@ class StatusResponse(_message.Message): NUMBEROFTEAMMEMBERSHIPSDENIED_FIELD_NUMBER: _ClassVar[int] NUMBEROFTEAMSAPPROVED_FIELD_NUMBER: _ClassVar[int] NUMBEROFTEAMSDENIED_FIELD_NUMBER: _ClassVar[int] + SSLCERTIFICATEINFO_FIELD_NUMBER: _ClassVar[int] initialized: bool enabledTimestamp: int initializedTimestamp: int @@ -277,7 +281,8 @@ class StatusResponse(_message.Message): numberOfTeamMembershipsDenied: int numberOfTeamsApproved: int numberOfTeamsDenied: int - def __init__(self, initialized: bool = ..., enabledTimestamp: _Optional[int] = ..., initializedTimestamp: _Optional[int] = ..., updatedTimestamp: _Optional[int] = ..., numberOfDevicesApproved: _Optional[int] = ..., numberOfDevicesDenied: _Optional[int] = ..., numberOfErrors: _Optional[int] = ..., sslCertificateExpiration: _Optional[int] = ..., notInitializedResponse: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., config: _Optional[bytes] = ..., numberOfTeamMembershipsApproved: _Optional[int] = ..., numberOfTeamMembershipsDenied: _Optional[int] = ..., numberOfTeamsApproved: _Optional[int] = ..., numberOfTeamsDenied: _Optional[int] = ...) -> None: ... + sslCertificateInfo: _containers.RepeatedCompositeFieldContainer[SSLCertificateInfo] + def __init__(self, initialized: _Optional[bool] = ..., enabledTimestamp: _Optional[int] = ..., initializedTimestamp: _Optional[int] = ..., updatedTimestamp: _Optional[int] = ..., numberOfDevicesApproved: _Optional[int] = ..., numberOfDevicesDenied: _Optional[int] = ..., numberOfErrors: _Optional[int] = ..., sslCertificateExpiration: _Optional[int] = ..., notInitializedResponse: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., config: _Optional[bytes] = ..., numberOfTeamMembershipsApproved: _Optional[int] = ..., numberOfTeamMembershipsDenied: _Optional[int] = ..., numberOfTeamsApproved: _Optional[int] = ..., numberOfTeamsDenied: _Optional[int] = ..., sslCertificateInfo: _Optional[_Iterable[_Union[SSLCertificateInfo, _Mapping]]] = ...) -> None: ... class ErrorResponse(_message.Message): __slots__ = ("message",) @@ -305,7 +310,7 @@ class AdminResponse(_message.Message): success: bool message: str automatorInfo: _containers.RepeatedCompositeFieldContainer[AutomatorInfo] - def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorInfo: _Optional[_Iterable[_Union[AutomatorInfo, _Mapping]]] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorInfo: _Optional[_Iterable[_Union[AutomatorInfo, _Mapping]]] = ...) -> None: ... class AutomatorInfo(_message.Message): __slots__ = ("automatorId", "nodeId", "name", "enabled", "url", "automatorSkills", "automatorSettingValues", "status", "logEntries", "automatorState", "version") @@ -331,7 +336,7 @@ class AutomatorInfo(_message.Message): logEntries: _containers.RepeatedCompositeFieldContainer[LogEntry] automatorState: AutomatorState version: str - def __init__(self, automatorId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: bool = ..., url: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., logEntries: _Optional[_Iterable[_Union[LogEntry, _Mapping]]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., version: _Optional[str] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: _Optional[bool] = ..., url: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., logEntries: _Optional[_Iterable[_Union[LogEntry, _Mapping]]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., version: _Optional[str] = ...) -> None: ... class AdminCreateAutomatorRequest(_message.Message): __slots__ = ("nodeId", "name", "skill") @@ -373,7 +378,7 @@ class AdminEnableAutomatorRequest(_message.Message): ENABLED_FIELD_NUMBER: _ClassVar[int] automatorId: int enabled: bool - def __init__(self, automatorId: _Optional[int] = ..., enabled: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., enabled: _Optional[bool] = ...) -> None: ... class AdminEditAutomatorRequest(_message.Message): __slots__ = ("automatorId", "name", "enabled", "url", "skillTypes", "automatorSettingValues") @@ -389,7 +394,7 @@ class AdminEditAutomatorRequest(_message.Message): url: str skillTypes: _containers.RepeatedScalarFieldContainer[SkillType] automatorSettingValues: _containers.RepeatedCompositeFieldContainer[AutomatorSettingValue] - def __init__(self, automatorId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: bool = ..., url: _Optional[str] = ..., skillTypes: _Optional[_Iterable[_Union[SkillType, str]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: _Optional[bool] = ..., url: _Optional[str] = ..., skillTypes: _Optional[_Iterable[_Union[SkillType, str]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ...) -> None: ... class AdminSetupAutomatorRequest(_message.Message): __slots__ = ("automatorId", "automatorState", "encryptedEccEnterprisePrivateKey", "encryptedRsaEnterprisePrivateKey", "skillTypes", "encryptedTreeKey") @@ -419,7 +424,7 @@ class AdminSetupAutomatorResponse(_message.Message): automatorId: int automatorState: AutomatorState automatorEccPublicKey: bytes - def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorEccPublicKey: _Optional[bytes] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorEccPublicKey: _Optional[bytes] = ...) -> None: ... class AdminAutomatorSkillsRequest(_message.Message): __slots__ = ("automatorId",) @@ -445,7 +450,7 @@ class AdminAutomatorSkillsResponse(_message.Message): success: bool message: str automatorSkills: _containers.RepeatedCompositeFieldContainer[AutomatorSkill] - def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ...) -> None: ... class AdminResetAutomatorRequest(_message.Message): __slots__ = ("automatorId",) @@ -495,7 +500,7 @@ class ApproveTeamsForUserRequest(_message.Message): isTesting: bool isEccOnly: bool userPublicKeyEcc: bytes - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., userPublicKey: _Optional[bytes] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isTesting: bool = ..., isEccOnly: bool = ..., userPublicKeyEcc: _Optional[bytes] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., userPublicKey: _Optional[bytes] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isTesting: _Optional[bool] = ..., isEccOnly: _Optional[bool] = ..., userPublicKeyEcc: _Optional[bytes] = ...) -> None: ... class TeamDescription(_message.Message): __slots__ = ("teamUid", "teamName", "encryptedTeamKey", "encryptedTeamKeyType") @@ -539,7 +544,7 @@ class ApproveOneTeamForUserResponse(_message.Message): userEncryptedTeamKeyType: _enterprise_pb2.EncryptedKeyType userEncryptedTeamKeyByEcc: bytes userEncryptedTeamKeyByEccType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: bool = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., userEncryptedTeamKey: _Optional[bytes] = ..., userEncryptedTeamKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., userEncryptedTeamKeyByEcc: _Optional[bytes] = ..., userEncryptedTeamKeyByEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: _Optional[bool] = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., userEncryptedTeamKey: _Optional[bytes] = ..., userEncryptedTeamKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., userEncryptedTeamKeyByEcc: _Optional[bytes] = ..., userEncryptedTeamKeyByEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... class ApproveTeamsRequest(_message.Message): __slots__ = ("automatorId", "ssoAuthenticationProtocolType", "authMessage", "email", "serverEccPublicKeyId", "ipAddress", "teamDescription", "isEccOnly", "isTesting") @@ -561,7 +566,7 @@ class ApproveTeamsRequest(_message.Message): teamDescription: _containers.RepeatedCompositeFieldContainer[TeamDescription] isEccOnly: bool isTesting: bool - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isEccOnly: bool = ..., isTesting: bool = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isEccOnly: _Optional[bool] = ..., isTesting: _Optional[bool] = ...) -> None: ... class ApproveTeamsResponse(_message.Message): __slots__ = ("automatorId", "message", "approveTeamResponse") @@ -603,4 +608,22 @@ class ApproveOneTeamResponse(_message.Message): teamPublicKeyEcc: bytes encryptedTeamPrivateKeyEcc: bytes encryptedTeamPrivateKeyEccType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: bool = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., encryptedTeamKeyCbc: _Optional[bytes] = ..., encryptedTeamKeyCbcType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., encryptedTeamKeyGcm: _Optional[bytes] = ..., encryptedTeamKeyGcmType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsaType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: _Optional[bool] = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., encryptedTeamKeyCbc: _Optional[bytes] = ..., encryptedTeamKeyCbcType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., encryptedTeamKeyGcm: _Optional[bytes] = ..., encryptedTeamKeyGcmType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsaType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + +class SSLCertificateInfo(_message.Message): + __slots__ = ("automatorId", "hostUrl", "subject", "issuer", "issuedOn", "expiresOn", "checkedOn") + AUTOMATORID_FIELD_NUMBER: _ClassVar[int] + HOSTURL_FIELD_NUMBER: _ClassVar[int] + SUBJECT_FIELD_NUMBER: _ClassVar[int] + ISSUER_FIELD_NUMBER: _ClassVar[int] + ISSUEDON_FIELD_NUMBER: _ClassVar[int] + EXPIRESON_FIELD_NUMBER: _ClassVar[int] + CHECKEDON_FIELD_NUMBER: _ClassVar[int] + automatorId: int + hostUrl: str + subject: str + issuer: str + issuedOn: str + expiresOn: str + checkedOn: str + def __init__(self, automatorId: _Optional[int] = ..., hostUrl: _Optional[str] = ..., subject: _Optional[str] = ..., issuer: _Optional[str] = ..., issuedOn: _Optional[str] = ..., expiresOn: _Optional[str] = ..., checkedOn: _Optional[str] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py index 33d35660..77b8c0cb 100644 --- a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: breachwatch.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'breachwatch.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi index c888e6f2..d7e60f07 100644 --- a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi @@ -2,7 +2,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 @@ -23,7 +24,7 @@ class BreachWatchRecordRequest(_message.Message): encryptedData: bytes breachWatchInfoType: BreachWatchInfoType updateUserWhoScanned: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: _Optional[bool] = ...) -> None: ... class BreachWatchUpdateRequest(_message.Message): __slots__ = ("breachWatchRecordRequest", "encryptedData") @@ -61,7 +62,7 @@ class BreachWatchTokenResponse(_message.Message): CLIENTENCRYPTED_FIELD_NUMBER: _ClassVar[int] breachWatchToken: bytes clientEncrypted: bool - def __init__(self, breachWatchToken: _Optional[bytes] = ..., clientEncrypted: bool = ...) -> None: ... + def __init__(self, breachWatchToken: _Optional[bytes] = ..., clientEncrypted: _Optional[bool] = ...) -> None: ... class AnonymizedTokenResponse(_message.Message): __slots__ = ("domainToken", "emailToken", "passwordToken") @@ -99,7 +100,7 @@ class HashStatus(_message.Message): hash1: bytes euid: bytes breachDetected: bool - def __init__(self, hash1: _Optional[bytes] = ..., euid: _Optional[bytes] = ..., breachDetected: bool = ...) -> None: ... + def __init__(self, hash1: _Optional[bytes] = ..., euid: _Optional[bytes] = ..., breachDetected: _Optional[bool] = ...) -> None: ... class BreachWatchStatusResponse(_message.Message): __slots__ = ("hashStatus",) @@ -139,7 +140,7 @@ class PaidUserResponse(_message.Message): __slots__ = ("paidUser",) PAIDUSER_FIELD_NUMBER: _ClassVar[int] paidUser: bool - def __init__(self, paidUser: bool = ...) -> None: ... + def __init__(self, paidUser: _Optional[bool] = ...) -> None: ... class DetailedScanRequest(_message.Message): __slots__ = ("email",) @@ -165,7 +166,7 @@ class BreachEvent(_message.Message): passwordInBreach: bool date: str description: str - def __init__(self, site: _Optional[str] = ..., email: _Optional[str] = ..., passwordInBreach: bool = ..., date: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... + def __init__(self, site: _Optional[str] = ..., email: _Optional[str] = ..., passwordInBreach: _Optional[bool] = ..., date: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... class UseOneTimeTokenResponse(_message.Message): __slots__ = ("emailBreaches", "passwordBreaches", "breachEvents", "email") diff --git a/keepersdk-package/src/keepersdk/proto/client_pb2.py b/keepersdk-package/src/keepersdk/proto/client_pb2.py index 75860a15..2e0ec061 100644 --- a/keepersdk-package/src/keepersdk/proto/client_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/client_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: client.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'client.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/client_pb2.pyi b/keepersdk-package/src/keepersdk/proto/client_pb2.pyi index d04a2e6d..888b5f66 100644 --- a/keepersdk-package/src/keepersdk/proto/client_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/client_pb2.pyi @@ -2,7 +2,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 @@ -44,7 +45,7 @@ class BreachWatchRecordRequest(_message.Message): encryptedData: bytes breachWatchInfoType: BreachWatchInfoType updateUserWhoScanned: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: _Optional[bool] = ...) -> None: ... class BreachWatchData(_message.Message): __slots__ = ("passwords", "emails", "domains") diff --git a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py index e1575598..f6bfe9b7 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: enterprise.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'enterprise.proto' ) @@ -24,7 +24,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\"/\n\x0bTeamsByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\"<\n\x12ManagedNodesByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x03(\x03\"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\"E\n\x18TeamEnterpriseUserRemove\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"j\n TeamEnterpriseUserRemovesRequest\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x03(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\"{\n!TeamEnterpriseUserRemovesResponse\x12V\n teamEnterpriseUserRemoveResponse\x18\x01 \x03(\x0b\x32,.Enterprise.TeamEnterpriseUserRemoveResponse\"\xb8\x01\n TeamEnterpriseUserRemoveResponse\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x01(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\nresultCode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \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*\x9a\x02\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\x12\x13\n\x0fINVALID_ROLE_ID\x10\x08\x12\x1d\n\x19PAM_LICENSE_SEAT_EXCEEDED\x10\t*=\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*\xe1\x03\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*\"\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\"/\n\x0bTeamsByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\"<\n\x12ManagedNodesByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x03(\x03\"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\"d\n\x11LoginToMcResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTreeKey\x18\x02 \x01(\t\x12\x16\n\x0e\x66orbidKeyType2\x18\x04 \x01(\x08\"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\"T\n\x17PrivilegesByManagedNode\x12\x15\n\rmanagedNodeId\x18\x01 \x01(\x03\x12\x0e\n\x06roleId\x18\x02 \x01(\x03\x12\x12\n\nprivileges\x18\x03 \x03(\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\"E\n\x18TeamEnterpriseUserRemove\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"j\n TeamEnterpriseUserRemovesRequest\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x03(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\"{\n!TeamEnterpriseUserRemovesResponse\x12V\n teamEnterpriseUserRemoveResponse\x18\x01 \x03(\x0b\x32,.Enterprise.TeamEnterpriseUserRemoveResponse\"\xb8\x01\n TeamEnterpriseUserRemoveResponse\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x01(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\nresultCode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \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\".\n\x0bRolesByTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0e\n\x06roleId\x18\x02 \x03(\x03\"\x8d\x01\n\x10LockUsersRequest\x12\x1d\n\x15lockEnterpriseUserIds\x18\x01 \x03(\x03\x12 \n\x18\x64isableEnterpriseUserIds\x18\x02 \x03(\x03\x12\x1f\n\x17unlockEnterpriseUserIds\x18\x03 \x03(\x03\x12\x17\n\x0f\x64\x65leteIfPending\x18\x04 \x01(\x08\"C\n\x11LockUsersResponse\x12.\n\x08response\x18\x01 \x03(\x0b\x32\x1c.Enterprise.LockUserResponse\"n\n\x10LockUserResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.Enterprise.UserLockStatus\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x01(\t*\x1b\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x07\n\x03\x45\x43\x43\x10\x01*\x9a\x02\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\x12\x13\n\x0fINVALID_ROLE_ID\x10\x08\x12\x1d\n\x19PAM_LICENSE_SEAT_EXCEEDED\x10\t*=\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*\xe1\x03\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*\"\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*\xa5\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\x12\x15\n\x11\x46ORBID_KEY_TYPE_1\x10\x0b*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*s\n\x0eUserLockStatus\x12\x17\n\x13UNKNOWN_LOCK_STATUS\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x0c\n\x08\x44ISABLED\x10\x02\x12\x0c\n\x08UNLOCKED\x10\x03\x12\x0b\n\x07\x44\x45LETED\x10\x04\x12\x13\n\x0f\x43\x41NT_BE_PENDING\x10\x05\x42&\n\x18\x63om.keepersecurity.protoB\nEnterpriseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -38,38 +38,40 @@ _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=19919 - _globals['_KEYTYPE']._serialized_end=19946 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=19949 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=20231 - _globals['_ENTERPRISETYPE']._serialized_start=20233 - _globals['_ENTERPRISETYPE']._serialized_end=20294 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=20296 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=20411 - _globals['_ENTERPRISEDATAENTITY']._serialized_start=20414 - _globals['_ENTERPRISEDATAENTITY']._serialized_end=20895 - _globals['_CACHESTATUS']._serialized_start=20897 - _globals['_CACHESTATUS']._serialized_end=20931 - _globals['_BACKUPKEYTYPE']._serialized_start=20934 - _globals['_BACKUPKEYTYPE']._serialized_end=21081 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=21083 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=21141 - _globals['_ENCRYPTEDKEYTYPE']._serialized_start=21144 - _globals['_ENCRYPTEDKEYTYPE']._serialized_end=21309 - _globals['_ENTERPRISEFLAGTYPE']._serialized_start=21312 - _globals['_ENTERPRISEFLAGTYPE']._serialized_end=21582 - _globals['_USERUPDATESTATUS']._serialized_start=21584 - _globals['_USERUPDATESTATUS']._serialized_end=21653 - _globals['_AUDITUSERSTATUS']._serialized_start=21655 - _globals['_AUDITUSERSTATUS']._serialized_end=21728 - _globals['_TEAMUSERTYPE']._serialized_start=21730 - _globals['_TEAMUSERTYPE']._serialized_end=21781 - _globals['_APPCLIENTTYPE']._serialized_start=21783 - _globals['_APPCLIENTTYPE']._serialized_end=21903 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=21906 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=22049 - _globals['_CLEARSECURITYDATATYPE']._serialized_start=22052 - _globals['_CLEARSECURITYDATATYPE']._serialized_end=22187 + _globals['_KEYTYPE']._serialized_start=20402 + _globals['_KEYTYPE']._serialized_end=20429 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=20432 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=20714 + _globals['_ENTERPRISETYPE']._serialized_start=20716 + _globals['_ENTERPRISETYPE']._serialized_end=20777 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=20779 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=20894 + _globals['_ENTERPRISEDATAENTITY']._serialized_start=20897 + _globals['_ENTERPRISEDATAENTITY']._serialized_end=21378 + _globals['_CACHESTATUS']._serialized_start=21380 + _globals['_CACHESTATUS']._serialized_end=21414 + _globals['_BACKUPKEYTYPE']._serialized_start=21417 + _globals['_BACKUPKEYTYPE']._serialized_end=21564 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=21566 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=21624 + _globals['_ENCRYPTEDKEYTYPE']._serialized_start=21627 + _globals['_ENCRYPTEDKEYTYPE']._serialized_end=21792 + _globals['_ENTERPRISEFLAGTYPE']._serialized_start=21795 + _globals['_ENTERPRISEFLAGTYPE']._serialized_end=22088 + _globals['_USERUPDATESTATUS']._serialized_start=22090 + _globals['_USERUPDATESTATUS']._serialized_end=22159 + _globals['_AUDITUSERSTATUS']._serialized_start=22161 + _globals['_AUDITUSERSTATUS']._serialized_end=22234 + _globals['_TEAMUSERTYPE']._serialized_start=22236 + _globals['_TEAMUSERTYPE']._serialized_end=22287 + _globals['_APPCLIENTTYPE']._serialized_start=22289 + _globals['_APPCLIENTTYPE']._serialized_end=22409 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=22412 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=22555 + _globals['_CLEARSECURITYDATATYPE']._serialized_start=22558 + _globals['_CLEARSECURITYDATATYPE']._serialized_end=22693 + _globals['_USERLOCKSTATUS']._serialized_start=22695 + _globals['_USERLOCKSTATUS']._serialized_end=22810 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_start=33 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_end=165 _globals['_GETTEAMMEMBERREQUEST']._serialized_start=167 @@ -127,265 +129,275 @@ _globals['_LOGINTOMCREQUEST']._serialized_start=2873 _globals['_LOGINTOMCREQUEST']._serialized_end=2942 _globals['_LOGINTOMCRESPONSE']._serialized_start=2944 - _globals['_LOGINTOMCRESPONSE']._serialized_end=3020 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=3022 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=3125 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=3128 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3264 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3266 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3382 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3384 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3473 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3475 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3567 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3570 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3705 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3707 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3780 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3782 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3885 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3887 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=3987 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=3989 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=4083 - _globals['_ENTERPRISEDATAREQUEST']._serialized_start=4085 - _globals['_ENTERPRISEDATAREQUEST']._serialized_end=4135 - _globals['_SPECIALPROVISIONING']._serialized_start=4137 - _globals['_SPECIALPROVISIONING']._serialized_end=4185 - _globals['_GENERALDATAENTITY']._serialized_start=4188 - _globals['_GENERALDATAENTITY']._serialized_end=4448 - _globals['_NODE']._serialized_start=4451 - _globals['_NODE']._serialized_end=4704 - _globals['_ROLE']._serialized_start=4707 - _globals['_ROLE']._serialized_end=4849 - _globals['_USER']._serialized_start=4852 - _globals['_USER']._serialized_end=5164 - _globals['_USERALIAS']._serialized_start=5166 - _globals['_USERALIAS']._serialized_end=5221 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5224 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5396 - _globals['_MANAGEDNODE']._serialized_start=5398 - _globals['_MANAGEDNODE']._serialized_end=5481 - _globals['_USERMANAGEDNODE']._serialized_start=5483 - _globals['_USERMANAGEDNODE']._serialized_end=5567 - _globals['_USERPRIVILEGE']._serialized_start=5569 - _globals['_USERPRIVILEGE']._serialized_end=5688 - _globals['_ROLEUSER']._serialized_start=5690 - _globals['_ROLEUSER']._serialized_end=5742 - _globals['_ROLEPRIVILEGE']._serialized_start=5744 - _globals['_ROLEPRIVILEGE']._serialized_end=5821 - _globals['_ROLEENFORCEMENT']._serialized_start=5823 - _globals['_ROLEENFORCEMENT']._serialized_end=5896 - _globals['_TEAM']._serialized_start=5899 - _globals['_TEAM']._serialized_end=6068 - _globals['_TEAMUSER']._serialized_start=6070 - _globals['_TEAMUSER']._serialized_end=6141 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6143 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6218 - _globals['_DISTRIBUTOR']._serialized_start=6220 - _globals['_DISTRIBUTOR']._serialized_end=6286 - _globals['_MSPINFO']._serialized_start=6289 - _globals['_MSPINFO']._serialized_end=6574 - _globals['_MANAGEDCOMPANY']._serialized_start=6577 - _globals['_MANAGEDCOMPANY']._serialized_end=6850 - _globals['_MSPPOOL']._serialized_start=6852 - _globals['_MSPPOOL']._serialized_end=6934 - _globals['_MSPCONTACT']._serialized_start=6936 - _globals['_MSPCONTACT']._serialized_end=6994 - _globals['_LICENSEADDON']._serialized_start=6997 - _globals['_LICENSEADDON']._serialized_end=7233 - _globals['_MCDEFAULT']._serialized_start=7235 - _globals['_MCDEFAULT']._serialized_end=7350 - _globals['_MSPPERMITS']._serialized_start=7353 - _globals['_MSPPERMITS']._serialized_end=7563 - _globals['_LICENSE']._serialized_start=7566 - _globals['_LICENSE']._serialized_end=8110 - _globals['_BRIDGE']._serialized_start=8112 - _globals['_BRIDGE']._serialized_end=8222 - _globals['_SCIM']._serialized_start=8224 - _globals['_SCIM']._serialized_end=8340 - _globals['_EMAILPROVISION']._serialized_start=8342 - _globals['_EMAILPROVISION']._serialized_end=8418 - _globals['_QUEUEDTEAM']._serialized_start=8420 - _globals['_QUEUEDTEAM']._serialized_end=8502 - _globals['_QUEUEDTEAMUSER']._serialized_start=8504 - _globals['_QUEUEDTEAMUSER']._serialized_end=8552 - _globals['_TEAMSADDRESULT']._serialized_start=8555 - _globals['_TEAMSADDRESULT']._serialized_end=8719 - _globals['_TEAMADDRESULT']._serialized_start=8721 - _globals['_TEAMADDRESULT']._serialized_end=8806 - _globals['_SSOSERVICE']._serialized_start=8809 - _globals['_SSOSERVICE']._serialized_end=8954 - _globals['_REPORTFILTERUSER']._serialized_start=8956 - _globals['_REPORTFILTERUSER']._serialized_end=9005 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=9008 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9287 - _globals['_ENTERPRISEDATA']._serialized_start=9289 - _globals['_ENTERPRISEDATA']._serialized_end=9385 - _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9388 - _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9596 - _globals['_BACKUPREQUEST']._serialized_start=9598 - _globals['_BACKUPREQUEST']._serialized_end=9640 - _globals['_BACKUPRECORD']._serialized_start=9643 - _globals['_BACKUPRECORD']._serialized_end=9795 - _globals['_BACKUPKEY']._serialized_start=9797 - _globals['_BACKUPKEY']._serialized_end=9843 - _globals['_BACKUPUSER']._serialized_start=9846 - _globals['_BACKUPUSER']._serialized_end=10115 - _globals['_BACKUPRESPONSE']._serialized_start=10118 - _globals['_BACKUPRESPONSE']._serialized_end=10276 - _globals['_BACKUPFILE']._serialized_start=10278 - _globals['_BACKUPFILE']._serialized_end=10379 - _globals['_BACKUPSRESPONSE']._serialized_start=10381 - _globals['_BACKUPSRESPONSE']._serialized_end=10437 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10439 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10485 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10488 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10743 - _globals['_ROLEKEY']._serialized_start=10745 - _globals['_ROLEKEY']._serialized_end=10839 - _globals['_MSPKEY']._serialized_start=10841 - _globals['_MSPKEY']._serialized_end=10941 - _globals['_ENTERPRISEKEYS']._serialized_start=10943 - _globals['_ENTERPRISEKEYS']._serialized_end=11067 - _globals['_TREEKEY']._serialized_start=11069 - _globals['_TREEKEY']._serialized_end=11141 - _globals['_SHAREDRECORDRESPONSE']._serialized_start=11143 - _globals['_SHAREDRECORDRESPONSE']._serialized_end=11212 - _globals['_SHAREDRECORDEVENT']._serialized_start=11214 - _globals['_SHAREDRECORDEVENT']._serialized_end=11326 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11328 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11374 - _globals['_USERADDREQUEST']._serialized_start=11377 - _globals['_USERADDREQUEST']._serialized_end=11585 - _globals['_USERUPDATEREQUEST']._serialized_start=11587 - _globals['_USERUPDATEREQUEST']._serialized_end=11645 - _globals['_USERUPDATE']._serialized_start=11648 - _globals['_USERUPDATE']._serialized_end=11823 - _globals['_USERUPDATERESPONSE']._serialized_start=11825 - _globals['_USERUPDATERESPONSE']._serialized_end=11890 - _globals['_USERUPDATERESULT']._serialized_start=11892 - _globals['_USERUPDATERESULT']._serialized_end=11982 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=11984 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=12058 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=12060 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12139 - _globals['_RECORDOWNER']._serialized_start=12141 - _globals['_RECORDOWNER']._serialized_end=12196 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12199 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12365 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12368 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12527 - _globals['_AUDITUSERRECORD']._serialized_start=12529 - _globals['_AUDITUSERRECORD']._serialized_end=12604 - _globals['_AUDITUSERDATA']._serialized_start=12607 - _globals['_AUDITUSERDATA']._serialized_end=12748 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12750 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=12877 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=12879 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=13006 - _globals['_COMPLIANCEREPORTRUN']._serialized_start=13009 - _globals['_COMPLIANCEREPORTRUN']._serialized_end=13142 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13145 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13397 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13399 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13497 - _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13499 - _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13619 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13622 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14295 - _globals['_AUDITRECORD']._serialized_start=14298 - _globals['_AUDITRECORD']._serialized_end=14427 - _globals['_AUDITROLE']._serialized_start=14430 - _globals['_AUDITROLE']._serialized_end=14686 - _globals['_ROLENODEMANAGEMENT']._serialized_start=14688 - _globals['_ROLENODEMANAGEMENT']._serialized_end=14782 - _globals['_USERPROFILE']._serialized_start=14784 - _globals['_USERPROFILE']._serialized_end=14891 - _globals['_RECORDPERMISSION']._serialized_start=14893 - _globals['_RECORDPERMISSION']._serialized_end=14954 - _globals['_USERRECORD']._serialized_start=14956 - _globals['_USERRECORD']._serialized_end=15051 - _globals['_AUDITTEAM']._serialized_start=15053 - _globals['_AUDITTEAM']._serialized_end=15144 - _globals['_AUDITTEAMUSER']._serialized_start=15146 - _globals['_AUDITTEAMUSER']._serialized_end=15205 - _globals['_SHAREDFOLDERRECORD']._serialized_start=15208 - _globals['_SHAREDFOLDERRECORD']._serialized_end=15367 - _globals['_SHAREADMINRECORD']._serialized_start=15369 - _globals['_SHAREADMINRECORD']._serialized_end=15446 - _globals['_SHAREDFOLDERUSER']._serialized_start=15448 - _globals['_SHAREDFOLDERUSER']._serialized_end=15518 - _globals['_SHAREDFOLDERTEAM']._serialized_start=15520 - _globals['_SHAREDFOLDERTEAM']._serialized_end=15581 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15583 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15630 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15632 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15682 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15684 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15738 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15740 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15799 - _globals['_LINKEDRECORD']._serialized_start=15801 - _globals['_LINKEDRECORD']._serialized_end=15853 - _globals['_GETSHARINGADMINSREQUEST']._serialized_start=15855 - _globals['_GETSHARINGADMINSREQUEST']._serialized_end=15942 - _globals['_USERPROFILEEXT']._serialized_start=15945 - _globals['_USERPROFILEEXT']._serialized_end=16169 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16171 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16250 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16252 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16347 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16349 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16465 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16468 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16639 - _globals['_TYPEDKEY']._serialized_start=16641 - _globals['_TYPEDKEY']._serialized_end=16711 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16713 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=16828 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=16831 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=17027 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=17030 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17189 - _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_start=17191 - _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_end=17260 - _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_start=17262 - _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_end=17368 - _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_start=17370 - _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_end=17493 - _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_start=17496 - _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_end=17680 - _globals['_DOMAINALIAS']._serialized_start=17682 - _globals['_DOMAINALIAS']._serialized_end=17759 - _globals['_DOMAINALIASREQUEST']._serialized_start=17761 - _globals['_DOMAINALIASREQUEST']._serialized_end=17827 - _globals['_DOMAINALIASRESPONSE']._serialized_start=17829 - _globals['_DOMAINALIASRESPONSE']._serialized_end=17896 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=17898 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=18007 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=18010 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=18448 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=18450 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=18545 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=18547 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18660 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18662 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18759 - _globals['_ENTERPRISEUSERSADD']._serialized_start=18762 - _globals['_ENTERPRISEUSERSADD']._serialized_end=19030 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=19033 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=19188 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=19191 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=19341 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=19344 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=19529 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=19531 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=19588 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=19590 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19701 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19703 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19796 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19798 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=19917 + _globals['_LOGINTOMCRESPONSE']._serialized_end=3044 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=3046 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=3149 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=3152 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3288 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3290 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3406 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3408 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3497 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3499 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3591 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3594 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3729 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3731 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3804 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3806 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3909 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3911 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=4011 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=4013 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=4107 + _globals['_ENTERPRISEDATAREQUEST']._serialized_start=4109 + _globals['_ENTERPRISEDATAREQUEST']._serialized_end=4159 + _globals['_SPECIALPROVISIONING']._serialized_start=4161 + _globals['_SPECIALPROVISIONING']._serialized_end=4209 + _globals['_GENERALDATAENTITY']._serialized_start=4212 + _globals['_GENERALDATAENTITY']._serialized_end=4472 + _globals['_NODE']._serialized_start=4475 + _globals['_NODE']._serialized_end=4728 + _globals['_ROLE']._serialized_start=4731 + _globals['_ROLE']._serialized_end=4873 + _globals['_USER']._serialized_start=4876 + _globals['_USER']._serialized_end=5188 + _globals['_USERALIAS']._serialized_start=5190 + _globals['_USERALIAS']._serialized_end=5245 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5248 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5420 + _globals['_MANAGEDNODE']._serialized_start=5422 + _globals['_MANAGEDNODE']._serialized_end=5505 + _globals['_USERMANAGEDNODE']._serialized_start=5507 + _globals['_USERMANAGEDNODE']._serialized_end=5591 + _globals['_USERPRIVILEGE']._serialized_start=5593 + _globals['_USERPRIVILEGE']._serialized_end=5712 + _globals['_ROLEUSER']._serialized_start=5714 + _globals['_ROLEUSER']._serialized_end=5766 + _globals['_ROLEPRIVILEGE']._serialized_start=5768 + _globals['_ROLEPRIVILEGE']._serialized_end=5845 + _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_start=5847 + _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_end=5931 + _globals['_ROLEENFORCEMENT']._serialized_start=5933 + _globals['_ROLEENFORCEMENT']._serialized_end=6006 + _globals['_TEAM']._serialized_start=6009 + _globals['_TEAM']._serialized_end=6178 + _globals['_TEAMUSER']._serialized_start=6180 + _globals['_TEAMUSER']._serialized_end=6251 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6253 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6328 + _globals['_DISTRIBUTOR']._serialized_start=6330 + _globals['_DISTRIBUTOR']._serialized_end=6396 + _globals['_MSPINFO']._serialized_start=6399 + _globals['_MSPINFO']._serialized_end=6684 + _globals['_MANAGEDCOMPANY']._serialized_start=6687 + _globals['_MANAGEDCOMPANY']._serialized_end=6960 + _globals['_MSPPOOL']._serialized_start=6962 + _globals['_MSPPOOL']._serialized_end=7044 + _globals['_MSPCONTACT']._serialized_start=7046 + _globals['_MSPCONTACT']._serialized_end=7104 + _globals['_LICENSEADDON']._serialized_start=7107 + _globals['_LICENSEADDON']._serialized_end=7343 + _globals['_MCDEFAULT']._serialized_start=7345 + _globals['_MCDEFAULT']._serialized_end=7460 + _globals['_MSPPERMITS']._serialized_start=7463 + _globals['_MSPPERMITS']._serialized_end=7673 + _globals['_LICENSE']._serialized_start=7676 + _globals['_LICENSE']._serialized_end=8220 + _globals['_BRIDGE']._serialized_start=8222 + _globals['_BRIDGE']._serialized_end=8332 + _globals['_SCIM']._serialized_start=8334 + _globals['_SCIM']._serialized_end=8450 + _globals['_EMAILPROVISION']._serialized_start=8452 + _globals['_EMAILPROVISION']._serialized_end=8528 + _globals['_QUEUEDTEAM']._serialized_start=8530 + _globals['_QUEUEDTEAM']._serialized_end=8612 + _globals['_QUEUEDTEAMUSER']._serialized_start=8614 + _globals['_QUEUEDTEAMUSER']._serialized_end=8662 + _globals['_TEAMSADDRESULT']._serialized_start=8665 + _globals['_TEAMSADDRESULT']._serialized_end=8829 + _globals['_TEAMADDRESULT']._serialized_start=8831 + _globals['_TEAMADDRESULT']._serialized_end=8916 + _globals['_SSOSERVICE']._serialized_start=8919 + _globals['_SSOSERVICE']._serialized_end=9064 + _globals['_REPORTFILTERUSER']._serialized_start=9066 + _globals['_REPORTFILTERUSER']._serialized_end=9115 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=9118 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9397 + _globals['_ENTERPRISEDATA']._serialized_start=9399 + _globals['_ENTERPRISEDATA']._serialized_end=9495 + _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9498 + _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9706 + _globals['_BACKUPREQUEST']._serialized_start=9708 + _globals['_BACKUPREQUEST']._serialized_end=9750 + _globals['_BACKUPRECORD']._serialized_start=9753 + _globals['_BACKUPRECORD']._serialized_end=9905 + _globals['_BACKUPKEY']._serialized_start=9907 + _globals['_BACKUPKEY']._serialized_end=9953 + _globals['_BACKUPUSER']._serialized_start=9956 + _globals['_BACKUPUSER']._serialized_end=10225 + _globals['_BACKUPRESPONSE']._serialized_start=10228 + _globals['_BACKUPRESPONSE']._serialized_end=10386 + _globals['_BACKUPFILE']._serialized_start=10388 + _globals['_BACKUPFILE']._serialized_end=10489 + _globals['_BACKUPSRESPONSE']._serialized_start=10491 + _globals['_BACKUPSRESPONSE']._serialized_end=10547 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10549 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10595 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10598 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10853 + _globals['_ROLEKEY']._serialized_start=10855 + _globals['_ROLEKEY']._serialized_end=10949 + _globals['_MSPKEY']._serialized_start=10951 + _globals['_MSPKEY']._serialized_end=11051 + _globals['_ENTERPRISEKEYS']._serialized_start=11053 + _globals['_ENTERPRISEKEYS']._serialized_end=11177 + _globals['_TREEKEY']._serialized_start=11179 + _globals['_TREEKEY']._serialized_end=11251 + _globals['_SHAREDRECORDRESPONSE']._serialized_start=11253 + _globals['_SHAREDRECORDRESPONSE']._serialized_end=11322 + _globals['_SHAREDRECORDEVENT']._serialized_start=11324 + _globals['_SHAREDRECORDEVENT']._serialized_end=11436 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11438 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11484 + _globals['_USERADDREQUEST']._serialized_start=11487 + _globals['_USERADDREQUEST']._serialized_end=11695 + _globals['_USERUPDATEREQUEST']._serialized_start=11697 + _globals['_USERUPDATEREQUEST']._serialized_end=11755 + _globals['_USERUPDATE']._serialized_start=11758 + _globals['_USERUPDATE']._serialized_end=11933 + _globals['_USERUPDATERESPONSE']._serialized_start=11935 + _globals['_USERUPDATERESPONSE']._serialized_end=12000 + _globals['_USERUPDATERESULT']._serialized_start=12002 + _globals['_USERUPDATERESULT']._serialized_end=12092 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=12094 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=12168 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=12170 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12249 + _globals['_RECORDOWNER']._serialized_start=12251 + _globals['_RECORDOWNER']._serialized_end=12306 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12309 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12475 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12478 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12637 + _globals['_AUDITUSERRECORD']._serialized_start=12639 + _globals['_AUDITUSERRECORD']._serialized_end=12714 + _globals['_AUDITUSERDATA']._serialized_start=12717 + _globals['_AUDITUSERDATA']._serialized_end=12858 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12860 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=12987 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=12989 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=13116 + _globals['_COMPLIANCEREPORTRUN']._serialized_start=13119 + _globals['_COMPLIANCEREPORTRUN']._serialized_end=13252 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13255 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13507 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13509 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13607 + _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13609 + _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13729 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13732 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14405 + _globals['_AUDITRECORD']._serialized_start=14408 + _globals['_AUDITRECORD']._serialized_end=14537 + _globals['_AUDITROLE']._serialized_start=14540 + _globals['_AUDITROLE']._serialized_end=14796 + _globals['_ROLENODEMANAGEMENT']._serialized_start=14798 + _globals['_ROLENODEMANAGEMENT']._serialized_end=14892 + _globals['_USERPROFILE']._serialized_start=14894 + _globals['_USERPROFILE']._serialized_end=15001 + _globals['_RECORDPERMISSION']._serialized_start=15003 + _globals['_RECORDPERMISSION']._serialized_end=15064 + _globals['_USERRECORD']._serialized_start=15066 + _globals['_USERRECORD']._serialized_end=15161 + _globals['_AUDITTEAM']._serialized_start=15163 + _globals['_AUDITTEAM']._serialized_end=15254 + _globals['_AUDITTEAMUSER']._serialized_start=15256 + _globals['_AUDITTEAMUSER']._serialized_end=15315 + _globals['_SHAREDFOLDERRECORD']._serialized_start=15318 + _globals['_SHAREDFOLDERRECORD']._serialized_end=15477 + _globals['_SHAREADMINRECORD']._serialized_start=15479 + _globals['_SHAREADMINRECORD']._serialized_end=15556 + _globals['_SHAREDFOLDERUSER']._serialized_start=15558 + _globals['_SHAREDFOLDERUSER']._serialized_end=15628 + _globals['_SHAREDFOLDERTEAM']._serialized_start=15630 + _globals['_SHAREDFOLDERTEAM']._serialized_end=15691 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15693 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15740 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15742 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15792 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15794 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15848 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15850 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15909 + _globals['_LINKEDRECORD']._serialized_start=15911 + _globals['_LINKEDRECORD']._serialized_end=15963 + _globals['_GETSHARINGADMINSREQUEST']._serialized_start=15965 + _globals['_GETSHARINGADMINSREQUEST']._serialized_end=16052 + _globals['_USERPROFILEEXT']._serialized_start=16055 + _globals['_USERPROFILEEXT']._serialized_end=16279 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16281 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16360 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16362 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16457 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16459 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16575 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16578 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16749 + _globals['_TYPEDKEY']._serialized_start=16751 + _globals['_TYPEDKEY']._serialized_end=16821 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16823 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=16938 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=16941 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=17137 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=17140 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17299 + _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_start=17301 + _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_end=17370 + _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_start=17372 + _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_end=17478 + _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_start=17480 + _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_end=17603 + _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_start=17606 + _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_end=17790 + _globals['_DOMAINALIAS']._serialized_start=17792 + _globals['_DOMAINALIAS']._serialized_end=17869 + _globals['_DOMAINALIASREQUEST']._serialized_start=17871 + _globals['_DOMAINALIASREQUEST']._serialized_end=17937 + _globals['_DOMAINALIASRESPONSE']._serialized_start=17939 + _globals['_DOMAINALIASRESPONSE']._serialized_end=18006 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=18008 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=18117 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=18120 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=18558 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=18560 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=18655 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=18657 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18770 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18772 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18869 + _globals['_ENTERPRISEUSERSADD']._serialized_start=18872 + _globals['_ENTERPRISEUSERSADD']._serialized_end=19140 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=19143 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=19298 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=19301 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=19451 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=19454 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=19639 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=19641 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=19698 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=19700 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19811 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19813 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19906 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19908 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=20027 + _globals['_ROLESBYTEAM']._serialized_start=20029 + _globals['_ROLESBYTEAM']._serialized_end=20075 + _globals['_LOCKUSERSREQUEST']._serialized_start=20078 + _globals['_LOCKUSERSREQUEST']._serialized_end=20219 + _globals['_LOCKUSERSRESPONSE']._serialized_start=20221 + _globals['_LOCKUSERSRESPONSE']._serialized_end=20288 + _globals['_LOCKUSERRESPONSE']._serialized_start=20290 + _globals['_LOCKUSERRESPONSE']._serialized_end=20400 # @@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 172e2eee..55a62f59 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi @@ -2,7 +2,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 @@ -103,6 +104,7 @@ class EnterpriseFlagType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): FORBID_ACCOUNT_TRANSFER: _ClassVar[EnterpriseFlagType] NPS_POPUP_OPT_OUT: _ClassVar[EnterpriseFlagType] SHOW_USER_ONBOARD: _ClassVar[EnterpriseFlagType] + FORBID_KEY_TYPE_1: _ClassVar[EnterpriseFlagType] class UserUpdateStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -142,6 +144,15 @@ class ClearSecurityDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): RECALCULATE_SUMMARY_REPORT: _ClassVar[ClearSecurityDataType] FORCE_CLIENT_CHECK_FOR_MISSING_DATA: _ClassVar[ClearSecurityDataType] FORCE_CLIENT_RESEND_SECURITY_DATA: _ClassVar[ClearSecurityDataType] + +class UserLockStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN_LOCK_STATUS: _ClassVar[UserLockStatus] + LOCKED: _ClassVar[UserLockStatus] + DISABLED: _ClassVar[UserLockStatus] + UNLOCKED: _ClassVar[UserLockStatus] + DELETED: _ClassVar[UserLockStatus] + CANT_BE_PENDING: _ClassVar[UserLockStatus] RSA: KeyType ECC: KeyType ROLE_EXISTS: RoleUserModifyStatus @@ -210,6 +221,7 @@ CONSOLE_ONBOARDED: EnterpriseFlagType FORBID_ACCOUNT_TRANSFER: EnterpriseFlagType NPS_POPUP_OPT_OUT: EnterpriseFlagType SHOW_USER_ONBOARD: EnterpriseFlagType +FORBID_KEY_TYPE_1: EnterpriseFlagType USER_UPDATE_OK: UserUpdateStatus USER_UPDATE_ACCESS_DENIED: UserUpdateStatus OK: AuditUserStatus @@ -231,6 +243,12 @@ ERROR: DeleteEnterpriseUsersResult RECALCULATE_SUMMARY_REPORT: ClearSecurityDataType FORCE_CLIENT_CHECK_FOR_MISSING_DATA: ClearSecurityDataType FORCE_CLIENT_RESEND_SECURITY_DATA: ClearSecurityDataType +UNKNOWN_LOCK_STATUS: UserLockStatus +LOCKED: UserLockStatus +DISABLED: UserLockStatus +UNLOCKED: UserLockStatus +DELETED: UserLockStatus +CANT_BE_PENDING: UserLockStatus class EnterpriseKeyPairRequest(_message.Message): __slots__ = ("enterprisePublicKey", "encryptedEnterprisePrivateKey", "keyType") @@ -260,7 +278,7 @@ class EnterpriseUser(_message.Message): enterpriseUsername: str isShareAdmin: bool username: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., email: _Optional[str] = ..., enterpriseUsername: _Optional[str] = ..., isShareAdmin: bool = ..., username: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., email: _Optional[str] = ..., enterpriseUsername: _Optional[str] = ..., isShareAdmin: _Optional[bool] = ..., username: _Optional[str] = ...) -> None: ... class GetTeamMemberResponse(_message.Message): __slots__ = ("enterpriseUser",) @@ -290,7 +308,7 @@ class EncryptedTeamKeyRequest(_message.Message): teamUid: bytes encryptedTeamKey: bytes force: bool - def __init__(self, teamUid: _Optional[bytes] = ..., encryptedTeamKey: _Optional[bytes] = ..., force: bool = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., encryptedTeamKey: _Optional[bytes] = ..., force: _Optional[bool] = ...) -> None: ... class ReEncryptedData(_message.Message): __slots__ = ("id", "data") @@ -492,7 +510,7 @@ class DomainPasswordRulesFields(_message.Message): minimum: int maximum: int allowed: bool - def __init__(self, type: _Optional[str] = ..., minimum: _Optional[int] = ..., maximum: _Optional[int] = ..., allowed: bool = ...) -> None: ... + def __init__(self, type: _Optional[str] = ..., minimum: _Optional[int] = ..., maximum: _Optional[int] = ..., allowed: _Optional[bool] = ...) -> None: ... class LoginToMcRequest(_message.Message): __slots__ = ("mcEnterpriseId", "messageSessionUid") @@ -503,12 +521,14 @@ class LoginToMcRequest(_message.Message): def __init__(self, mcEnterpriseId: _Optional[int] = ..., messageSessionUid: _Optional[bytes] = ...) -> None: ... class LoginToMcResponse(_message.Message): - __slots__ = ("encryptedSessionToken", "encryptedTreeKey") + __slots__ = ("encryptedSessionToken", "encryptedTreeKey", "forbidKeyType2") ENCRYPTEDSESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDTREEKEY_FIELD_NUMBER: _ClassVar[int] + FORBIDKEYTYPE2_FIELD_NUMBER: _ClassVar[int] encryptedSessionToken: bytes encryptedTreeKey: str - def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., encryptedTreeKey: _Optional[str] = ...) -> None: ... + forbidKeyType2: bool + def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., encryptedTreeKey: _Optional[str] = ..., forbidKeyType2: _Optional[bool] = ...) -> None: ... class DomainPasswordRulesResponse(_message.Message): __slots__ = ("domainPasswordRulesFields",) @@ -526,7 +546,7 @@ class ApproveUserDeviceRequest(_message.Message): encryptedDeviceToken: bytes encryptedDeviceDataKey: bytes denyApproval: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: bool = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: _Optional[bool] = ...) -> None: ... class ApproveUserDeviceResponse(_message.Message): __slots__ = ("enterpriseUserId", "encryptedDeviceToken", "failed", "message") @@ -538,7 +558,7 @@ class ApproveUserDeviceResponse(_message.Message): encryptedDeviceToken: bytes failed: bool message: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., failed: bool = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., failed: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... class ApproveUserDevicesRequest(_message.Message): __slots__ = ("deviceRequests",) @@ -626,7 +646,7 @@ class GeneralDataEntity(_message.Message): distributor: bool forbidAccountTransfer: bool showUserOnboard: bool - def __init__(self, enterpriseName: _Optional[str] = ..., restrictVisibility: bool = ..., specialProvisioning: _Optional[_Union[SpecialProvisioning, _Mapping]] = ..., userPrivilege: _Optional[_Union[UserPrivilege, _Mapping]] = ..., distributor: bool = ..., forbidAccountTransfer: bool = ..., showUserOnboard: bool = ...) -> None: ... + def __init__(self, enterpriseName: _Optional[str] = ..., restrictVisibility: _Optional[bool] = ..., specialProvisioning: _Optional[_Union[SpecialProvisioning, _Mapping]] = ..., userPrivilege: _Optional[_Union[UserPrivilege, _Mapping]] = ..., distributor: _Optional[bool] = ..., forbidAccountTransfer: _Optional[bool] = ..., showUserOnboard: _Optional[bool] = ...) -> None: ... class Node(_message.Message): __slots__ = ("nodeId", "parentId", "bridgeId", "scimId", "licenseId", "encryptedData", "duoEnabled", "rsaEnabled", "ssoServiceProviderId", "restrictVisibility", "ssoServiceProviderIds") @@ -652,7 +672,7 @@ class Node(_message.Message): ssoServiceProviderId: int restrictVisibility: bool ssoServiceProviderIds: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, nodeId: _Optional[int] = ..., parentId: _Optional[int] = ..., bridgeId: _Optional[int] = ..., scimId: _Optional[int] = ..., licenseId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., duoEnabled: bool = ..., rsaEnabled: bool = ..., ssoServiceProviderId: _Optional[int] = ..., restrictVisibility: bool = ..., ssoServiceProviderIds: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., parentId: _Optional[int] = ..., bridgeId: _Optional[int] = ..., scimId: _Optional[int] = ..., licenseId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., duoEnabled: _Optional[bool] = ..., rsaEnabled: _Optional[bool] = ..., ssoServiceProviderId: _Optional[int] = ..., restrictVisibility: _Optional[bool] = ..., ssoServiceProviderIds: _Optional[_Iterable[int]] = ...) -> None: ... class Role(_message.Message): __slots__ = ("roleId", "nodeId", "encryptedData", "keyType", "visibleBelow", "newUserInherit", "roleType") @@ -670,7 +690,7 @@ class Role(_message.Message): visibleBelow: bool newUserInherit: bool roleType: str - def __init__(self, roleId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., visibleBelow: bool = ..., newUserInherit: bool = ..., roleType: _Optional[str] = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., visibleBelow: _Optional[bool] = ..., newUserInherit: _Optional[bool] = ..., roleType: _Optional[str] = ...) -> None: ... class User(_message.Message): __slots__ = ("enterpriseUserId", "nodeId", "encryptedData", "keyType", "username", "status", "lock", "userId", "accountShareExpiration", "fullName", "jobTitle", "tfaEnabled", "transferAcceptanceStatus") @@ -700,7 +720,7 @@ class User(_message.Message): jobTitle: str tfaEnabled: bool transferAcceptanceStatus: TransferAcceptanceStatus - def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., username: _Optional[str] = ..., status: _Optional[str] = ..., lock: _Optional[int] = ..., userId: _Optional[int] = ..., accountShareExpiration: _Optional[int] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., tfaEnabled: bool = ..., transferAcceptanceStatus: _Optional[_Union[TransferAcceptanceStatus, str]] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., username: _Optional[str] = ..., status: _Optional[str] = ..., lock: _Optional[int] = ..., userId: _Optional[int] = ..., accountShareExpiration: _Optional[int] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., tfaEnabled: _Optional[bool] = ..., transferAcceptanceStatus: _Optional[_Union[TransferAcceptanceStatus, str]] = ...) -> None: ... class UserAlias(_message.Message): __slots__ = ("enterpriseUserId", "username") @@ -736,7 +756,7 @@ class ManagedNode(_message.Message): roleId: int managedNodeId: int cascadeNodeManagement: bool - def __init__(self, roleId: _Optional[int] = ..., managedNodeId: _Optional[int] = ..., cascadeNodeManagement: bool = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., managedNodeId: _Optional[int] = ..., cascadeNodeManagement: _Optional[bool] = ...) -> None: ... class UserManagedNode(_message.Message): __slots__ = ("nodeId", "cascadeNodeManagement", "privileges") @@ -746,7 +766,7 @@ class UserManagedNode(_message.Message): nodeId: int cascadeNodeManagement: bool privileges: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, nodeId: _Optional[int] = ..., cascadeNodeManagement: bool = ..., privileges: _Optional[_Iterable[str]] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., cascadeNodeManagement: _Optional[bool] = ..., privileges: _Optional[_Iterable[str]] = ...) -> None: ... class UserPrivilege(_message.Message): __slots__ = ("userManagedNodes", "enterpriseUserId", "encryptedData") @@ -776,6 +796,16 @@ class RolePrivilege(_message.Message): privilegeType: str def __init__(self, managedNodeId: _Optional[int] = ..., roleId: _Optional[int] = ..., privilegeType: _Optional[str] = ...) -> None: ... +class PrivilegesByManagedNode(_message.Message): + __slots__ = ("managedNodeId", "roleId", "privileges") + MANAGEDNODEID_FIELD_NUMBER: _ClassVar[int] + ROLEID_FIELD_NUMBER: _ClassVar[int] + PRIVILEGES_FIELD_NUMBER: _ClassVar[int] + managedNodeId: int + roleId: int + privileges: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, managedNodeId: _Optional[int] = ..., roleId: _Optional[int] = ..., privileges: _Optional[_Iterable[str]] = ...) -> None: ... + class RoleEnforcement(_message.Message): __slots__ = ("roleId", "enforcementType", "value") ROLEID_FIELD_NUMBER: _ClassVar[int] @@ -804,7 +834,7 @@ class Team(_message.Message): restrictView: bool encryptedData: str encryptedTeamKey: str - def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., nodeId: _Optional[int] = ..., restrictEdit: bool = ..., restrictShare: bool = ..., restrictView: bool = ..., encryptedData: _Optional[str] = ..., encryptedTeamKey: _Optional[str] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., nodeId: _Optional[int] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ..., restrictView: _Optional[bool] = ..., encryptedData: _Optional[str] = ..., encryptedTeamKey: _Optional[str] = ...) -> None: ... class TeamUser(_message.Message): __slots__ = ("teamUid", "enterpriseUserId", "userType") @@ -850,7 +880,7 @@ class MspInfo(_message.Message): managedCompanies: _containers.RepeatedCompositeFieldContainer[ManagedCompany] allowUnlimitedLicenses: bool addOns: _containers.RepeatedCompositeFieldContainer[LicenseAddOn] - def __init__(self, enterpriseId: _Optional[int] = ..., enterpriseName: _Optional[str] = ..., allocatedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., managedCompanies: _Optional[_Iterable[_Union[ManagedCompany, _Mapping]]] = ..., allowUnlimitedLicenses: bool = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... + def __init__(self, enterpriseId: _Optional[int] = ..., enterpriseName: _Optional[str] = ..., allocatedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., managedCompanies: _Optional[_Iterable[_Union[ManagedCompany, _Mapping]]] = ..., allowUnlimitedLicenses: _Optional[bool] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... class ManagedCompany(_message.Message): __slots__ = ("mcEnterpriseId", "mcEnterpriseName", "mspNodeId", "numberOfSeats", "numberOfUsers", "productId", "isExpired", "treeKey", "tree_key_role", "filePlanType", "addOns") @@ -876,7 +906,7 @@ class ManagedCompany(_message.Message): tree_key_role: int filePlanType: str addOns: _containers.RepeatedCompositeFieldContainer[LicenseAddOn] - def __init__(self, mcEnterpriseId: _Optional[int] = ..., mcEnterpriseName: _Optional[str] = ..., mspNodeId: _Optional[int] = ..., numberOfSeats: _Optional[int] = ..., numberOfUsers: _Optional[int] = ..., productId: _Optional[str] = ..., isExpired: bool = ..., treeKey: _Optional[str] = ..., tree_key_role: _Optional[int] = ..., filePlanType: _Optional[str] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... + def __init__(self, mcEnterpriseId: _Optional[int] = ..., mcEnterpriseName: _Optional[str] = ..., mspNodeId: _Optional[int] = ..., numberOfSeats: _Optional[int] = ..., numberOfUsers: _Optional[int] = ..., productId: _Optional[str] = ..., isExpired: _Optional[bool] = ..., treeKey: _Optional[str] = ..., tree_key_role: _Optional[int] = ..., filePlanType: _Optional[str] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... class MSPPool(_message.Message): __slots__ = ("productId", "seats", "availableSeats", "stash") @@ -922,7 +952,7 @@ class LicenseAddOn(_message.Message): apiCallCount: int tierDescription: str seatsAllocated: int - def __init__(self, name: _Optional[str] = ..., enabled: bool = ..., isTrial: bool = ..., expiration: _Optional[int] = ..., created: _Optional[int] = ..., seats: _Optional[int] = ..., activationTime: _Optional[int] = ..., includedInProduct: bool = ..., apiCallCount: _Optional[int] = ..., tierDescription: _Optional[str] = ..., seatsAllocated: _Optional[int] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., enabled: _Optional[bool] = ..., isTrial: _Optional[bool] = ..., expiration: _Optional[int] = ..., created: _Optional[int] = ..., seats: _Optional[int] = ..., activationTime: _Optional[int] = ..., includedInProduct: _Optional[bool] = ..., apiCallCount: _Optional[int] = ..., tierDescription: _Optional[str] = ..., seatsAllocated: _Optional[int] = ...) -> None: ... class MCDefault(_message.Message): __slots__ = ("mcProduct", "addOns", "filePlanType", "maxLicenses", "fixedMaxLicenses") @@ -936,7 +966,7 @@ class MCDefault(_message.Message): filePlanType: str maxLicenses: int fixedMaxLicenses: bool - def __init__(self, mcProduct: _Optional[str] = ..., addOns: _Optional[_Iterable[str]] = ..., filePlanType: _Optional[str] = ..., maxLicenses: _Optional[int] = ..., fixedMaxLicenses: bool = ...) -> None: ... + def __init__(self, mcProduct: _Optional[str] = ..., addOns: _Optional[_Iterable[str]] = ..., filePlanType: _Optional[str] = ..., maxLicenses: _Optional[int] = ..., fixedMaxLicenses: _Optional[bool] = ...) -> None: ... class MSPPermits(_message.Message): __slots__ = ("restricted", "maxAllowedLicenses", "allowedMcProducts", "allowedAddOns", "maxFilePlanType", "allowUnlimitedLicenses", "mcDefaults") @@ -954,7 +984,7 @@ class MSPPermits(_message.Message): maxFilePlanType: str allowUnlimitedLicenses: bool mcDefaults: _containers.RepeatedCompositeFieldContainer[MCDefault] - def __init__(self, restricted: bool = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: bool = ..., mcDefaults: _Optional[_Iterable[_Union[MCDefault, _Mapping]]] = ...) -> None: ... + def __init__(self, restricted: _Optional[bool] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: _Optional[bool] = ..., mcDefaults: _Optional[_Iterable[_Union[MCDefault, _Mapping]]] = ...) -> None: ... class License(_message.Message): __slots__ = ("paid", "numberOfSeats", "expiration", "licenseKeyId", "productTypeId", "name", "enterpriseLicenseId", "seatsAllocated", "seatsPending", "tier", "filePlanTypeId", "maxBytes", "storageExpiration", "licenseStatus", "mspPool", "managedBy", "addOns", "nextBillingDate", "hasMSPLegacyLog", "mspPermits", "distributor") @@ -1000,7 +1030,7 @@ class License(_message.Message): hasMSPLegacyLog: bool mspPermits: MSPPermits distributor: bool - def __init__(self, paid: bool = ..., numberOfSeats: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseKeyId: _Optional[int] = ..., productTypeId: _Optional[int] = ..., name: _Optional[str] = ..., enterpriseLicenseId: _Optional[int] = ..., seatsAllocated: _Optional[int] = ..., seatsPending: _Optional[int] = ..., tier: _Optional[int] = ..., filePlanTypeId: _Optional[int] = ..., maxBytes: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., licenseStatus: _Optional[str] = ..., mspPool: _Optional[_Iterable[_Union[MSPPool, _Mapping]]] = ..., managedBy: _Optional[_Union[MSPContact, _Mapping]] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ..., nextBillingDate: _Optional[int] = ..., hasMSPLegacyLog: bool = ..., mspPermits: _Optional[_Union[MSPPermits, _Mapping]] = ..., distributor: bool = ...) -> None: ... + def __init__(self, paid: _Optional[bool] = ..., numberOfSeats: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseKeyId: _Optional[int] = ..., productTypeId: _Optional[int] = ..., name: _Optional[str] = ..., enterpriseLicenseId: _Optional[int] = ..., seatsAllocated: _Optional[int] = ..., seatsPending: _Optional[int] = ..., tier: _Optional[int] = ..., filePlanTypeId: _Optional[int] = ..., maxBytes: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., licenseStatus: _Optional[str] = ..., mspPool: _Optional[_Iterable[_Union[MSPPool, _Mapping]]] = ..., managedBy: _Optional[_Union[MSPContact, _Mapping]] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ..., nextBillingDate: _Optional[int] = ..., hasMSPLegacyLog: _Optional[bool] = ..., mspPermits: _Optional[_Union[MSPPermits, _Mapping]] = ..., distributor: _Optional[bool] = ...) -> None: ... class Bridge(_message.Message): __slots__ = ("bridgeId", "nodeId", "wanIpEnforcement", "lanIpEnforcement", "status") @@ -1030,7 +1060,7 @@ class Scim(_message.Message): lastSynced: int rolePrefix: str uniqueGroups: bool - def __init__(self, scimId: _Optional[int] = ..., nodeId: _Optional[int] = ..., status: _Optional[str] = ..., lastSynced: _Optional[int] = ..., rolePrefix: _Optional[str] = ..., uniqueGroups: bool = ...) -> None: ... + def __init__(self, scimId: _Optional[int] = ..., nodeId: _Optional[int] = ..., status: _Optional[str] = ..., lastSynced: _Optional[int] = ..., rolePrefix: _Optional[str] = ..., uniqueGroups: _Optional[bool] = ...) -> None: ... class EmailProvision(_message.Message): __slots__ = ("id", "nodeId", "domain", "method") @@ -1102,7 +1132,7 @@ class SsoService(_message.Message): inviteNewUsers: bool active: bool isCloud: bool - def __init__(self, ssoServiceProviderId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., sp_url: _Optional[str] = ..., inviteNewUsers: bool = ..., active: bool = ..., isCloud: bool = ...) -> None: ... + def __init__(self, ssoServiceProviderId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., sp_url: _Optional[str] = ..., inviteNewUsers: _Optional[bool] = ..., active: _Optional[bool] = ..., isCloud: _Optional[bool] = ...) -> None: ... class ReportFilterUser(_message.Message): __slots__ = ("userId", "email") @@ -1148,7 +1178,7 @@ class EnterpriseData(_message.Message): entity: EnterpriseDataEntity delete: bool data: _containers.RepeatedScalarFieldContainer[bytes] - def __init__(self, entity: _Optional[_Union[EnterpriseDataEntity, str]] = ..., delete: bool = ..., data: _Optional[_Iterable[bytes]] = ...) -> None: ... + def __init__(self, entity: _Optional[_Union[EnterpriseDataEntity, str]] = ..., delete: _Optional[bool] = ..., data: _Optional[_Iterable[bytes]] = ...) -> None: ... class EnterpriseDataResponse(_message.Message): __slots__ = ("continuationToken", "hasMore", "cacheStatus", "data", "generalData") @@ -1162,7 +1192,7 @@ class EnterpriseDataResponse(_message.Message): cacheStatus: CacheStatus data: _containers.RepeatedCompositeFieldContainer[EnterpriseData] generalData: GeneralDataEntity - def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., data: _Optional[_Iterable[_Union[EnterpriseData, _Mapping]]] = ..., generalData: _Optional[_Union[GeneralDataEntity, _Mapping]] = ...) -> None: ... + def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., data: _Optional[_Iterable[_Union[EnterpriseData, _Mapping]]] = ..., generalData: _Optional[_Union[GeneralDataEntity, _Mapping]] = ...) -> None: ... class BackupRequest(_message.Message): __slots__ = ("continuationToken",) @@ -1326,7 +1356,7 @@ class SharedRecordEvent(_message.Message): canEdit: bool canReshare: bool shareFrom: int - def __init__(self, recordUid: _Optional[bytes] = ..., userName: _Optional[str] = ..., canEdit: bool = ..., canReshare: bool = ..., shareFrom: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., userName: _Optional[str] = ..., canEdit: _Optional[bool] = ..., canReshare: _Optional[bool] = ..., shareFrom: _Optional[int] = ...) -> None: ... class SetRestrictVisibilityRequest(_message.Message): __slots__ = ("nodeId",) @@ -1352,7 +1382,7 @@ class UserAddRequest(_message.Message): jobTitle: str email: str suppressEmailInvite: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., email: _Optional[str] = ..., suppressEmailInvite: bool = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., email: _Optional[str] = ..., suppressEmailInvite: _Optional[bool] = ...) -> None: ... class UserUpdateRequest(_message.Message): __slots__ = ("users",) @@ -1398,7 +1428,7 @@ class ComplianceRecordOwnersRequest(_message.Message): INCLUDENONSHARED_FIELD_NUMBER: _ClassVar[int] nodeIds: _containers.RepeatedScalarFieldContainer[int] includeNonShared: bool - def __init__(self, nodeIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ...) -> None: ... + def __init__(self, nodeIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ...) -> None: ... class ComplianceRecordOwnersResponse(_message.Message): __slots__ = ("recordOwners",) @@ -1412,7 +1442,7 @@ class RecordOwner(_message.Message): SHARED_FIELD_NUMBER: _ClassVar[int] enterpriseUserId: int shared: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., shared: bool = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., shared: _Optional[bool] = ...) -> None: ... class PreliminaryComplianceDataRequest(_message.Message): __slots__ = ("enterpriseUserIds", "includeNonShared", "continuationToken", "includeTotalMatchingRecordsInFirstResponse") @@ -1424,7 +1454,7 @@ class PreliminaryComplianceDataRequest(_message.Message): includeNonShared: bool continuationToken: bytes includeTotalMatchingRecordsInFirstResponse: bool - def __init__(self, enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ..., continuationToken: _Optional[bytes] = ..., includeTotalMatchingRecordsInFirstResponse: bool = ...) -> None: ... + def __init__(self, enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ..., continuationToken: _Optional[bytes] = ..., includeTotalMatchingRecordsInFirstResponse: _Optional[bool] = ...) -> None: ... class PreliminaryComplianceDataResponse(_message.Message): __slots__ = ("auditUserData", "continuationToken", "hasMore", "totalMatchingRecords") @@ -1436,7 +1466,7 @@ class PreliminaryComplianceDataResponse(_message.Message): continuationToken: bytes hasMore: bool totalMatchingRecords: int - def __init__(self, auditUserData: _Optional[_Iterable[_Union[AuditUserData, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., totalMatchingRecords: _Optional[int] = ...) -> None: ... + def __init__(self, auditUserData: _Optional[_Iterable[_Union[AuditUserData, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., totalMatchingRecords: _Optional[int] = ...) -> None: ... class AuditUserRecord(_message.Message): __slots__ = ("recordUid", "encryptedData", "shared") @@ -1446,7 +1476,7 @@ class AuditUserRecord(_message.Message): recordUid: bytes encryptedData: bytes shared: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., shared: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., shared: _Optional[bool] = ...) -> None: ... class AuditUserData(_message.Message): __slots__ = ("enterpriseUserId", "auditUserRecords", "status") @@ -1480,7 +1510,7 @@ class ComplianceReportRequest(_message.Message): complianceReportRun: ComplianceReportRun reportName: str saveReport: bool - def __init__(self, complianceReportRun: _Optional[_Union[ComplianceReportRun, _Mapping]] = ..., reportName: _Optional[str] = ..., saveReport: bool = ...) -> None: ... + def __init__(self, complianceReportRun: _Optional[_Union[ComplianceReportRun, _Mapping]] = ..., reportName: _Optional[str] = ..., saveReport: _Optional[bool] = ...) -> None: ... class ComplianceReportRun(_message.Message): __slots__ = ("reportCriteriaAndFilter", "users", "records") @@ -1518,7 +1548,7 @@ class ComplianceReportCriteria(_message.Message): jobTitles: _containers.RepeatedScalarFieldContainer[str] enterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] includeNonShared: bool - def __init__(self, jobTitles: _Optional[_Iterable[str]] = ..., enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ...) -> None: ... + def __init__(self, jobTitles: _Optional[_Iterable[str]] = ..., enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ...) -> None: ... class ComplianceReportFilter(_message.Message): __slots__ = ("recordTitles", "recordUids", "jobTitles", "urls", "recordTypes") @@ -1582,7 +1612,7 @@ class AuditRecord(_message.Message): inTrash: bool treeLeft: int treeRight: int - def __init__(self, recordUid: _Optional[bytes] = ..., auditData: _Optional[bytes] = ..., hasAttachments: bool = ..., inTrash: bool = ..., treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., auditData: _Optional[bytes] = ..., hasAttachments: _Optional[bool] = ..., inTrash: _Optional[bool] = ..., treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ...) -> None: ... class AuditRole(_message.Message): __slots__ = ("roleId", "encryptedData", "restrictShareOutsideEnterprise", "restrictShareAll", "restrictShareOfAttachments", "restrictMaskPasswordsWhileEditing", "roleNodeManagements") @@ -1600,7 +1630,7 @@ class AuditRole(_message.Message): restrictShareOfAttachments: bool restrictMaskPasswordsWhileEditing: bool roleNodeManagements: _containers.RepeatedCompositeFieldContainer[RoleNodeManagement] - def __init__(self, roleId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., restrictShareOutsideEnterprise: bool = ..., restrictShareAll: bool = ..., restrictShareOfAttachments: bool = ..., restrictMaskPasswordsWhileEditing: bool = ..., roleNodeManagements: _Optional[_Iterable[_Union[RoleNodeManagement, _Mapping]]] = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., restrictShareOutsideEnterprise: _Optional[bool] = ..., restrictShareAll: _Optional[bool] = ..., restrictShareOfAttachments: _Optional[bool] = ..., restrictMaskPasswordsWhileEditing: _Optional[bool] = ..., roleNodeManagements: _Optional[_Iterable[_Union[RoleNodeManagement, _Mapping]]] = ...) -> None: ... class RoleNodeManagement(_message.Message): __slots__ = ("treeLeft", "treeRight", "cascade", "privileges") @@ -1612,7 +1642,7 @@ class RoleNodeManagement(_message.Message): treeRight: int cascade: bool privileges: int - def __init__(self, treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ..., cascade: bool = ..., privileges: _Optional[int] = ...) -> None: ... + def __init__(self, treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ..., cascade: _Optional[bool] = ..., privileges: _Optional[int] = ...) -> None: ... class UserProfile(_message.Message): __slots__ = ("enterpriseUserId", "fullName", "jobTitle", "email", "roleIds") @@ -1654,7 +1684,7 @@ class AuditTeam(_message.Message): teamName: str restrictEdit: bool restrictShare: bool - def __init__(self, teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., restrictEdit: bool = ..., restrictShare: bool = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ...) -> None: ... class AuditTeamUser(_message.Message): __slots__ = ("teamUid", "enterpriseUserIds") @@ -1758,7 +1788,7 @@ class UserProfileExt(_message.Message): isShareAdminForRequestedObject: bool isShareAdminForSharedFolderOwner: bool hasAccessToObject: bool - def __init__(self, email: _Optional[str] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., isMSPMCAdmin: bool = ..., isInSharedFolder: bool = ..., isShareAdminForRequestedObject: bool = ..., isShareAdminForSharedFolderOwner: bool = ..., hasAccessToObject: bool = ...) -> None: ... + def __init__(self, email: _Optional[str] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., isMSPMCAdmin: _Optional[bool] = ..., isInSharedFolder: _Optional[bool] = ..., isShareAdminForRequestedObject: _Optional[bool] = ..., isShareAdminForSharedFolderOwner: _Optional[bool] = ..., hasAccessToObject: _Optional[bool] = ...) -> None: ... class GetSharingAdminsResponse(_message.Message): __slots__ = ("userProfileExts",) @@ -1822,7 +1852,7 @@ class TeamsEnterpriseUsersAddTeamResponse(_message.Message): message: str resultCode: str additionalInfo: str - def __init__(self, teamUid: _Optional[bytes] = ..., users: _Optional[_Iterable[_Union[TeamsEnterpriseUsersAddUserResponse, _Mapping]]] = ..., success: bool = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., users: _Optional[_Iterable[_Union[TeamsEnterpriseUsersAddUserResponse, _Mapping]]] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class TeamsEnterpriseUsersAddUserResponse(_message.Message): __slots__ = ("enterpriseUserId", "revision", "success", "message", "resultCode", "additionalInfo") @@ -1838,7 +1868,7 @@ class TeamsEnterpriseUsersAddUserResponse(_message.Message): message: str resultCode: str additionalInfo: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., revision: _Optional[int] = ..., success: bool = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., revision: _Optional[int] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class TeamEnterpriseUserRemove(_message.Message): __slots__ = ("teamUid", "enterpriseUserId") @@ -1872,7 +1902,7 @@ class TeamEnterpriseUserRemoveResponse(_message.Message): resultCode: str message: str additionalInfo: str - def __init__(self, teamEnterpriseUserRemove: _Optional[_Union[TeamEnterpriseUserRemove, _Mapping]] = ..., success: bool = ..., resultCode: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, teamEnterpriseUserRemove: _Optional[_Union[TeamEnterpriseUserRemove, _Mapping]] = ..., success: _Optional[bool] = ..., resultCode: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class DomainAlias(_message.Message): __slots__ = ("domain", "alias", "status", "message") @@ -1992,7 +2022,7 @@ class EnterpriseUsersAdd(_message.Message): inviteeLocale: str move: bool roleId: int - def __init__(self, enterpriseUserId: _Optional[int] = ..., username: _Optional[str] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., suppressEmailInvite: bool = ..., inviteeLocale: _Optional[str] = ..., move: bool = ..., roleId: _Optional[int] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., username: _Optional[str] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., suppressEmailInvite: _Optional[bool] = ..., inviteeLocale: _Optional[str] = ..., move: _Optional[bool] = ..., roleId: _Optional[int] = ...) -> None: ... class EnterpriseUsersAddResponse(_message.Message): __slots__ = ("results", "success", "code", "message", "additionalInfo") @@ -2006,7 +2036,7 @@ class EnterpriseUsersAddResponse(_message.Message): code: str message: str additionalInfo: str - def __init__(self, results: _Optional[_Iterable[_Union[EnterpriseUsersAddResult, _Mapping]]] = ..., success: bool = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, results: _Optional[_Iterable[_Union[EnterpriseUsersAddResult, _Mapping]]] = ..., success: _Optional[bool] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class EnterpriseUsersAddResult(_message.Message): __slots__ = ("enterpriseUserId", "success", "verificationCode", "code", "message", "additionalInfo") @@ -2022,7 +2052,7 @@ class EnterpriseUsersAddResult(_message.Message): code: str message: str additionalInfo: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., success: bool = ..., verificationCode: _Optional[str] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., success: _Optional[bool] = ..., verificationCode: _Optional[str] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class UpdateMSPPermitsRequest(_message.Message): __slots__ = ("mspEnterpriseId", "maxAllowedLicenses", "allowedMcProducts", "allowedAddOns", "maxFilePlanType", "allowUnlimitedLicenses") @@ -2038,7 +2068,7 @@ class UpdateMSPPermitsRequest(_message.Message): allowedAddOns: _containers.RepeatedScalarFieldContainer[str] maxFilePlanType: str allowUnlimitedLicenses: bool - def __init__(self, mspEnterpriseId: _Optional[int] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: bool = ...) -> None: ... + def __init__(self, mspEnterpriseId: _Optional[int] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: _Optional[bool] = ...) -> None: ... class DeleteEnterpriseUsersRequest(_message.Message): __slots__ = ("enterpriseUserIds",) @@ -2068,4 +2098,40 @@ class ClearSecurityDataRequest(_message.Message): enterpriseUserId: _containers.RepeatedScalarFieldContainer[int] allUsers: bool type: ClearSecurityDataType - def __init__(self, enterpriseUserId: _Optional[_Iterable[int]] = ..., allUsers: bool = ..., type: _Optional[_Union[ClearSecurityDataType, str]] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[_Iterable[int]] = ..., allUsers: _Optional[bool] = ..., type: _Optional[_Union[ClearSecurityDataType, str]] = ...) -> None: ... + +class RolesByTeam(_message.Message): + __slots__ = ("teamUid", "roleId") + TEAMUID_FIELD_NUMBER: _ClassVar[int] + ROLEID_FIELD_NUMBER: _ClassVar[int] + teamUid: bytes + roleId: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, teamUid: _Optional[bytes] = ..., roleId: _Optional[_Iterable[int]] = ...) -> None: ... + +class LockUsersRequest(_message.Message): + __slots__ = ("lockEnterpriseUserIds", "disableEnterpriseUserIds", "unlockEnterpriseUserIds", "deleteIfPending") + LOCKENTERPRISEUSERIDS_FIELD_NUMBER: _ClassVar[int] + DISABLEENTERPRISEUSERIDS_FIELD_NUMBER: _ClassVar[int] + UNLOCKENTERPRISEUSERIDS_FIELD_NUMBER: _ClassVar[int] + DELETEIFPENDING_FIELD_NUMBER: _ClassVar[int] + lockEnterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] + disableEnterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] + unlockEnterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] + deleteIfPending: bool + def __init__(self, lockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., disableEnterpriseUserIds: _Optional[_Iterable[int]] = ..., unlockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., deleteIfPending: _Optional[bool] = ...) -> None: ... + +class LockUsersResponse(_message.Message): + __slots__ = ("response",) + RESPONSE_FIELD_NUMBER: _ClassVar[int] + response: _containers.RepeatedCompositeFieldContainer[LockUserResponse] + def __init__(self, response: _Optional[_Iterable[_Union[LockUserResponse, _Mapping]]] = ...) -> None: ... + +class LockUserResponse(_message.Message): + __slots__ = ("enterpriseUserId", "status", "errorMessage") + ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + ERRORMESSAGE_FIELD_NUMBER: _ClassVar[int] + enterpriseUserId: int + status: UserLockStatus + errorMessage: str + def __init__(self, enterpriseUserId: _Optional[int] = ..., status: _Optional[_Union[UserLockStatus, str]] = ..., errorMessage: _Optional[str] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/folder_pb2.py b/keepersdk-package/src/keepersdk/proto/folder_pb2.py index 7543a01b..aca26a46 100644 --- a/keepersdk-package/src/keepersdk/proto/folder_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/folder_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: folder.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'folder.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi b/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi index 2dbcee3b..1f4fb01d 100644 --- a/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi @@ -3,7 +3,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 @@ -135,7 +136,7 @@ class SharedFolderFields(_message.Message): manageRecords: bool canEdit: bool canShare: bool - def __init__(self, encryptedFolderName: _Optional[bytes] = ..., manageUsers: bool = ..., manageRecords: bool = ..., canEdit: bool = ..., canShare: bool = ...) -> None: ... + def __init__(self, encryptedFolderName: _Optional[bytes] = ..., manageUsers: _Optional[bool] = ..., manageRecords: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., canShare: _Optional[bool] = ...) -> None: ... class SharedFolderFolderFields(_message.Message): __slots__ = ("sharedFolderUid",) @@ -209,7 +210,7 @@ class SharedFolderUpdateRecord(_message.Message): expiration: int timerNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, recordUid: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., canEdit: _Optional[_Union[SetBooleanValue, str]] = ..., canShare: _Optional[_Union[SetBooleanValue, str]] = ..., encryptedRecordKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., canEdit: _Optional[_Union[SetBooleanValue, str]] = ..., canShare: _Optional[_Union[SetBooleanValue, str]] = ..., encryptedRecordKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderUpdateUser(_message.Message): __slots__ = ("username", "manageUsers", "manageRecords", "sharedFolderKey", "expiration", "timerNotificationType", "typedSharedFolderKey", "rotateOnExpiration") @@ -229,7 +230,7 @@ class SharedFolderUpdateUser(_message.Message): timerNotificationType: _record_pb2.TimerNotificationType typedSharedFolderKey: EncryptedDataKey rotateOnExpiration: bool - def __init__(self, username: _Optional[str] = ..., manageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., manageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., manageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., manageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderUpdateTeam(_message.Message): __slots__ = ("teamUid", "manageUsers", "manageRecords", "sharedFolderKey", "expiration", "timerNotificationType", "typedSharedFolderKey", "rotateOnExpiration") @@ -249,7 +250,7 @@ class SharedFolderUpdateTeam(_message.Message): timerNotificationType: _record_pb2.TimerNotificationType typedSharedFolderKey: EncryptedDataKey rotateOnExpiration: bool - def __init__(self, teamUid: _Optional[bytes] = ..., manageUsers: bool = ..., manageRecords: bool = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., manageUsers: _Optional[bool] = ..., manageRecords: _Optional[bool] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderUpdateV3Request(_message.Message): __slots__ = ("sharedFolderUpdateOperation_dont_use", "sharedFolderUid", "encryptedSharedFolderName", "revision", "forceUpdate", "fromTeamUid", "defaultManageUsers", "defaultManageRecords", "defaultCanEdit", "defaultCanShare", "sharedFolderAddRecord", "sharedFolderAddUser", "sharedFolderAddTeam", "sharedFolderUpdateRecord", "sharedFolderUpdateUser", "sharedFolderUpdateTeam", "sharedFolderRemoveRecord", "sharedFolderRemoveUser", "sharedFolderRemoveTeam", "sharedFolderOwner") @@ -293,7 +294,7 @@ class SharedFolderUpdateV3Request(_message.Message): sharedFolderRemoveUser: _containers.RepeatedScalarFieldContainer[str] sharedFolderRemoveTeam: _containers.RepeatedScalarFieldContainer[bytes] sharedFolderOwner: str - def __init__(self, sharedFolderUpdateOperation_dont_use: _Optional[int] = ..., sharedFolderUid: _Optional[bytes] = ..., encryptedSharedFolderName: _Optional[bytes] = ..., revision: _Optional[int] = ..., forceUpdate: bool = ..., fromTeamUid: _Optional[bytes] = ..., defaultManageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., defaultManageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanEdit: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanShare: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderAddRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderAddUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderAddTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderUpdateRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderUpdateUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderUpdateTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderRemoveRecord: _Optional[_Iterable[bytes]] = ..., sharedFolderRemoveUser: _Optional[_Iterable[str]] = ..., sharedFolderRemoveTeam: _Optional[_Iterable[bytes]] = ..., sharedFolderOwner: _Optional[str] = ...) -> None: ... + def __init__(self, sharedFolderUpdateOperation_dont_use: _Optional[int] = ..., sharedFolderUid: _Optional[bytes] = ..., encryptedSharedFolderName: _Optional[bytes] = ..., revision: _Optional[int] = ..., forceUpdate: _Optional[bool] = ..., fromTeamUid: _Optional[bytes] = ..., defaultManageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., defaultManageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanEdit: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanShare: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderAddRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderAddUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderAddTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderUpdateRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderUpdateUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderUpdateTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderRemoveRecord: _Optional[_Iterable[bytes]] = ..., sharedFolderRemoveUser: _Optional[_Iterable[str]] = ..., sharedFolderRemoveTeam: _Optional[_Iterable[bytes]] = ..., sharedFolderOwner: _Optional[str] = ...) -> None: ... class SharedFolderUpdateV3RequestV2(_message.Message): __slots__ = ("sharedFoldersUpdateV3",) diff --git a/keepersdk-package/src/keepersdk/proto/pam_pb2.py b/keepersdk-package/src/keepersdk/proto/pam_pb2.py index 1aae1ed0..2bc86c21 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pam_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pam.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'pam.proto' ) @@ -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\"\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*\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*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\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\"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\"\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\"\xd5\x01\n\x13\x43nappWebhookRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\x12\x10\n\x08\x63lientId\x18\x03 \x01(\t\x12\x14\n\x0c\x63lientSecret\x18\x04 \x01(\t\x12\x16\n\x0e\x61piEndpointUrl\x18\x05 \x01(\t\x12\x0f\n\x07\x61uthUrl\x18\x06 \x01(\t\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x07 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x08 \x01(\x0c\x12\x11\n\twebhookId\x18\t \x01(\t\"S\n\x14\x43nappWebhookResponse\x12\x11\n\twebhookId\x18\x01 \x01(\t\x12\x12\n\nwebhookUrl\x18\x02 \x01(\t\x12\x14\n\x0cwebhookToken\x18\x03 \x01(\t\"/\n\x19\x43nappDeleteWebhookRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\"\x80\x01\n\x1b\x43nappTestCredentialsRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x10\n\x08\x63lientId\x18\x02 \x01(\t\x12\x14\n\x0c\x63lientSecret\x18\x03 \x01(\t\x12\x16\n\x0e\x61piEndpointUrl\x18\x04 \x01(\t\x12\x0f\n\x07\x61uthUrl\x18\x05 \x01(\t\"M\n\x1c\x43nappTestCredentialsResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\"`\n\x15\x43nappQueueListRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x0cstatusFilter\x18\x02 \x01(\x05\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\"\xbb\x01\n\x0e\x43nappQueueItem\x12\x14\n\x0c\x63nappQueueId\x18\x01 \x01(\t\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x1a\n\x12\x63nappQueueStatusId\x18\x04 \x01(\x05\x12\x12\n\nreceivedAt\x18\x05 \x01(\x03\x12\x12\n\nresolvedAt\x18\x06 \x01(\x03\x12\x11\n\trecordUid\x18\x07 \x01(\x0c\x12\x0f\n\x07payload\x18\x08 \x01(\x0c\"j\n\x16\x43nappQueueListResponse\x12\"\n\x05items\x18\x01 \x03(\x0b\x32\x13.PAM.CnappQueueItem\x12\r\n\x05total\x18\x02 \x01(\x05\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x03 \x01(\x0c\"\x8c\x02\n\x16\x43nappQueueItemResponse\x12\x14\n\x0c\x63nappQueueId\x18\x01 \x01(\t\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x1a\n\x12\x63nappQueueStatusId\x18\x04 \x01(\x05\x12\x12\n\nreceivedAt\x18\x05 \x01(\x03\x12\x12\n\nresolvedAt\x18\x06 \x01(\x03\x12\x11\n\trecordUid\x18\x07 \x01(\x0c\x12\x0f\n\x07payload\x18\x08 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\t \x01(\x0c\x12\x11\n\tnetworkId\x18\n \x01(\x0c\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x0b \x01(\x0c\"E\n\x15\x43nappAssociateRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65xecuteAfterSetup\x18\x02 \x01(\x08\"F\n\x16\x43nappAssociateResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x1c\n\x14remediationTriggered\x18\x02 \x01(\x08\".\n\x13\x43nappResolveRequest\x12\x17\n\x0fresolutionNotes\x18\x01 \x01(\t\"+\n\x15\x43nappRemediateRequest\x12\x12\n\nactionType\x18\x01 \x01(\t\"e\n\x16\x43nappRemediateResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x12\n\nactionType\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\x17\n\x0f\x65xecutionTimeMs\x18\x04 \x01(\x03\"$\n\x12\x43nappIgnoreRequest\x12\x0e\n\x06reason\x18\x01 \x01(\t\"\x8d\x01\n\x1b\x43nappDefaultBehaviorRequest\x12\x11\n\tnetworkId\x18\x01 \x01(\x0c\x12\x17\n\x0f\x63nappProviderId\x18\x02 \x01(\x05\x12\x12\n\ncontrolKey\x18\x03 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x04 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x05 \x01(\x08\">\n\x1c\x43nappDefaultBehaviorResponse\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05\".\n\x18\x43nappBehaviorListRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\"I\n\x19\x43nappBehaviorListResponse\x12,\n\x05items\x18\x01 \x03(\x0b\x32\x1d.PAM.CnappDefaultBehaviorItem\"\xa7\x01\n\x18\x43nappDefaultBehaviorItem\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x11\n\tnetworkId\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x12\n\ncontrolKey\x18\x04 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x05 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x06 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x07 \x01(\x08\"\x91\x01\n\x1a\x43nappBehaviorUpdateRequest\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x03 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x04 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x05 \x01(\x08\"<\n\x1a\x43nappBehaviorDeleteRequest\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05*\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*\\\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*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\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,18 +34,18 @@ 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=3796 - _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=3938 - _globals['_PAMOPERATIONTYPE']._serialized_start=3940 - _globals['_PAMOPERATIONTYPE']._serialized_end=4004 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=4006 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=4118 - _globals['_CONTROLLERMESSAGETYPE']._serialized_start=4120 - _globals['_CONTROLLERMESSAGETYPE']._serialized_end=4212 - _globals['_PAMRECORDINGTYPE']._serialized_start=4214 - _globals['_PAMRECORDINGTYPE']._serialized_end=4300 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=4302 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=4407 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=6405 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=6563 + _globals['_PAMOPERATIONTYPE']._serialized_start=6565 + _globals['_PAMOPERATIONTYPE']._serialized_end=6629 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=6631 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=6743 + _globals['_CONTROLLERMESSAGETYPE']._serialized_start=6745 + _globals['_CONTROLLERMESSAGETYPE']._serialized_end=6837 + _globals['_PAMRECORDINGTYPE']._serialized_start=6839 + _globals['_PAMRECORDINGTYPE']._serialized_end=6925 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=6927 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=7032 _globals['_PAMROTATIONSCHEDULE']._serialized_start=51 _globals['_PAMROTATIONSCHEDULE']._serialized_end=182 _globals['_PAMROTATIONSCHEDULESRESPONSE']._serialized_start=184 @@ -110,4 +110,52 @@ _globals['_UIDLIST']._serialized_end=3402 _globals['_PAMRESOURCECONFIG']._serialized_start=3405 _globals['_PAMRESOURCECONFIG']._serialized_end=3793 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_start=3795 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=3832 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=3835 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4087 + _globals['_CNAPPWEBHOOKREQUEST']._serialized_start=4090 + _globals['_CNAPPWEBHOOKREQUEST']._serialized_end=4303 + _globals['_CNAPPWEBHOOKRESPONSE']._serialized_start=4305 + _globals['_CNAPPWEBHOOKRESPONSE']._serialized_end=4388 + _globals['_CNAPPDELETEWEBHOOKREQUEST']._serialized_start=4390 + _globals['_CNAPPDELETEWEBHOOKREQUEST']._serialized_end=4437 + _globals['_CNAPPTESTCREDENTIALSREQUEST']._serialized_start=4440 + _globals['_CNAPPTESTCREDENTIALSREQUEST']._serialized_end=4568 + _globals['_CNAPPTESTCREDENTIALSRESPONSE']._serialized_start=4570 + _globals['_CNAPPTESTCREDENTIALSRESPONSE']._serialized_end=4647 + _globals['_CNAPPQUEUELISTREQUEST']._serialized_start=4649 + _globals['_CNAPPQUEUELISTREQUEST']._serialized_end=4745 + _globals['_CNAPPQUEUEITEM']._serialized_start=4748 + _globals['_CNAPPQUEUEITEM']._serialized_end=4935 + _globals['_CNAPPQUEUELISTRESPONSE']._serialized_start=4937 + _globals['_CNAPPQUEUELISTRESPONSE']._serialized_end=5043 + _globals['_CNAPPQUEUEITEMRESPONSE']._serialized_start=5046 + _globals['_CNAPPQUEUEITEMRESPONSE']._serialized_end=5314 + _globals['_CNAPPASSOCIATEREQUEST']._serialized_start=5316 + _globals['_CNAPPASSOCIATEREQUEST']._serialized_end=5385 + _globals['_CNAPPASSOCIATERESPONSE']._serialized_start=5387 + _globals['_CNAPPASSOCIATERESPONSE']._serialized_end=5457 + _globals['_CNAPPRESOLVEREQUEST']._serialized_start=5459 + _globals['_CNAPPRESOLVEREQUEST']._serialized_end=5505 + _globals['_CNAPPREMEDIATEREQUEST']._serialized_start=5507 + _globals['_CNAPPREMEDIATEREQUEST']._serialized_end=5550 + _globals['_CNAPPREMEDIATERESPONSE']._serialized_start=5552 + _globals['_CNAPPREMEDIATERESPONSE']._serialized_end=5653 + _globals['_CNAPPIGNOREREQUEST']._serialized_start=5655 + _globals['_CNAPPIGNOREREQUEST']._serialized_end=5691 + _globals['_CNAPPDEFAULTBEHAVIORREQUEST']._serialized_start=5694 + _globals['_CNAPPDEFAULTBEHAVIORREQUEST']._serialized_end=5835 + _globals['_CNAPPDEFAULTBEHAVIORRESPONSE']._serialized_start=5837 + _globals['_CNAPPDEFAULTBEHAVIORRESPONSE']._serialized_end=5899 + _globals['_CNAPPBEHAVIORLISTREQUEST']._serialized_start=5901 + _globals['_CNAPPBEHAVIORLISTREQUEST']._serialized_end=5947 + _globals['_CNAPPBEHAVIORLISTRESPONSE']._serialized_start=5949 + _globals['_CNAPPBEHAVIORLISTRESPONSE']._serialized_end=6022 + _globals['_CNAPPDEFAULTBEHAVIORITEM']._serialized_start=6025 + _globals['_CNAPPDEFAULTBEHAVIORITEM']._serialized_end=6192 + _globals['_CNAPPBEHAVIORUPDATEREQUEST']._serialized_start=6195 + _globals['_CNAPPBEHAVIORUPDATEREQUEST']._serialized_end=6340 + _globals['_CNAPPBEHAVIORDELETEREQUEST']._serialized_start=6342 + _globals['_CNAPPBEHAVIORDELETEREQUEST']._serialized_end=6402 # @@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 e7a5ffc3..5c5acb4e 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi +++ b/keepersdk-package/src/keepersdk/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 @@ -20,6 +21,7 @@ class WebRtcConnectionType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): MYSQL: _ClassVar[WebRtcConnectionType] SQL_SERVER: _ClassVar[WebRtcConnectionType] POSTGRESQL: _ClassVar[WebRtcConnectionType] + KUBERNETES: _ClassVar[WebRtcConnectionType] class PAMOperationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -66,6 +68,7 @@ TELNET: WebRtcConnectionType MYSQL: WebRtcConnectionType SQL_SERVER: WebRtcConnectionType POSTGRESQL: WebRtcConnectionType +KUBERNETES: WebRtcConnectionType ADD: PAMOperationType UPDATE: PAMOperationType REPLACE: PAMOperationType @@ -100,7 +103,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",) @@ -300,7 +303,7 @@ 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: ... + 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] = ...) -> None: ... class PAMSetMaxInstanceCountRequest(_message.Message): __slots__ = ("controllerUid", "maxInstanceCount") @@ -404,7 +407,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") @@ -441,3 +444,271 @@ class PAMResourceConfig(_message.Message): jitSettings: bytes keeperAiSettings: 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] = ..., keeperAiSettings: _Optional[bytes] = ...) -> None: ... + +class PAMUniversalSyncFolder(_message.Message): + __slots__ = ("uid",) + UID_FIELD_NUMBER: _ClassVar[int] + uid: bytes + def __init__(self, uid: _Optional[bytes] = ...) -> None: ... + +class PAMUniversalSyncConfig(_message.Message): + __slots__ = ("networkUid", "enabled", "dryRunEnabled", "folders", "syncIdentity", "vaultName") + 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] + networkUid: bytes + enabled: bool + dryRunEnabled: bool + folders: _containers.RepeatedCompositeFieldContainer[PAMUniversalSyncFolder] + syncIdentity: bytes + vaultName: bytes + def __init__(self, networkUid: _Optional[bytes] = ..., enabled: _Optional[bool] = ..., dryRunEnabled: _Optional[bool] = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ...) -> None: ... + +class CnappWebhookRequest(_message.Message): + __slots__ = ("networkUid", "provider", "clientId", "clientSecret", "apiEndpointUrl", "authUrl", "encryptionRecordKeyId", "controllerUid", "webhookId") + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + PROVIDER_FIELD_NUMBER: _ClassVar[int] + CLIENTID_FIELD_NUMBER: _ClassVar[int] + CLIENTSECRET_FIELD_NUMBER: _ClassVar[int] + APIENDPOINTURL_FIELD_NUMBER: _ClassVar[int] + AUTHURL_FIELD_NUMBER: _ClassVar[int] + ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] + CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] + WEBHOOKID_FIELD_NUMBER: _ClassVar[int] + networkUid: bytes + provider: str + clientId: str + clientSecret: str + apiEndpointUrl: str + authUrl: str + encryptionRecordKeyId: bytes + controllerUid: bytes + webhookId: str + def __init__(self, networkUid: _Optional[bytes] = ..., provider: _Optional[str] = ..., clientId: _Optional[str] = ..., clientSecret: _Optional[str] = ..., apiEndpointUrl: _Optional[str] = ..., authUrl: _Optional[str] = ..., encryptionRecordKeyId: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., webhookId: _Optional[str] = ...) -> None: ... + +class CnappWebhookResponse(_message.Message): + __slots__ = ("webhookId", "webhookUrl", "webhookToken") + WEBHOOKID_FIELD_NUMBER: _ClassVar[int] + WEBHOOKURL_FIELD_NUMBER: _ClassVar[int] + WEBHOOKTOKEN_FIELD_NUMBER: _ClassVar[int] + webhookId: str + webhookUrl: str + webhookToken: str + def __init__(self, webhookId: _Optional[str] = ..., webhookUrl: _Optional[str] = ..., webhookToken: _Optional[str] = ...) -> None: ... + +class CnappDeleteWebhookRequest(_message.Message): + __slots__ = ("networkUid",) + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + networkUid: bytes + def __init__(self, networkUid: _Optional[bytes] = ...) -> None: ... + +class CnappTestCredentialsRequest(_message.Message): + __slots__ = ("provider", "clientId", "clientSecret", "apiEndpointUrl", "authUrl") + PROVIDER_FIELD_NUMBER: _ClassVar[int] + CLIENTID_FIELD_NUMBER: _ClassVar[int] + CLIENTSECRET_FIELD_NUMBER: _ClassVar[int] + APIENDPOINTURL_FIELD_NUMBER: _ClassVar[int] + AUTHURL_FIELD_NUMBER: _ClassVar[int] + provider: str + clientId: str + clientSecret: str + apiEndpointUrl: str + authUrl: str + def __init__(self, provider: _Optional[str] = ..., clientId: _Optional[str] = ..., clientSecret: _Optional[str] = ..., apiEndpointUrl: _Optional[str] = ..., authUrl: _Optional[str] = ...) -> None: ... + +class CnappTestCredentialsResponse(_message.Message): + __slots__ = ("valid", "error", "message") + VALID_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + valid: bool + error: str + message: str + def __init__(self, valid: _Optional[bool] = ..., error: _Optional[str] = ..., message: _Optional[str] = ...) -> None: ... + +class CnappQueueListRequest(_message.Message): + __slots__ = ("networkUid", "statusFilter", "limit", "offset") + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + STATUSFILTER_FIELD_NUMBER: _ClassVar[int] + LIMIT_FIELD_NUMBER: _ClassVar[int] + OFFSET_FIELD_NUMBER: _ClassVar[int] + networkUid: bytes + statusFilter: int + limit: int + offset: int + def __init__(self, networkUid: _Optional[bytes] = ..., statusFilter: _Optional[int] = ..., limit: _Optional[int] = ..., offset: _Optional[int] = ...) -> None: ... + +class CnappQueueItem(_message.Message): + __slots__ = ("cnappQueueId", "controlKey", "cnappProviderId", "cnappQueueStatusId", "receivedAt", "resolvedAt", "recordUid", "payload") + CNAPPQUEUEID_FIELD_NUMBER: _ClassVar[int] + CONTROLKEY_FIELD_NUMBER: _ClassVar[int] + CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] + CNAPPQUEUESTATUSID_FIELD_NUMBER: _ClassVar[int] + RECEIVEDAT_FIELD_NUMBER: _ClassVar[int] + RESOLVEDAT_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + cnappQueueId: str + controlKey: str + cnappProviderId: int + cnappQueueStatusId: int + receivedAt: int + resolvedAt: int + recordUid: bytes + payload: bytes + def __init__(self, cnappQueueId: _Optional[str] = ..., controlKey: _Optional[str] = ..., cnappProviderId: _Optional[int] = ..., cnappQueueStatusId: _Optional[int] = ..., receivedAt: _Optional[int] = ..., resolvedAt: _Optional[int] = ..., recordUid: _Optional[bytes] = ..., payload: _Optional[bytes] = ...) -> None: ... + +class CnappQueueListResponse(_message.Message): + __slots__ = ("items", "total", "encryptionRecordKeyId") + ITEMS_FIELD_NUMBER: _ClassVar[int] + TOTAL_FIELD_NUMBER: _ClassVar[int] + ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] + items: _containers.RepeatedCompositeFieldContainer[CnappQueueItem] + total: int + encryptionRecordKeyId: bytes + def __init__(self, items: _Optional[_Iterable[_Union[CnappQueueItem, _Mapping]]] = ..., total: _Optional[int] = ..., encryptionRecordKeyId: _Optional[bytes] = ...) -> None: ... + +class CnappQueueItemResponse(_message.Message): + __slots__ = ("cnappQueueId", "controlKey", "cnappProviderId", "cnappQueueStatusId", "receivedAt", "resolvedAt", "recordUid", "payload", "controllerUid", "networkId", "encryptionRecordKeyId") + CNAPPQUEUEID_FIELD_NUMBER: _ClassVar[int] + CONTROLKEY_FIELD_NUMBER: _ClassVar[int] + CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] + CNAPPQUEUESTATUSID_FIELD_NUMBER: _ClassVar[int] + RECEIVEDAT_FIELD_NUMBER: _ClassVar[int] + RESOLVEDAT_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] + PAYLOAD_FIELD_NUMBER: _ClassVar[int] + CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] + NETWORKID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] + cnappQueueId: str + controlKey: str + cnappProviderId: int + cnappQueueStatusId: int + receivedAt: int + resolvedAt: int + recordUid: bytes + payload: bytes + controllerUid: bytes + networkId: bytes + encryptionRecordKeyId: bytes + def __init__(self, cnappQueueId: _Optional[str] = ..., controlKey: _Optional[str] = ..., cnappProviderId: _Optional[int] = ..., cnappQueueStatusId: _Optional[int] = ..., receivedAt: _Optional[int] = ..., resolvedAt: _Optional[int] = ..., recordUid: _Optional[bytes] = ..., payload: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., networkId: _Optional[bytes] = ..., encryptionRecordKeyId: _Optional[bytes] = ...) -> None: ... + +class CnappAssociateRequest(_message.Message): + __slots__ = ("recordUid", "executeAfterSetup") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + EXECUTEAFTERSETUP_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + executeAfterSetup: bool + def __init__(self, recordUid: _Optional[bytes] = ..., executeAfterSetup: _Optional[bool] = ...) -> None: ... + +class CnappAssociateResponse(_message.Message): + __slots__ = ("status", "remediationTriggered") + STATUS_FIELD_NUMBER: _ClassVar[int] + REMEDIATIONTRIGGERED_FIELD_NUMBER: _ClassVar[int] + status: str + remediationTriggered: bool + def __init__(self, status: _Optional[str] = ..., remediationTriggered: _Optional[bool] = ...) -> None: ... + +class CnappResolveRequest(_message.Message): + __slots__ = ("resolutionNotes",) + RESOLUTIONNOTES_FIELD_NUMBER: _ClassVar[int] + resolutionNotes: str + def __init__(self, resolutionNotes: _Optional[str] = ...) -> None: ... + +class CnappRemediateRequest(_message.Message): + __slots__ = ("actionType",) + ACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + actionType: str + def __init__(self, actionType: _Optional[str] = ...) -> None: ... + +class CnappRemediateResponse(_message.Message): + __slots__ = ("status", "actionType", "result", "executionTimeMs") + STATUS_FIELD_NUMBER: _ClassVar[int] + ACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + RESULT_FIELD_NUMBER: _ClassVar[int] + EXECUTIONTIMEMS_FIELD_NUMBER: _ClassVar[int] + status: str + actionType: str + result: str + executionTimeMs: int + def __init__(self, status: _Optional[str] = ..., actionType: _Optional[str] = ..., result: _Optional[str] = ..., executionTimeMs: _Optional[int] = ...) -> None: ... + +class CnappIgnoreRequest(_message.Message): + __slots__ = ("reason",) + REASON_FIELD_NUMBER: _ClassVar[int] + reason: str + def __init__(self, reason: _Optional[str] = ...) -> None: ... + +class CnappDefaultBehaviorRequest(_message.Message): + __slots__ = ("networkId", "cnappProviderId", "controlKey", "cnappActionTypeId", "autoExecute") + NETWORKID_FIELD_NUMBER: _ClassVar[int] + CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] + CONTROLKEY_FIELD_NUMBER: _ClassVar[int] + CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] + AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] + networkId: bytes + cnappProviderId: int + controlKey: str + cnappActionTypeId: int + autoExecute: bool + def __init__(self, networkId: _Optional[bytes] = ..., cnappProviderId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ...) -> None: ... + +class CnappDefaultBehaviorResponse(_message.Message): + __slots__ = ("cnappDefaultBehaviorId",) + CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] + cnappDefaultBehaviorId: int + def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ...) -> None: ... + +class CnappBehaviorListRequest(_message.Message): + __slots__ = ("networkUid",) + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + networkUid: bytes + def __init__(self, networkUid: _Optional[bytes] = ...) -> None: ... + +class CnappBehaviorListResponse(_message.Message): + __slots__ = ("items",) + ITEMS_FIELD_NUMBER: _ClassVar[int] + items: _containers.RepeatedCompositeFieldContainer[CnappDefaultBehaviorItem] + def __init__(self, items: _Optional[_Iterable[_Union[CnappDefaultBehaviorItem, _Mapping]]] = ...) -> None: ... + +class CnappDefaultBehaviorItem(_message.Message): + __slots__ = ("id", "networkId", "cnappProviderId", "controlKey", "cnappActionTypeId", "autoExecute", "enabled") + ID_FIELD_NUMBER: _ClassVar[int] + NETWORKID_FIELD_NUMBER: _ClassVar[int] + CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] + CONTROLKEY_FIELD_NUMBER: _ClassVar[int] + CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] + AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] + ENABLED_FIELD_NUMBER: _ClassVar[int] + id: int + networkId: bytes + cnappProviderId: int + controlKey: str + cnappActionTypeId: int + autoExecute: bool + enabled: bool + def __init__(self, id: _Optional[int] = ..., networkId: _Optional[bytes] = ..., cnappProviderId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ..., enabled: _Optional[bool] = ...) -> None: ... + +class CnappBehaviorUpdateRequest(_message.Message): + __slots__ = ("cnappDefaultBehaviorId", "controlKey", "cnappActionTypeId", "autoExecute", "enabled") + CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] + CONTROLKEY_FIELD_NUMBER: _ClassVar[int] + CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] + AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] + ENABLED_FIELD_NUMBER: _ClassVar[int] + cnappDefaultBehaviorId: int + controlKey: str + cnappActionTypeId: int + autoExecute: bool + enabled: bool + def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ..., enabled: _Optional[bool] = ...) -> None: ... + +class CnappBehaviorDeleteRequest(_message.Message): + __slots__ = ("cnappDefaultBehaviorId",) + CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] + cnappDefaultBehaviorId: int + def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py index 324a4b0c..217859e5 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pedm.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'pedm.proto' ) @@ -26,7 +26,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\"p\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\x12\x10\n\x08\x64isabled\x18\x05 \x01(\x08\"v\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\x12)\n\x08\x64isabled\x18\x04 \x01(\x0e\x32\x17.Folder.SetBooleanValue\"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\";\n\x12\x41pprovalExtendData\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x65xpireIn\x18\x02 \x01(\x05\"I\n\x15ModifyApprovalRequest\x12\x30\n\x0e\x65xtendApproval\x18\x01 \x03(\x0b\x32\x18.PEDM.ApprovalExtendData\"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\"\x94\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\x12\x10\n\x08\x64isabled\x18\x07 \x01(\x08\"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\x12PolicyAgentRequest\x12\x11\n\tpolicyUid\x18\x01 \x03(\x0c\x12\x13\n\x0bsummaryOnly\x18\x02 \x01(\x08\";\n\x13PolicyAgentResponse\x12\x12\n\nagentCount\x18\x01 \x01(\x05\x12\x10\n\x08\x61gentUid\x18\x02 \x03(\x0c\"]\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\"?\n\x17GetAgentLastSeenRequest\x12\x12\n\nactiveOnly\x18\x01 \x01(\x08\x12\x10\n\x08\x61gentUid\x18\x02 \x03(\x0c\"3\n\rAgentLastSeen\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12\x10\n\x08lastSeen\x18\x02 \x01(\x03\"A\n\x18GetAgentLastSeenResponse\x12%\n\x08lastSeen\x18\x01 \x03(\x0b\x32\x13.PEDM.AgentLastSeen\"2\n\x1aGetActiveAgentCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x03(\x05\">\n\x10\x41\x63tiveAgentCount\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x14\n\x0c\x61\x63tiveAgents\x18\x02 \x01(\x05\";\n\x12\x41\x63tiveAgentFailure\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"x\n\x1bGetActiveAgentCountResponse\x12*\n\nagentCount\x18\x01 \x03(\x0b\x32\x16.PEDM.ActiveAgentCount\x12-\n\x0b\x66\x61iledCount\x18\x02 \x03(\x0b\x32\x18.PEDM.ActiveAgentFailure\"\x87\x01\n\x19GetAgentDailyCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x03(\x05\x12$\n\tmonthYear\x18\x02 \x01(\x0b\x32\x0f.PEDM.MonthYearH\x00\x12$\n\tdateRange\x18\x03 \x01(\x0b\x32\x0f.PEDM.DateRangeH\x00\x42\x08\n\x06period\"(\n\tMonthYear\x12\r\n\x05month\x18\x01 \x01(\x05\x12\x0c\n\x04year\x18\x02 \x01(\x05\"\'\n\tDateRange\x12\r\n\x05start\x18\x01 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x03\"3\n\x0f\x41gentDailyCount\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\x03\x12\x12\n\nagentCount\x18\x02 \x01(\x05\"V\n\x17\x41gentCountForEnterprise\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12%\n\x06\x63ounts\x18\x02 \x03(\x0b\x32\x15.PEDM.AgentDailyCount\"U\n\x1aGetAgentDailyCountResponse\x12\x37\n\x10\x65nterpriseCounts\x18\x01 \x03(\x0b\x32\x1d.PEDM.AgentCountForEnterprise*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\"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\"p\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\x12\x10\n\x08\x64isabled\x18\x05 \x01(\x08\"v\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\x12)\n\x08\x64isabled\x18\x04 \x01(\x0e\x32\x17.Folder.SetBooleanValue\"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\";\n\x12\x41pprovalExtendData\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x65xpireIn\x18\x02 \x01(\x05\"I\n\x15ModifyApprovalRequest\x12\x30\n\x0e\x65xtendApproval\x18\x01 \x03(\x0b\x32\x18.PEDM.ApprovalExtendData\"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\"\x94\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\x12\x10\n\x08\x64isabled\x18\x07 \x01(\x08\"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\"\x87\x01\n\x12\x41pprovalStatusNode\x12\x13\n\x0b\x61pprovalUid\x18\x01 \x01(\x0c\x12\x30\n\x0e\x61pprovalStatus\x18\x02 \x01(\x0e\x32\x18.PEDM.ApprovalStatusType\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\x12PolicyAgentRequest\x12\x11\n\tpolicyUid\x18\x01 \x03(\x0c\x12\x13\n\x0bsummaryOnly\x18\x02 \x01(\x08\";\n\x13PolicyAgentResponse\x12\x12\n\nagentCount\x18\x01 \x01(\x05\x12\x10\n\x08\x61gentUid\x18\x02 \x03(\x0c\"]\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\"\xaa\x01\n\x1bOfflineAgentRegisterRequest\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12\x15\n\rdeploymentUid\x18\x02 \x01(\x0c\x12\x11\n\tpublicKey\x18\x03 \x01(\x0c\x12\x11\n\tmachineId\x18\x04 \x01(\t\x12)\n\ncollection\x18\x05 \x03(\x0b\x32\x15.PEDM.CollectionValue\x12\x11\n\tagentData\x18\x07 \x01(\x0c\"0\n\x1cOfflineAgentRegisterResponse\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\"/\n\x1bOfflineAgentSyncDownRequest\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\"9\n\x1cOfflineAgentSyncDownResponse\x12\x19\n\x11\x65ncryptedSyncData\x18\x01 \x01(\x0c\"?\n\x17GetAgentLastSeenRequest\x12\x12\n\nactiveOnly\x18\x01 \x01(\x08\x12\x10\n\x08\x61gentUid\x18\x02 \x03(\x0c\"3\n\rAgentLastSeen\x12\x10\n\x08\x61gentUid\x18\x01 \x01(\x0c\x12\x10\n\x08lastSeen\x18\x02 \x01(\x03\"A\n\x18GetAgentLastSeenResponse\x12%\n\x08lastSeen\x18\x01 \x03(\x0b\x32\x13.PEDM.AgentLastSeen\"2\n\x1aGetActiveAgentCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x03(\x05\">\n\x10\x41\x63tiveAgentCount\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x14\n\x0c\x61\x63tiveAgents\x18\x02 \x01(\x05\";\n\x12\x41\x63tiveAgentFailure\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"x\n\x1bGetActiveAgentCountResponse\x12*\n\nagentCount\x18\x01 \x03(\x0b\x32\x16.PEDM.ActiveAgentCount\x12-\n\x0b\x66\x61iledCount\x18\x02 \x03(\x0b\x32\x18.PEDM.ActiveAgentFailure\"\x87\x01\n\x19GetAgentDailyCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x03(\x05\x12$\n\tmonthYear\x18\x02 \x01(\x0b\x32\x0f.PEDM.MonthYearH\x00\x12$\n\tdateRange\x18\x03 \x01(\x0b\x32\x0f.PEDM.DateRangeH\x00\x42\x08\n\x06period\"(\n\tMonthYear\x12\r\n\x05month\x18\x01 \x01(\x05\x12\x0c\n\x04year\x18\x02 \x01(\x05\"\'\n\tDateRange\x12\r\n\x05start\x18\x01 \x01(\x03\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x03\"3\n\x0f\x41gentDailyCount\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\x03\x12\x12\n\nagentCount\x18\x02 \x01(\x05\"V\n\x17\x41gentCountForEnterprise\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12%\n\x06\x63ounts\x18\x02 \x03(\x0b\x32\x15.PEDM.AgentDailyCount\"U\n\x1aGetAgentDailyCountResponse\x12\x37\n\x10\x65nterpriseCounts\x18\x01 \x03(\x0b\x32\x1d.PEDM.AgentCountForEnterprise*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*o\n\x12\x41pprovalStatusType\x12\x13\n\x0f\x41ST_UNSPECIFIED\x10\x00\x12\x10\n\x0c\x41ST_APPROVED\x10\x01\x12\x0e\n\nAST_DENIED\x10\x02\x12\x0f\n\x0b\x41ST_EXPIRED\x10\x03\x12\x11\n\rAST_ESCALATED\x10\x05\x42 \n\x18\x63om.keepersecurity.protoB\x04PEDMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,112 +34,122 @@ 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=5607 - _globals['_COLLECTIONLINKTYPE']._serialized_end=5713 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=60 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=139 - _globals['_PEDMSTATUS']._serialized_start=141 - _globals['_PEDMSTATUS']._serialized_end=200 - _globals['_PEDMSTATUSRESPONSE']._serialized_start=203 - _globals['_PEDMSTATUSRESPONSE']._serialized_end=340 - _globals['_DEPLOYMENTDATA']._serialized_start=342 - _globals['_DEPLOYMENTDATA']._serialized_end=394 - _globals['_DEPLOYMENTCREATEREQUEST']._serialized_start=397 - _globals['_DEPLOYMENTCREATEREQUEST']._serialized_end=551 - _globals['_DEPLOYMENTUPDATEREQUEST']._serialized_start=554 - _globals['_DEPLOYMENTUPDATEREQUEST']._serialized_end=695 - _globals['_MODIFYDEPLOYMENTREQUEST']._serialized_start=698 - _globals['_MODIFYDEPLOYMENTREQUEST']._serialized_end=860 - _globals['_AGENTUPDATE']._serialized_start=862 - _globals['_AGENTUPDATE']._serialized_end=959 - _globals['_MODIFYAGENTREQUEST']._serialized_start=961 - _globals['_MODIFYAGENTREQUEST']._serialized_end=1042 - _globals['_POLICYADD']._serialized_start=1044 - _globals['_POLICYADD']._serialized_end=1156 - _globals['_POLICYUPDATE']._serialized_start=1158 - _globals['_POLICYUPDATE']._serialized_end=1276 - _globals['_POLICYREQUEST']._serialized_start=1278 - _globals['_POLICYREQUEST']._serialized_end=1393 - _globals['_POLICYLINK']._serialized_start=1395 - _globals['_POLICYLINK']._serialized_end=1449 - _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_start=1451 - _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_end=1520 - _globals['_COLLECTIONVALUE']._serialized_start=1522 - _globals['_COLLECTIONVALUE']._serialized_end=1609 - _globals['_COLLECTIONLINKDATA']._serialized_start=1611 - _globals['_COLLECTIONLINKDATA']._serialized_end=1733 - _globals['_COLLECTIONREQUEST']._serialized_start=1736 - _globals['_COLLECTIONREQUEST']._serialized_end=1876 - _globals['_SETCOLLECTIONLINKREQUEST']._serialized_start=1878 - _globals['_SETCOLLECTIONLINKREQUEST']._serialized_end=2001 - _globals['_APPROVALEXTENDDATA']._serialized_start=2003 - _globals['_APPROVALEXTENDDATA']._serialized_end=2062 - _globals['_MODIFYAPPROVALREQUEST']._serialized_start=2064 - _globals['_MODIFYAPPROVALREQUEST']._serialized_end=2137 - _globals['_APPROVALACTIONREQUEST']._serialized_start=2139 - _globals['_APPROVALACTIONREQUEST']._serialized_end=2209 - _globals['_DEPLOYMENTNODE']._serialized_start=2212 - _globals['_DEPLOYMENTNODE']._serialized_end=2383 - _globals['_AGENTNODE']._serialized_start=2386 - _globals['_AGENTNODE']._serialized_end=2554 - _globals['_POLICYNODE']._serialized_start=2557 - _globals['_POLICYNODE']._serialized_end=2705 - _globals['_COLLECTIONNODE']._serialized_start=2707 - _globals['_COLLECTIONNODE']._serialized_end=2810 - _globals['_COLLECTIONLINK']._serialized_start=2812 - _globals['_COLLECTIONLINK']._serialized_end=2912 - _globals['_APPROVALSTATUSNODE']._serialized_start=2915 - _globals['_APPROVALSTATUSNODE']._serialized_end=3072 - _globals['_APPROVALNODE']._serialized_start=3075 - _globals['_APPROVALNODE']._serialized_end=3254 - _globals['_FULLSYNCTOKEN']._serialized_start=3256 - _globals['_FULLSYNCTOKEN']._serialized_end=3323 - _globals['_INCSYNCTOKEN']._serialized_start=3325 - _globals['_INCSYNCTOKEN']._serialized_end=3361 - _globals['_PEDMSYNCTOKEN']._serialized_start=3363 - _globals['_PEDMSYNCTOKEN']._serialized_end=3467 - _globals['_GETPEDMDATAREQUEST']._serialized_start=3469 - _globals['_GETPEDMDATAREQUEST']._serialized_end=3516 - _globals['_GETPEDMDATARESPONSE']._serialized_start=3519 - _globals['_GETPEDMDATARESPONSE']._serialized_end=4076 - _globals['_POLICYAGENTREQUEST']._serialized_start=4078 - _globals['_POLICYAGENTREQUEST']._serialized_end=4138 - _globals['_POLICYAGENTRESPONSE']._serialized_start=4140 - _globals['_POLICYAGENTRESPONSE']._serialized_end=4199 - _globals['_AUDITCOLLECTIONREQUEST']._serialized_start=4201 - _globals['_AUDITCOLLECTIONREQUEST']._serialized_end=4294 - _globals['_AUDITCOLLECTIONVALUE']._serialized_start=4296 - _globals['_AUDITCOLLECTIONVALUE']._serialized_end=4400 - _globals['_AUDITCOLLECTIONRESPONSE']._serialized_start=4402 - _globals['_AUDITCOLLECTIONRESPONSE']._serialized_end=4515 - _globals['_GETCOLLECTIONLINKREQUEST']._serialized_start=4517 - _globals['_GETCOLLECTIONLINKREQUEST']._serialized_end=4589 - _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_start=4591 - _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_end=4672 - _globals['_GETAGENTLASTSEENREQUEST']._serialized_start=4674 - _globals['_GETAGENTLASTSEENREQUEST']._serialized_end=4737 - _globals['_AGENTLASTSEEN']._serialized_start=4739 - _globals['_AGENTLASTSEEN']._serialized_end=4790 - _globals['_GETAGENTLASTSEENRESPONSE']._serialized_start=4792 - _globals['_GETAGENTLASTSEENRESPONSE']._serialized_end=4857 - _globals['_GETACTIVEAGENTCOUNTREQUEST']._serialized_start=4859 - _globals['_GETACTIVEAGENTCOUNTREQUEST']._serialized_end=4909 - _globals['_ACTIVEAGENTCOUNT']._serialized_start=4911 - _globals['_ACTIVEAGENTCOUNT']._serialized_end=4973 - _globals['_ACTIVEAGENTFAILURE']._serialized_start=4975 - _globals['_ACTIVEAGENTFAILURE']._serialized_end=5034 - _globals['_GETACTIVEAGENTCOUNTRESPONSE']._serialized_start=5036 - _globals['_GETACTIVEAGENTCOUNTRESPONSE']._serialized_end=5156 - _globals['_GETAGENTDAILYCOUNTREQUEST']._serialized_start=5159 - _globals['_GETAGENTDAILYCOUNTREQUEST']._serialized_end=5294 - _globals['_MONTHYEAR']._serialized_start=5296 - _globals['_MONTHYEAR']._serialized_end=5336 - _globals['_DATERANGE']._serialized_start=5338 - _globals['_DATERANGE']._serialized_end=5377 - _globals['_AGENTDAILYCOUNT']._serialized_start=5379 - _globals['_AGENTDAILYCOUNT']._serialized_end=5430 - _globals['_AGENTCOUNTFORENTERPRISE']._serialized_start=5432 - _globals['_AGENTCOUNTFORENTERPRISE']._serialized_end=5518 - _globals['_GETAGENTDAILYCOUNTRESPONSE']._serialized_start=5520 - _globals['_GETAGENTDAILYCOUNTRESPONSE']._serialized_end=5605 + _globals['_COLLECTIONLINKTYPE']._serialized_start=5890 + _globals['_COLLECTIONLINKTYPE']._serialized_end=5996 + _globals['_APPROVALSTATUSTYPE']._serialized_start=5998 + _globals['_APPROVALSTATUSTYPE']._serialized_end=6109 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=34 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=113 + _globals['_PEDMSTATUS']._serialized_start=115 + _globals['_PEDMSTATUS']._serialized_end=174 + _globals['_PEDMSTATUSRESPONSE']._serialized_start=177 + _globals['_PEDMSTATUSRESPONSE']._serialized_end=314 + _globals['_DEPLOYMENTDATA']._serialized_start=316 + _globals['_DEPLOYMENTDATA']._serialized_end=368 + _globals['_DEPLOYMENTCREATEREQUEST']._serialized_start=371 + _globals['_DEPLOYMENTCREATEREQUEST']._serialized_end=525 + _globals['_DEPLOYMENTUPDATEREQUEST']._serialized_start=528 + _globals['_DEPLOYMENTUPDATEREQUEST']._serialized_end=669 + _globals['_MODIFYDEPLOYMENTREQUEST']._serialized_start=672 + _globals['_MODIFYDEPLOYMENTREQUEST']._serialized_end=834 + _globals['_AGENTUPDATE']._serialized_start=836 + _globals['_AGENTUPDATE']._serialized_end=933 + _globals['_MODIFYAGENTREQUEST']._serialized_start=935 + _globals['_MODIFYAGENTREQUEST']._serialized_end=1016 + _globals['_POLICYADD']._serialized_start=1018 + _globals['_POLICYADD']._serialized_end=1130 + _globals['_POLICYUPDATE']._serialized_start=1132 + _globals['_POLICYUPDATE']._serialized_end=1250 + _globals['_POLICYREQUEST']._serialized_start=1252 + _globals['_POLICYREQUEST']._serialized_end=1367 + _globals['_POLICYLINK']._serialized_start=1369 + _globals['_POLICYLINK']._serialized_end=1423 + _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_start=1425 + _globals['_SETPOLICYCOLLECTIONREQUEST']._serialized_end=1494 + _globals['_COLLECTIONVALUE']._serialized_start=1496 + _globals['_COLLECTIONVALUE']._serialized_end=1583 + _globals['_COLLECTIONLINKDATA']._serialized_start=1585 + _globals['_COLLECTIONLINKDATA']._serialized_end=1707 + _globals['_COLLECTIONREQUEST']._serialized_start=1710 + _globals['_COLLECTIONREQUEST']._serialized_end=1850 + _globals['_SETCOLLECTIONLINKREQUEST']._serialized_start=1852 + _globals['_SETCOLLECTIONLINKREQUEST']._serialized_end=1975 + _globals['_APPROVALEXTENDDATA']._serialized_start=1977 + _globals['_APPROVALEXTENDDATA']._serialized_end=2036 + _globals['_MODIFYAPPROVALREQUEST']._serialized_start=2038 + _globals['_MODIFYAPPROVALREQUEST']._serialized_end=2111 + _globals['_APPROVALACTIONREQUEST']._serialized_start=2113 + _globals['_APPROVALACTIONREQUEST']._serialized_end=2183 + _globals['_DEPLOYMENTNODE']._serialized_start=2186 + _globals['_DEPLOYMENTNODE']._serialized_end=2357 + _globals['_AGENTNODE']._serialized_start=2360 + _globals['_AGENTNODE']._serialized_end=2528 + _globals['_POLICYNODE']._serialized_start=2531 + _globals['_POLICYNODE']._serialized_end=2679 + _globals['_COLLECTIONNODE']._serialized_start=2681 + _globals['_COLLECTIONNODE']._serialized_end=2784 + _globals['_COLLECTIONLINK']._serialized_start=2786 + _globals['_COLLECTIONLINK']._serialized_end=2886 + _globals['_APPROVALSTATUSNODE']._serialized_start=2889 + _globals['_APPROVALSTATUSNODE']._serialized_end=3024 + _globals['_APPROVALNODE']._serialized_start=3027 + _globals['_APPROVALNODE']._serialized_end=3206 + _globals['_FULLSYNCTOKEN']._serialized_start=3208 + _globals['_FULLSYNCTOKEN']._serialized_end=3275 + _globals['_INCSYNCTOKEN']._serialized_start=3277 + _globals['_INCSYNCTOKEN']._serialized_end=3313 + _globals['_PEDMSYNCTOKEN']._serialized_start=3315 + _globals['_PEDMSYNCTOKEN']._serialized_end=3419 + _globals['_GETPEDMDATAREQUEST']._serialized_start=3421 + _globals['_GETPEDMDATAREQUEST']._serialized_end=3468 + _globals['_GETPEDMDATARESPONSE']._serialized_start=3471 + _globals['_GETPEDMDATARESPONSE']._serialized_end=4028 + _globals['_POLICYAGENTREQUEST']._serialized_start=4030 + _globals['_POLICYAGENTREQUEST']._serialized_end=4090 + _globals['_POLICYAGENTRESPONSE']._serialized_start=4092 + _globals['_POLICYAGENTRESPONSE']._serialized_end=4151 + _globals['_AUDITCOLLECTIONREQUEST']._serialized_start=4153 + _globals['_AUDITCOLLECTIONREQUEST']._serialized_end=4246 + _globals['_AUDITCOLLECTIONVALUE']._serialized_start=4248 + _globals['_AUDITCOLLECTIONVALUE']._serialized_end=4352 + _globals['_AUDITCOLLECTIONRESPONSE']._serialized_start=4354 + _globals['_AUDITCOLLECTIONRESPONSE']._serialized_end=4467 + _globals['_GETCOLLECTIONLINKREQUEST']._serialized_start=4469 + _globals['_GETCOLLECTIONLINKREQUEST']._serialized_end=4541 + _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_start=4543 + _globals['_GETCOLLECTIONLINKRESPONSE']._serialized_end=4624 + _globals['_OFFLINEAGENTREGISTERREQUEST']._serialized_start=4627 + _globals['_OFFLINEAGENTREGISTERREQUEST']._serialized_end=4797 + _globals['_OFFLINEAGENTREGISTERRESPONSE']._serialized_start=4799 + _globals['_OFFLINEAGENTREGISTERRESPONSE']._serialized_end=4847 + _globals['_OFFLINEAGENTSYNCDOWNREQUEST']._serialized_start=4849 + _globals['_OFFLINEAGENTSYNCDOWNREQUEST']._serialized_end=4896 + _globals['_OFFLINEAGENTSYNCDOWNRESPONSE']._serialized_start=4898 + _globals['_OFFLINEAGENTSYNCDOWNRESPONSE']._serialized_end=4955 + _globals['_GETAGENTLASTSEENREQUEST']._serialized_start=4957 + _globals['_GETAGENTLASTSEENREQUEST']._serialized_end=5020 + _globals['_AGENTLASTSEEN']._serialized_start=5022 + _globals['_AGENTLASTSEEN']._serialized_end=5073 + _globals['_GETAGENTLASTSEENRESPONSE']._serialized_start=5075 + _globals['_GETAGENTLASTSEENRESPONSE']._serialized_end=5140 + _globals['_GETACTIVEAGENTCOUNTREQUEST']._serialized_start=5142 + _globals['_GETACTIVEAGENTCOUNTREQUEST']._serialized_end=5192 + _globals['_ACTIVEAGENTCOUNT']._serialized_start=5194 + _globals['_ACTIVEAGENTCOUNT']._serialized_end=5256 + _globals['_ACTIVEAGENTFAILURE']._serialized_start=5258 + _globals['_ACTIVEAGENTFAILURE']._serialized_end=5317 + _globals['_GETACTIVEAGENTCOUNTRESPONSE']._serialized_start=5319 + _globals['_GETACTIVEAGENTCOUNTRESPONSE']._serialized_end=5439 + _globals['_GETAGENTDAILYCOUNTREQUEST']._serialized_start=5442 + _globals['_GETAGENTDAILYCOUNTREQUEST']._serialized_end=5577 + _globals['_MONTHYEAR']._serialized_start=5579 + _globals['_MONTHYEAR']._serialized_end=5619 + _globals['_DATERANGE']._serialized_start=5621 + _globals['_DATERANGE']._serialized_end=5660 + _globals['_AGENTDAILYCOUNT']._serialized_start=5662 + _globals['_AGENTDAILYCOUNT']._serialized_end=5713 + _globals['_AGENTCOUNTFORENTERPRISE']._serialized_start=5715 + _globals['_AGENTCOUNTFORENTERPRISE']._serialized_end=5801 + _globals['_GETAGENTDAILYCOUNTRESPONSE']._serialized_start=5803 + _globals['_GETAGENTDAILYCOUNTRESPONSE']._serialized_end=5888 # @@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 bdbb0427..bb0f669a 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi @@ -1,10 +1,10 @@ import folder_pb2 as _folder_pb2 -import NotificationCenter_pb2 as _NotificationCenter_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 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 @@ -15,11 +15,24 @@ class CollectionLinkType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): CLT_POLICY: _ClassVar[CollectionLinkType] CLT_COLLECTION: _ClassVar[CollectionLinkType] CLT_DEPLOYMENT: _ClassVar[CollectionLinkType] + +class ApprovalStatusType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + AST_UNSPECIFIED: _ClassVar[ApprovalStatusType] + AST_APPROVED: _ClassVar[ApprovalStatusType] + AST_DENIED: _ClassVar[ApprovalStatusType] + AST_EXPIRED: _ClassVar[ApprovalStatusType] + AST_ESCALATED: _ClassVar[ApprovalStatusType] CLT_OTHER: CollectionLinkType CLT_AGENT: CollectionLinkType CLT_POLICY: CollectionLinkType CLT_COLLECTION: CollectionLinkType CLT_DEPLOYMENT: CollectionLinkType +AST_UNSPECIFIED: ApprovalStatusType +AST_APPROVED: ApprovalStatusType +AST_DENIED: ApprovalStatusType +AST_EXPIRED: ApprovalStatusType +AST_ESCALATED: ApprovalStatusType class PEDMTOTPValidateRequest(_message.Message): __slots__ = ("username", "enterpriseId", "code") @@ -39,7 +52,7 @@ class PedmStatus(_message.Message): key: _containers.RepeatedScalarFieldContainer[bytes] success: bool message: str - def __init__(self, key: _Optional[_Iterable[bytes]] = ..., success: bool = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, key: _Optional[_Iterable[bytes]] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... class PedmStatusResponse(_message.Message): __slots__ = ("addStatus", "updateStatus", "removeStatus") @@ -127,7 +140,7 @@ class PolicyAdd(_message.Message): encryptedData: bytes encryptedKey: bytes disabled: bool - def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., disabled: bool = ...) -> None: ... + def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., disabled: _Optional[bool] = ...) -> None: ... class PolicyUpdate(_message.Message): __slots__ = ("policyUid", "plainData", "encryptedData", "disabled") @@ -247,7 +260,7 @@ class DeploymentNode(_message.Message): agentData: bytes created: int modified: int - def __init__(self, deploymentUid: _Optional[bytes] = ..., disabled: bool = ..., aesKey: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., agentData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... + def __init__(self, deploymentUid: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., aesKey: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., agentData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... class AgentNode(_message.Message): __slots__ = ("agentUid", "machineId", "deploymentUid", "ecPublicKey", "disabled", "encryptedData", "created", "modified") @@ -267,7 +280,7 @@ class AgentNode(_message.Message): encryptedData: bytes created: int modified: int - def __init__(self, agentUid: _Optional[bytes] = ..., machineId: _Optional[str] = ..., deploymentUid: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., disabled: bool = ..., encryptedData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... + def __init__(self, agentUid: _Optional[bytes] = ..., machineId: _Optional[str] = ..., deploymentUid: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., encryptedData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... class PolicyNode(_message.Message): __slots__ = ("policyUid", "plainData", "encryptedData", "encryptedKey", "created", "modified", "disabled") @@ -285,7 +298,7 @@ class PolicyNode(_message.Message): created: int modified: int disabled: bool - def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ..., disabled: bool = ...) -> None: ... + def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ..., disabled: _Optional[bool] = ...) -> None: ... class CollectionNode(_message.Message): __slots__ = ("collectionUid", "collectionType", "encryptedData", "created") @@ -316,10 +329,10 @@ class ApprovalStatusNode(_message.Message): ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] MODIFIED_FIELD_NUMBER: _ClassVar[int] approvalUid: bytes - approvalStatus: _NotificationCenter_pb2.NotificationApprovalStatus + approvalStatus: ApprovalStatusType enterpriseUserId: int modified: int - def __init__(self, approvalUid: _Optional[bytes] = ..., approvalStatus: _Optional[_Union[_NotificationCenter_pb2.NotificationApprovalStatus, str]] = ..., enterpriseUserId: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... + def __init__(self, approvalUid: _Optional[bytes] = ..., approvalStatus: _Optional[_Union[ApprovalStatusType, str]] = ..., enterpriseUserId: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... class ApprovalNode(_message.Message): __slots__ = ("approvalUid", "approvalType", "agentUid", "accountInfo", "applicationInfo", "justification", "expireIn", "created") @@ -405,7 +418,7 @@ class GetPedmDataResponse(_message.Message): collectionLink: _containers.RepeatedCompositeFieldContainer[CollectionLink] approvals: _containers.RepeatedCompositeFieldContainer[ApprovalNode] approvalStatus: _containers.RepeatedCompositeFieldContainer[ApprovalStatusNode] - def __init__(self, continuationToken: _Optional[bytes] = ..., resetCache: bool = ..., hasMore: bool = ..., removedDeployments: _Optional[_Iterable[bytes]] = ..., removedAgents: _Optional[_Iterable[bytes]] = ..., removedPolicies: _Optional[_Iterable[bytes]] = ..., removedCollection: _Optional[_Iterable[bytes]] = ..., removedCollectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., removedApprovals: _Optional[_Iterable[bytes]] = ..., deployments: _Optional[_Iterable[_Union[DeploymentNode, _Mapping]]] = ..., agents: _Optional[_Iterable[_Union[AgentNode, _Mapping]]] = ..., policies: _Optional[_Iterable[_Union[PolicyNode, _Mapping]]] = ..., collections: _Optional[_Iterable[_Union[CollectionNode, _Mapping]]] = ..., collectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., approvals: _Optional[_Iterable[_Union[ApprovalNode, _Mapping]]] = ..., approvalStatus: _Optional[_Iterable[_Union[ApprovalStatusNode, _Mapping]]] = ...) -> None: ... + def __init__(self, continuationToken: _Optional[bytes] = ..., resetCache: _Optional[bool] = ..., hasMore: _Optional[bool] = ..., removedDeployments: _Optional[_Iterable[bytes]] = ..., removedAgents: _Optional[_Iterable[bytes]] = ..., removedPolicies: _Optional[_Iterable[bytes]] = ..., removedCollection: _Optional[_Iterable[bytes]] = ..., removedCollectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., removedApprovals: _Optional[_Iterable[bytes]] = ..., deployments: _Optional[_Iterable[_Union[DeploymentNode, _Mapping]]] = ..., agents: _Optional[_Iterable[_Union[AgentNode, _Mapping]]] = ..., policies: _Optional[_Iterable[_Union[PolicyNode, _Mapping]]] = ..., collections: _Optional[_Iterable[_Union[CollectionNode, _Mapping]]] = ..., collectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., approvals: _Optional[_Iterable[_Union[ApprovalNode, _Mapping]]] = ..., approvalStatus: _Optional[_Iterable[_Union[ApprovalStatusNode, _Mapping]]] = ...) -> None: ... class PolicyAgentRequest(_message.Message): __slots__ = ("policyUid", "summaryOnly") @@ -413,7 +426,7 @@ class PolicyAgentRequest(_message.Message): SUMMARYONLY_FIELD_NUMBER: _ClassVar[int] policyUid: _containers.RepeatedScalarFieldContainer[bytes] summaryOnly: bool - def __init__(self, policyUid: _Optional[_Iterable[bytes]] = ..., summaryOnly: bool = ...) -> None: ... + def __init__(self, policyUid: _Optional[_Iterable[bytes]] = ..., summaryOnly: _Optional[bool] = ...) -> None: ... class PolicyAgentResponse(_message.Message): __slots__ = ("agentCount", "agentUid") @@ -453,7 +466,7 @@ class AuditCollectionResponse(_message.Message): values: _containers.RepeatedCompositeFieldContainer[AuditCollectionValue] hasMore: bool continuationToken: bytes - def __init__(self, values: _Optional[_Iterable[_Union[AuditCollectionValue, _Mapping]]] = ..., hasMore: bool = ..., continuationToken: _Optional[bytes] = ...) -> None: ... + def __init__(self, values: _Optional[_Iterable[_Union[AuditCollectionValue, _Mapping]]] = ..., hasMore: _Optional[bool] = ..., continuationToken: _Optional[bytes] = ...) -> None: ... class GetCollectionLinkRequest(_message.Message): __slots__ = ("collectionLink",) @@ -467,13 +480,47 @@ class GetCollectionLinkResponse(_message.Message): collectionLinkData: _containers.RepeatedCompositeFieldContainer[CollectionLinkData] def __init__(self, collectionLinkData: _Optional[_Iterable[_Union[CollectionLinkData, _Mapping]]] = ...) -> None: ... +class OfflineAgentRegisterRequest(_message.Message): + __slots__ = ("agentUid", "deploymentUid", "publicKey", "machineId", "collection", "agentData") + AGENTUID_FIELD_NUMBER: _ClassVar[int] + DEPLOYMENTUID_FIELD_NUMBER: _ClassVar[int] + PUBLICKEY_FIELD_NUMBER: _ClassVar[int] + MACHINEID_FIELD_NUMBER: _ClassVar[int] + COLLECTION_FIELD_NUMBER: _ClassVar[int] + AGENTDATA_FIELD_NUMBER: _ClassVar[int] + agentUid: bytes + deploymentUid: bytes + publicKey: bytes + machineId: str + collection: _containers.RepeatedCompositeFieldContainer[CollectionValue] + agentData: bytes + def __init__(self, agentUid: _Optional[bytes] = ..., deploymentUid: _Optional[bytes] = ..., publicKey: _Optional[bytes] = ..., machineId: _Optional[str] = ..., collection: _Optional[_Iterable[_Union[CollectionValue, _Mapping]]] = ..., agentData: _Optional[bytes] = ...) -> None: ... + +class OfflineAgentRegisterResponse(_message.Message): + __slots__ = ("agentUid",) + AGENTUID_FIELD_NUMBER: _ClassVar[int] + agentUid: bytes + def __init__(self, agentUid: _Optional[bytes] = ...) -> None: ... + +class OfflineAgentSyncDownRequest(_message.Message): + __slots__ = ("agentUid",) + AGENTUID_FIELD_NUMBER: _ClassVar[int] + agentUid: bytes + def __init__(self, agentUid: _Optional[bytes] = ...) -> None: ... + +class OfflineAgentSyncDownResponse(_message.Message): + __slots__ = ("encryptedSyncData",) + ENCRYPTEDSYNCDATA_FIELD_NUMBER: _ClassVar[int] + encryptedSyncData: bytes + def __init__(self, encryptedSyncData: _Optional[bytes] = ...) -> None: ... + class GetAgentLastSeenRequest(_message.Message): __slots__ = ("activeOnly", "agentUid") ACTIVEONLY_FIELD_NUMBER: _ClassVar[int] AGENTUID_FIELD_NUMBER: _ClassVar[int] activeOnly: bool agentUid: _containers.RepeatedScalarFieldContainer[bytes] - def __init__(self, activeOnly: bool = ..., agentUid: _Optional[_Iterable[bytes]] = ...) -> None: ... + def __init__(self, activeOnly: _Optional[bool] = ..., agentUid: _Optional[_Iterable[bytes]] = ...) -> None: ... class AgentLastSeen(_message.Message): __slots__ = ("agentUid", "lastSeen") diff --git a/keepersdk-package/src/keepersdk/proto/push_pb2.py b/keepersdk-package/src/keepersdk/proto/push_pb2.py index 47538aa1..49214c23 100644 --- a/keepersdk-package/src/keepersdk/proto/push_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/push_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: push.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'push.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/proto/push_pb2.pyi b/keepersdk-package/src/keepersdk/proto/push_pb2.pyi index 85bde767..1e888c81 100644 --- a/keepersdk-package/src/keepersdk/proto/push_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/push_pb2.pyi @@ -2,7 +2,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, Optional as _Optional, Union as _Union +from collections.abc import Iterable as _Iterable +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor diff --git a/keepersdk-package/src/keepersdk/proto/record_pb2.py b/keepersdk-package/src/keepersdk/proto/record_pb2.py index f32a5415..8537d816 100644 --- a/keepersdk-package/src/keepersdk/proto/record_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/record_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: record.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'record.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/record_pb2.pyi b/keepersdk-package/src/keepersdk/proto/record_pb2.pyi index aec21a56..bac4c4a9 100644 --- a/keepersdk-package/src/keepersdk/proto/record_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/record_pb2.pyi @@ -2,7 +2,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 @@ -168,7 +169,7 @@ class RecordTypesRequest(_message.Message): user: bool enterprise: bool pam: bool - def __init__(self, standard: bool = ..., user: bool = ..., enterprise: bool = ..., pam: bool = ...) -> None: ... + def __init__(self, standard: _Optional[bool] = ..., user: _Optional[bool] = ..., enterprise: _Optional[bool] = ..., pam: _Optional[bool] = ...) -> None: ... class RecordTypesResponse(_message.Message): __slots__ = ("recordTypes", "standardCounter", "userCounter", "enterpriseCounter", "pamCounter") @@ -502,7 +503,7 @@ class File(_message.Message): fileSize: int thumbSize: int is_script: bool - def __init__(self, record_uid: _Optional[bytes] = ..., record_key: _Optional[bytes] = ..., data: _Optional[bytes] = ..., fileSize: _Optional[int] = ..., thumbSize: _Optional[int] = ..., is_script: bool = ...) -> None: ... + def __init__(self, record_uid: _Optional[bytes] = ..., record_key: _Optional[bytes] = ..., data: _Optional[bytes] = ..., fileSize: _Optional[int] = ..., thumbSize: _Optional[int] = ..., is_script: _Optional[bool] = ...) -> None: ... class FilesAddRequest(_message.Message): __slots__ = ("files", "client_time") @@ -544,7 +545,7 @@ class FilesGetRequest(_message.Message): record_uids: _containers.RepeatedScalarFieldContainer[bytes] for_thumbnails: bool emergency_access_account_owner: str - def __init__(self, record_uids: _Optional[_Iterable[bytes]] = ..., for_thumbnails: bool = ..., emergency_access_account_owner: _Optional[str] = ...) -> None: ... + def __init__(self, record_uids: _Optional[_Iterable[bytes]] = ..., for_thumbnails: _Optional[bool] = ..., emergency_access_account_owner: _Optional[str] = ...) -> None: ... class FileGetStatus(_message.Message): __slots__ = ("record_uid", "status", "url", "success_status_code", "fileKeyType") @@ -612,7 +613,7 @@ class UserPermission(_message.Message): accountUid: bytes timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, username: _Optional[str] = ..., owner: bool = ..., shareAdmin: bool = ..., sharable: bool = ..., editable: bool = ..., awaitingApproval: bool = ..., expiration: _Optional[int] = ..., accountUid: _Optional[bytes] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., owner: _Optional[bool] = ..., shareAdmin: _Optional[bool] = ..., sharable: _Optional[bool] = ..., editable: _Optional[bool] = ..., awaitingApproval: _Optional[bool] = ..., expiration: _Optional[int] = ..., accountUid: _Optional[bytes] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class SharedFolderPermission(_message.Message): __slots__ = ("sharedFolderUid", "resharable", "editable", "revision", "expiration", "timerNotificationType", "rotateOnExpiration") @@ -630,7 +631,7 @@ class SharedFolderPermission(_message.Message): expiration: int timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., resharable: bool = ..., editable: bool = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., resharable: _Optional[bool] = ..., editable: _Optional[bool] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class RecordData(_message.Message): __slots__ = ("revision", "version", "shared", "encryptedRecordData", "encryptedExtraData", "clientModifiedTime", "nonSharedData", "linkedRecordData", "fileId", "fileSize", "thumbnailSize", "recordKeyType", "recordKey", "recordUid") @@ -662,7 +663,7 @@ class RecordData(_message.Message): recordKeyType: RecordKeyType recordKey: bytes recordUid: bytes - def __init__(self, revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: bool = ..., encryptedRecordData: _Optional[str] = ..., encryptedExtraData: _Optional[str] = ..., clientModifiedTime: _Optional[int] = ..., nonSharedData: _Optional[str] = ..., linkedRecordData: _Optional[_Iterable[_Union[RecordData, _Mapping]]] = ..., fileId: _Optional[_Iterable[bytes]] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ..., recordKeyType: _Optional[_Union[RecordKeyType, str]] = ..., recordKey: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ...) -> None: ... + def __init__(self, revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: _Optional[bool] = ..., encryptedRecordData: _Optional[str] = ..., encryptedExtraData: _Optional[str] = ..., clientModifiedTime: _Optional[int] = ..., nonSharedData: _Optional[str] = ..., linkedRecordData: _Optional[_Iterable[_Union[RecordData, _Mapping]]] = ..., fileId: _Optional[_Iterable[bytes]] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ..., recordKeyType: _Optional[_Union[RecordKeyType, str]] = ..., recordKey: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ...) -> None: ... class RecordDataWithAccessInfo(_message.Message): __slots__ = ("recordUid", "recordData", "userPermission", "sharedFolderPermission") @@ -692,7 +693,7 @@ class IsObjectShareAdmin(_message.Message): uid: bytes isAdmin: bool objectType: CheckShareAdminObjectType - def __init__(self, uid: _Optional[bytes] = ..., isAdmin: bool = ..., objectType: _Optional[_Union[CheckShareAdminObjectType, str]] = ...) -> None: ... + def __init__(self, uid: _Optional[bytes] = ..., isAdmin: _Optional[bool] = ..., objectType: _Optional[_Union[CheckShareAdminObjectType, str]] = ...) -> None: ... class AmIShareAdmin(_message.Message): __slots__ = ("isObjectShareAdmin",) @@ -740,7 +741,7 @@ class SharedRecord(_message.Message): expiration: int timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, toUsername: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., editable: bool = ..., shareable: bool = ..., transfer: bool = ..., useEccKey: bool = ..., removeVaultData: bool = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... + def __init__(self, toUsername: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., editable: _Optional[bool] = ..., shareable: _Optional[bool] = ..., transfer: _Optional[bool] = ..., useEccKey: _Optional[bool] = ..., removeVaultData: _Optional[bool] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... class RecordShareUpdateResponse(_message.Message): __slots__ = ("addSharedRecordStatus", "updateSharedRecordStatus", "removeSharedRecordStatus") @@ -770,7 +771,7 @@ class GetRecordPermissionsRequest(_message.Message): ISSHAREADMIN_FIELD_NUMBER: _ClassVar[int] recordUids: _containers.RepeatedScalarFieldContainer[bytes] isShareAdmin: bool - def __init__(self, recordUids: _Optional[_Iterable[bytes]] = ..., isShareAdmin: bool = ...) -> None: ... + def __init__(self, recordUids: _Optional[_Iterable[bytes]] = ..., isShareAdmin: _Optional[bool] = ...) -> None: ... class GetRecordPermissionsResponse(_message.Message): __slots__ = ("recordPermissions",) @@ -790,7 +791,7 @@ class RecordPermission(_message.Message): canEdit: bool canShare: bool canTransfer: bool - def __init__(self, recordUid: _Optional[bytes] = ..., owner: bool = ..., canEdit: bool = ..., canShare: bool = ..., canTransfer: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., owner: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., canShare: _Optional[bool] = ..., canTransfer: _Optional[bool] = ...) -> None: ... class GetShareObjectsRequest(_message.Message): __slots__ = ("startWith", "contains", "filtered", "sharedFolderUid") @@ -802,7 +803,7 @@ class GetShareObjectsRequest(_message.Message): contains: str filtered: bool sharedFolderUid: bytes - def __init__(self, startWith: _Optional[str] = ..., contains: _Optional[str] = ..., filtered: bool = ..., sharedFolderUid: _Optional[bytes] = ...) -> None: ... + def __init__(self, startWith: _Optional[str] = ..., contains: _Optional[str] = ..., filtered: _Optional[bool] = ..., sharedFolderUid: _Optional[bytes] = ...) -> None: ... class GetShareObjectsResponse(_message.Message): __slots__ = ("shareRelationships", "shareFamilyUsers", "shareEnterpriseUsers", "shareTeams", "shareMCTeams", "shareMCEnterpriseUsers", "shareEnterpriseNames") @@ -836,7 +837,7 @@ class ShareUser(_message.Message): status: ShareStatus isShareAdmin: bool isAdminOfSharedFolderOwner: bool - def __init__(self, username: _Optional[str] = ..., fullname: _Optional[str] = ..., enterpriseId: _Optional[int] = ..., status: _Optional[_Union[ShareStatus, str]] = ..., isShareAdmin: bool = ..., isAdminOfSharedFolderOwner: bool = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., fullname: _Optional[str] = ..., enterpriseId: _Optional[int] = ..., status: _Optional[_Union[ShareStatus, str]] = ..., isShareAdmin: _Optional[bool] = ..., isAdminOfSharedFolderOwner: _Optional[bool] = ...) -> None: ... class ShareTeam(_message.Message): __slots__ = ("teamname", "enterpriseId", "teamUid") @@ -872,7 +873,7 @@ class TransferRecord(_message.Message): recordUid: bytes recordKey: bytes useEccKey: bool - def __init__(self, username: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., useEccKey: bool = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., useEccKey: _Optional[bool] = ...) -> None: ... class RecordsOnwershipTransferResponse(_message.Message): __slots__ = ("transferRecordStatus",) diff --git a/keepersdk-package/src/keepersdk/proto/router_pb2.py b/keepersdk-package/src/keepersdk/proto/router_pb2.py index d98e1ae9..9b9959cc 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: router.proto -# Protobuf Python Version: 5.29.5 +# Protobuf Python Version: 7.34.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 5, - 29, - 5, + 7, + 34, + 1, '', 'router.proto' ) @@ -25,7 +25,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\"\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\"\x9d\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\x12\x18\n\x10maxInstanceCount\x18\r \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\"M\n\x18UserRecordAccessRequests\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.Router.UserRecordAccessRequest\"P\n\x19UserRecordAccessResponses\x12\x33\n\tresponses\x18\x01 \x03(\x0b\x32 .Router.UserRecordAccessResponse\"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\"H\n\x18GetPEDMAdminInfoResponse\x12\x13\n\x0bisPedmAdmin\x18\x01 \x01(\x08\x12\x17\n\x0fpedmAddonActive\x18\x02 \x01(\x08\"-\n\x12PAMNetworkSettings\x12\x17\n\x0f\x61llowedSettings\x18\x01 \x01(\x0c\"\xe4\x01\n\x1ePAMNetworkConfigurationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x38\n\x0fnetworkSettings\x18\x02 \x01(\x0b\x32\x1a.Router.PAMNetworkSettingsH\x00\x88\x01\x01\x12)\n\tresources\x18\x03 \x03(\x0b\x32\x16.PAM.PAMResourceConfig\x12\x36\n\trotations\x18\x04 \x03(\x0b\x32#.Router.RouterRecordRotationRequestB\x12\n\x10_networkSettings*\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\x1a\x10\x41PIRequest.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\"\x9d\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\x12\x18\n\x10maxInstanceCount\x18\r \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\"\xba\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\x12\x1e\n\x11saasConfiguration\x18\x0c \x01(\x0cH\x00\x88\x01\x01\x42\x14\n\x12_saasConfiguration\"<\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\"M\n\x18UserRecordAccessRequests\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.Router.UserRecordAccessRequest\"P\n\x19UserRecordAccessResponses\x12\x33\n\tresponses\x18\x01 \x03(\x0b\x32 .Router.UserRecordAccessResponse\"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\"H\n\x18GetPEDMAdminInfoResponse\x12\x13\n\x0bisPedmAdmin\x18\x01 \x01(\x08\x12\x17\n\x0fpedmAddonActive\x18\x02 \x01(\x08\"-\n\x12PAMNetworkSettings\x12\x17\n\x0f\x61llowedSettings\x18\x01 \x01(\x0c\"\xe4\x01\n\x1ePAMNetworkConfigurationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x38\n\x0fnetworkSettings\x18\x02 \x01(\x0b\x32\x1a.Router.PAMNetworkSettingsH\x00\x88\x01\x01\x12)\n\tresources\x18\x03 \x03(\x0b\x32\x16.PAM.PAMResourceConfig\x12\x36\n\trotations\x18\x04 \x03(\x0b\x32#.Router.RouterRecordRotationRequestB\x12\n\x10_networkSettings\"R\n\x1bPAMDiscoveryRulesSetRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\r\n\x05rules\x18\x02 \x01(\x0c\x12\x10\n\x08rulesKey\x18\x03 \x01(\x0c\"X\n\x18Router2FAValidateRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\t\"~\n\x18Router2FASendPushRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\x33\n\x08pushType\x18\x03 \x01(\x0e\x32!.Authentication.TwoFactorPushType\"U\n$Router2FAGetWebAuthnChallengeRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\"P\n%Router2FAGetWebAuthnChallengeResponse\x12\x11\n\tchallenge\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t*\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,70 +33,80 @@ 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=3495 - _globals['_ROUTERRESPONSECODE']._serialized_end=3775 - _globals['_ROUTERROTATIONSTATUS']._serialized_start=3777 - _globals['_ROUTERROTATIONSTATUS']._serialized_end=3884 - _globals['_USERRECORDACCESSLEVEL']._serialized_start=3886 - _globals['_USERRECORDACCESSLEVEL']._serialized_end=4011 - _globals['_SERVICETYPE']._serialized_start=4013 - _globals['_SERVICETYPE']._serialized_end=4059 - _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=611 - _globals['_ROUTERDEVICEAUTH']._serialized_start=614 - _globals['_ROUTERDEVICEAUTH']._serialized_end=899 - _globals['_ROUTERRECORDROTATION']._serialized_start=902 - _globals['_ROUTERRECORDROTATION']._serialized_end=1033 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=1035 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1104 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1106 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1203 - _globals['_ROUTERROTATIONINFO']._serialized_start=1206 - _globals['_ROUTERROTATIONINFO']._serialized_end=1443 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1446 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1706 - _globals['_USERRECORDACCESSREQUEST']._serialized_start=1708 - _globals['_USERRECORDACCESSREQUEST']._serialized_end=1768 - _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1770 - _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1867 - _globals['_USERRECORDACCESSREQUESTS']._serialized_start=1869 - _globals['_USERRECORDACCESSREQUESTS']._serialized_end=1946 - _globals['_USERRECORDACCESSRESPONSES']._serialized_start=1948 - _globals['_USERRECORDACCESSRESPONSES']._serialized_end=2028 - _globals['_ROTATIONSCHEDULE']._serialized_start=2030 - _globals['_ROTATIONSCHEDULE']._serialized_end=2086 - _globals['_APICALLBACKREQUEST']._serialized_start=2089 - _globals['_APICALLBACKREQUEST']._serialized_end=2233 - _globals['_APICALLBACKSCHEDULE']._serialized_start=2235 - _globals['_APICALLBACKSCHEDULE']._serialized_end=2288 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2290 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2354 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2356 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2445 - _globals['_CONNECTIONPARAMETERS']._serialized_start=2448 - _globals['_CONNECTIONPARAMETERS']._serialized_end=2581 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2583 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=2662 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=2664 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=2738 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=2740 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=2833 - _globals['_GETENFORCEMENTREQUEST']._serialized_start=2835 - _globals['_GETENFORCEMENTREQUEST']._serialized_end=2884 - _globals['_ENFORCEMENTTYPE']._serialized_start=2886 - _globals['_ENFORCEMENTTYPE']._serialized_end=2945 - _globals['_GETENFORCEMENTRESPONSE']._serialized_start=2947 - _globals['_GETENFORCEMENTRESPONSE']._serialized_end=3059 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=3061 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=3140 - _globals['_GETPEDMADMININFORESPONSE']._serialized_start=3142 - _globals['_GETPEDMADMININFORESPONSE']._serialized_end=3214 - _globals['_PAMNETWORKSETTINGS']._serialized_start=3216 - _globals['_PAMNETWORKSETTINGS']._serialized_end=3261 - _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_start=3264 - _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_end=3492 + _globals['_ROUTERRESPONSECODE']._serialized_start=4038 + _globals['_ROUTERRESPONSECODE']._serialized_end=4318 + _globals['_ROUTERROTATIONSTATUS']._serialized_start=4320 + _globals['_ROUTERROTATIONSTATUS']._serialized_end=4427 + _globals['_USERRECORDACCESSLEVEL']._serialized_start=4429 + _globals['_USERRECORDACCESSLEVEL']._serialized_end=4554 + _globals['_SERVICETYPE']._serialized_start=4556 + _globals['_SERVICETYPE']._serialized_end=4602 + _globals['_ROUTERRESPONSE']._serialized_start=53 + _globals['_ROUTERRESPONSE']._serialized_end=167 + _globals['_ROUTERCONTROLLERMESSAGE']._serialized_start=170 + _globals['_ROUTERCONTROLLERMESSAGE']._serialized_end=345 + _globals['_ROUTERUSERAUTH']._serialized_start=348 + _globals['_ROUTERUSERAUTH']._serialized_end=629 + _globals['_ROUTERDEVICEAUTH']._serialized_start=632 + _globals['_ROUTERDEVICEAUTH']._serialized_end=917 + _globals['_ROUTERRECORDROTATION']._serialized_start=920 + _globals['_ROUTERRECORDROTATION']._serialized_end=1051 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=1053 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1122 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1124 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1221 + _globals['_ROUTERROTATIONINFO']._serialized_start=1224 + _globals['_ROUTERROTATIONINFO']._serialized_end=1461 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1464 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1778 + _globals['_USERRECORDACCESSREQUEST']._serialized_start=1780 + _globals['_USERRECORDACCESSREQUEST']._serialized_end=1840 + _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1842 + _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1939 + _globals['_USERRECORDACCESSREQUESTS']._serialized_start=1941 + _globals['_USERRECORDACCESSREQUESTS']._serialized_end=2018 + _globals['_USERRECORDACCESSRESPONSES']._serialized_start=2020 + _globals['_USERRECORDACCESSRESPONSES']._serialized_end=2100 + _globals['_ROTATIONSCHEDULE']._serialized_start=2102 + _globals['_ROTATIONSCHEDULE']._serialized_end=2158 + _globals['_APICALLBACKREQUEST']._serialized_start=2161 + _globals['_APICALLBACKREQUEST']._serialized_end=2305 + _globals['_APICALLBACKSCHEDULE']._serialized_start=2307 + _globals['_APICALLBACKSCHEDULE']._serialized_end=2360 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2362 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2426 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2428 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2517 + _globals['_CONNECTIONPARAMETERS']._serialized_start=2520 + _globals['_CONNECTIONPARAMETERS']._serialized_end=2653 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2655 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=2734 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=2736 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=2810 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=2812 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=2905 + _globals['_GETENFORCEMENTREQUEST']._serialized_start=2907 + _globals['_GETENFORCEMENTREQUEST']._serialized_end=2956 + _globals['_ENFORCEMENTTYPE']._serialized_start=2958 + _globals['_ENFORCEMENTTYPE']._serialized_end=3017 + _globals['_GETENFORCEMENTRESPONSE']._serialized_start=3019 + _globals['_GETENFORCEMENTRESPONSE']._serialized_end=3131 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=3133 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=3212 + _globals['_GETPEDMADMININFORESPONSE']._serialized_start=3214 + _globals['_GETPEDMADMININFORESPONSE']._serialized_end=3286 + _globals['_PAMNETWORKSETTINGS']._serialized_start=3288 + _globals['_PAMNETWORKSETTINGS']._serialized_end=3333 + _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_start=3336 + _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_end=3564 + _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_start=3566 + _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_end=3648 + _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_start=3650 + _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_end=3738 + _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_start=3740 + _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_end=3866 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_start=3868 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_end=3953 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_start=3955 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_end=4035 # @@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 b91019d5..486c4628 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.pyi @@ -1,9 +1,11 @@ import pam_pb2 as _pam_pb2 +import APIRequest_pb2 as _APIRequest_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 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 @@ -89,7 +91,7 @@ class RouterControllerMessage(_message.Message): streamResponse: bool payload: bytes timeout: int - 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: ... + def __init__(self, messageType: _Optional[_Union[_pam_pb2.ControllerMessageType, str]] = ..., messageUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., streamResponse: _Optional[bool] = ..., payload: _Optional[bytes] = ..., timeout: _Optional[int] = ...) -> None: ... class RouterUserAuth(_message.Message): __slots__ = ("transmissionKey", "sessionToken", "userId", "enterpriseUserId", "deviceName", "deviceToken", "clientVersionId", "needUsername", "username", "mspEnterpriseId", "isPedmAdmin", "mcEnterpriseId") @@ -117,7 +119,7 @@ class RouterUserAuth(_message.Message): mspEnterpriseId: int 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: ... + 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: _Optional[bool] = ..., username: _Optional[str] = ..., mspEnterpriseId: _Optional[int] = ..., isPedmAdmin: _Optional[bool] = ..., mcEnterpriseId: _Optional[int] = ...) -> None: ... class RouterDeviceAuth(_message.Message): __slots__ = ("clientId", "clientVersion", "signature", "enterpriseId", "nodeId", "deviceName", "deviceToken", "controllerName", "controllerUid", "ownerUser", "challenge", "ownerId", "maxInstanceCount") @@ -161,7 +163,7 @@ class RouterRecordRotation(_message.Message): controllerUid: bytes resourceUid: bytes noSchedule: bool - def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., noSchedule: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., noSchedule: _Optional[bool] = ...) -> None: ... class RouterRecordRotationsRequest(_message.Message): __slots__ = ("enterpriseId", "records") @@ -177,7 +179,7 @@ class RouterRecordRotationsResponse(_message.Message): HASMORE_FIELD_NUMBER: _ClassVar[int] rotations: _containers.RepeatedCompositeFieldContainer[RouterRecordRotation] hasMore: bool - def __init__(self, rotations: _Optional[_Iterable[_Union[RouterRecordRotation, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... + def __init__(self, rotations: _Optional[_Iterable[_Union[RouterRecordRotation, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... class RouterRotationInfo(_message.Message): __slots__ = ("status", "configurationUid", "resourceUid", "nodeId", "controllerUid", "controllerName", "scriptName", "pwdComplexity", "disabled") @@ -199,10 +201,10 @@ class RouterRotationInfo(_message.Message): scriptName: str pwdComplexity: str disabled: bool - def __init__(self, status: _Optional[_Union[RouterRotationStatus, str]] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., nodeId: _Optional[int] = ..., controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., scriptName: _Optional[str] = ..., pwdComplexity: _Optional[str] = ..., disabled: bool = ...) -> None: ... + def __init__(self, status: _Optional[_Union[RouterRotationStatus, str]] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., nodeId: _Optional[int] = ..., controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., scriptName: _Optional[str] = ..., pwdComplexity: _Optional[str] = ..., disabled: _Optional[bool] = ...) -> None: ... class RouterRecordRotationRequest(_message.Message): - __slots__ = ("recordUid", "revision", "configurationUid", "resourceUid", "schedule", "enterpriseUserId", "pwdComplexity", "disabled", "remoteAddress", "clientVersionId", "noop") + __slots__ = ("recordUid", "revision", "configurationUid", "resourceUid", "schedule", "enterpriseUserId", "pwdComplexity", "disabled", "remoteAddress", "clientVersionId", "noop", "saasConfiguration") RECORDUID_FIELD_NUMBER: _ClassVar[int] REVISION_FIELD_NUMBER: _ClassVar[int] CONFIGURATIONUID_FIELD_NUMBER: _ClassVar[int] @@ -214,6 +216,7 @@ class RouterRecordRotationRequest(_message.Message): REMOTEADDRESS_FIELD_NUMBER: _ClassVar[int] CLIENTVERSIONID_FIELD_NUMBER: _ClassVar[int] NOOP_FIELD_NUMBER: _ClassVar[int] + SAASCONFIGURATION_FIELD_NUMBER: _ClassVar[int] recordUid: bytes revision: int configurationUid: bytes @@ -225,7 +228,8 @@ class RouterRecordRotationRequest(_message.Message): remoteAddress: str clientVersionId: int noop: bool - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., enterpriseUserId: _Optional[int] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: bool = ..., remoteAddress: _Optional[str] = ..., clientVersionId: _Optional[int] = ..., noop: bool = ...) -> None: ... + saasConfiguration: bytes + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., enterpriseUserId: _Optional[int] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., remoteAddress: _Optional[str] = ..., clientVersionId: _Optional[int] = ..., noop: _Optional[bool] = ..., saasConfiguration: _Optional[bytes] = ...) -> None: ... class UserRecordAccessRequest(_message.Message): __slots__ = ("userId", "recordUid") @@ -353,7 +357,7 @@ class GetEnforcementResponse(_message.Message): enforcementTypes: _containers.RepeatedCompositeFieldContainer[EnforcementType] addOnIds: _containers.RepeatedScalarFieldContainer[int] isInTrial: bool - def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ..., addOnIds: _Optional[_Iterable[int]] = ..., isInTrial: bool = ...) -> None: ... + def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ..., addOnIds: _Optional[_Iterable[int]] = ..., isInTrial: _Optional[bool] = ...) -> None: ... class PEDMTOTPValidateRequest(_message.Message): __slots__ = ("username", "enterpriseId", "code") @@ -371,7 +375,7 @@ class GetPEDMAdminInfoResponse(_message.Message): PEDMADDONACTIVE_FIELD_NUMBER: _ClassVar[int] isPedmAdmin: bool pedmAddonActive: bool - def __init__(self, isPedmAdmin: bool = ..., pedmAddonActive: bool = ...) -> None: ... + def __init__(self, isPedmAdmin: _Optional[bool] = ..., pedmAddonActive: _Optional[bool] = ...) -> None: ... class PAMNetworkSettings(_message.Message): __slots__ = ("allowedSettings",) @@ -390,3 +394,49 @@ class PAMNetworkConfigurationRequest(_message.Message): resources: _containers.RepeatedCompositeFieldContainer[_pam_pb2.PAMResourceConfig] rotations: _containers.RepeatedCompositeFieldContainer[RouterRecordRotationRequest] def __init__(self, recordUid: _Optional[bytes] = ..., networkSettings: _Optional[_Union[PAMNetworkSettings, _Mapping]] = ..., resources: _Optional[_Iterable[_Union[_pam_pb2.PAMResourceConfig, _Mapping]]] = ..., rotations: _Optional[_Iterable[_Union[RouterRecordRotationRequest, _Mapping]]] = ...) -> None: ... + +class PAMDiscoveryRulesSetRequest(_message.Message): + __slots__ = ("networkUid", "rules", "rulesKey") + NETWORKUID_FIELD_NUMBER: _ClassVar[int] + RULES_FIELD_NUMBER: _ClassVar[int] + RULESKEY_FIELD_NUMBER: _ClassVar[int] + networkUid: bytes + rules: bytes + rulesKey: bytes + def __init__(self, networkUid: _Optional[bytes] = ..., rules: _Optional[bytes] = ..., rulesKey: _Optional[bytes] = ...) -> None: ... + +class Router2FAValidateRequest(_message.Message): + __slots__ = ("transmissionKey", "sessionToken", "value") + TRANSMISSIONKEY_FIELD_NUMBER: _ClassVar[int] + SESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + transmissionKey: bytes + sessionToken: bytes + value: str + def __init__(self, transmissionKey: _Optional[bytes] = ..., sessionToken: _Optional[bytes] = ..., value: _Optional[str] = ...) -> None: ... + +class Router2FASendPushRequest(_message.Message): + __slots__ = ("transmissionKey", "sessionToken", "pushType") + TRANSMISSIONKEY_FIELD_NUMBER: _ClassVar[int] + SESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] + PUSHTYPE_FIELD_NUMBER: _ClassVar[int] + transmissionKey: bytes + sessionToken: bytes + pushType: _APIRequest_pb2.TwoFactorPushType + def __init__(self, transmissionKey: _Optional[bytes] = ..., sessionToken: _Optional[bytes] = ..., pushType: _Optional[_Union[_APIRequest_pb2.TwoFactorPushType, str]] = ...) -> None: ... + +class Router2FAGetWebAuthnChallengeRequest(_message.Message): + __slots__ = ("transmissionKey", "sessionToken") + TRANSMISSIONKEY_FIELD_NUMBER: _ClassVar[int] + SESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] + transmissionKey: bytes + sessionToken: bytes + def __init__(self, transmissionKey: _Optional[bytes] = ..., sessionToken: _Optional[bytes] = ...) -> None: ... + +class Router2FAGetWebAuthnChallengeResponse(_message.Message): + __slots__ = ("challenge", "capabilities") + CHALLENGE_FIELD_NUMBER: _ClassVar[int] + CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + challenge: str + capabilities: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, challenge: _Optional[str] = ..., capabilities: _Optional[_Iterable[str]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py index 626e0830..f77d1963 100644 --- a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: ssocloud.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'ssocloud.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -17,7 +25,7 @@ from . import APIRequest_pb2 as APIRequest__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0essocloud.proto\x12\x08SsoCloud\x1a\x10\x41PIRequest.proto\"\xd5\x01\n\x14SsoCloudSettingValue\x12\x11\n\tsettingId\x18\x01 \x01(\x04\x12\x13\n\x0bsettingName\x18\x02 \x01(\t\x12\r\n\x05label\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\t\x12%\n\tvalueType\x18\x05 \x01(\x0e\x32\x12.SsoCloud.DataType\x12\x14\n\x0clastModified\x18\x07 \x01(\t\x12\x12\n\nisFromFile\x18\x08 \x01(\x08\x12\x12\n\nisEditable\x18\t \x01(\x08\x12\x12\n\nisRequired\x18\n \x01(\x08\"\x89\x01\n\x15SsoCloudSettingAction\x12\x11\n\tsettingId\x18\x01 \x01(\x04\x12\x13\n\x0bsettingName\x18\x02 \x01(\t\x12\x39\n\toperation\x18\x03 \x01(\x0e\x32&.SsoCloud.SsoCloudSettingOperationType\x12\r\n\x05value\x18\x04 \x01(\t\"\xe1\x01\n\x1cSsoCloudConfigurationRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x37\n\x13ssoAuthProtocolType\x18\x04 \x01(\x0e\x32\x1a.SsoCloud.AuthProtocolType\x12>\n\x15ssoCloudSettingAction\x18\x05 \x03(\x0b\x32\x1f.SsoCloud.SsoCloudSettingAction\"d\n\x13SsoSharedConfigItem\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoServiceProviderId\x18\x02 \x01(\x04\x12\x11\n\tssoNodeId\x18\x03 \x01(\x04\"\xad\x02\n\x1dSsoCloudConfigurationResponse\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x14\n\x0c\x65nterpriseId\x18\x03 \x01(\x04\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08protocol\x18\x05 \x01(\t\x12\x14\n\x0clastModified\x18\x06 \x01(\t\x12<\n\x14ssoCloudSettingValue\x18\x07 \x03(\x0b\x32\x1e.SsoCloud.SsoCloudSettingValue\x12\x10\n\x08isShared\x18\x08 \x01(\x08\x12\x34\n\rsharedConfigs\x18\t \x03(\x0b\x32\x1d.SsoCloud.SsoSharedConfigItem\"E\n\x11SsoIdpTypeRequest\x12\x14\n\x0cssoIdpTypeId\x18\x01 \x01(\r\x12\x0b\n\x03tag\x18\x02 \x01(\t\x12\r\n\x05label\x18\x03 \x01(\t\"F\n\x12SsoIdpTypeResponse\x12\x14\n\x0cssoIdpTypeId\x18\x01 \x01(\x05\x12\x0b\n\x03tag\x18\x02 \x01(\x05\x12\r\n\x05label\x18\x03 \x01(\x05\"6\n\x16SsoCloudSAMLLogRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\"\xdc\x01\n\x14SsoCloudSAMLLogEntry\x12\x12\n\nserverTime\x18\x01 \x01(\t\x12\x11\n\tdirection\x18\x02 \x01(\t\x12\x13\n\x0bmessageType\x18\x03 \x01(\t\x12\x15\n\rmessageIssued\x18\x04 \x01(\t\x12\x14\n\x0c\x66romEntityId\x18\x05 \x01(\t\x12\x12\n\nsamlStatus\x18\x06 \x01(\t\x12\x12\n\nrelayState\x18\x07 \x01(\t\x12\x13\n\x0bsamlContent\x18\x08 \x01(\t\x12\x10\n\x08isSigned\x18\t \x01(\x08\x12\x0c\n\x04isOK\x18\n \x01(\x08\"f\n\x17SsoCloudSAMLLogResponse\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12-\n\x05\x65ntry\x18\x02 \x03(\x0b\x32\x1e.SsoCloud.SsoCloudSAMLLogEntry\"b\n$SsoCloudServiceProviderUpdateRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\"]\n\x1aSsoCloudIdpMetadataRequest\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"\x9b\x01\n!SsoCloudIdpMetadataSupportRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x17\n\x0fssoEnterpriseId\x18\x03 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x04 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x05 \x01(\x0c\"F\n&SsoCloudConfigurationValidationRequest\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x03(\x04\"]\n\x11ValidationContent\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x14\n\x0cisSuccessful\x18\x02 \x01(\x08\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x03(\t\"a\n\'SsoCloudConfigurationValidationResponse\x12\x36\n\x11validationContent\x18\x01 \x03(\x0b\x32\x1b.SsoCloud.ValidationContent\"O\n/SsoCloudServiceProviderConfigurationListRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\"u\n\x15\x43onfigurationListItem\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\nisSelected\x18\x03 \x01(\x08\x12\x1c\n\x14ssoServiceProviderId\x18\x04 \x03(\x04\"n\n0SsoCloudServiceProviderConfigurationListResponse\x12:\n\x11\x63onfigurationItem\x18\x01 \x03(\x0b\x32\x1f.SsoCloud.ConfigurationListItem\"\xbf\x01\n\x0fSsoCloudRequest\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x10\n\x08\x65mbedded\x18\x03 \x01(\x08\x12\x0c\n\x04json\x18\x04 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x05 \x01(\t\x12\x14\n\x0cidpSessionId\x18\x06 \x01(\t\x12\x12\n\nforceLogin\x18\x07 \x01(\x08\x12\x10\n\x08username\x18\x08 \x01(\t\x12\x10\n\x08\x64\x65tached\x18\t \x01(\x08\"\xc9\x01\n\x10SsoCloudResponse\x12\x0f\n\x07\x63ommand\x18\x01 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x02 \x01(\x0c\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0c\x12\x14\n\x0cproviderName\x18\x05 \x01(\t\x12\x14\n\x0cidpSessionId\x18\x06 \x01(\t\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x07 \x01(\x0c\x12\x12\n\nerrorToken\x18\x08 \x01(\t\"Z\n\x12SsoCloudLogRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x13\n\x0bserviceName\x18\x02 \x01(\t\x12\x11\n\tserviceId\x18\x03 \x01(\r\"\x88\x02\n\x0eSamlRelayState\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x10\n\x08\x65mbedded\x18\x03 \x01(\x08\x12\x0c\n\x04json\x18\x04 \x01(\x08\x12\x0e\n\x06\x64\x65stId\x18\x05 \x01(\r\x12\r\n\x05keyId\x18\x06 \x01(\x05\x12<\n\x11supportedLanguage\x18\x07 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x10\n\x08\x63hecksum\x18\x08 \x01(\x04\x12\x16\n\x0eisGeneratedUid\x18\t \x01(\x08\x12\x10\n\x08\x64\x65viceId\x18\n \x01(\x03\x12\x10\n\x08\x64\x65tached\x18\x0b \x01(\x08\"q\n\x1eSsoCloudMigrationStatusRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x04\x12\x12\n\nfullStatus\x18\x02 \x01(\x08\x12\x1c\n\x14includeMigratedUsers\x18\x03 \x01(\x08\x12\r\n\x05limit\x18\x04 \x01(\x05\"\xe8\x02\n\x1fSsoCloudMigrationStatusResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x04\x12\x14\n\x0cssoConnectId\x18\x04 \x01(\x04\x12\x16\n\x0essoConnectName\x18\x05 \x01(\t\x12\x19\n\x11ssoConnectCloudId\x18\x06 \x01(\x04\x12\x1b\n\x13ssoConnectCloudName\x18\x07 \x01(\t\x12\x17\n\x0ftotalUsersCount\x18\x08 \x01(\r\x12\x1a\n\x12usersMigratedCount\x18\t \x01(\r\x12:\n\rmigratedUsers\x18\n \x03(\x0b\x32#.SsoCloud.SsoCloudMigrationUserInfo\x12<\n\x0funmigratedUsers\x18\x0b \x03(\x0b\x32#.SsoCloud.SsoCloudMigrationUserInfo\"`\n\x19SsoCloudMigrationUserInfo\x12\x0e\n\x06userId\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08\x66ullName\x18\x03 \x01(\t\x12\x12\n\nisMigrated\x18\x04 \x01(\x08*\x1d\n\x10\x41uthProtocolType\x12\t\n\x05SAML2\x10\x00*\x80\x02\n\x08\x44\x61taType\x12\x07\n\x03\x41NY\x10\x00\x12\x0b\n\x07\x42OOLEAN\x10\x01\x12\x0b\n\x07INTEGER\x10\x02\x12\n\n\x06STRING\x10\x03\x12\t\n\x05\x42YTES\x10\x04\x12\x07\n\x03URL\x10\x05\x12.\n*com_keepersecurity_proto_SsoCloud_DataType\x10\x06\x12\x36\n2com_keepersecurity_proto_SsoCloud_AuthProtocolType\x10\x07\x12\x30\n,com_keepersecurity_proto_SsoCloud_SsoIdpType\x10\x08\x12\x08\n\x04LONG\x10\t\x12\r\n\tTIMESTAMP\x10\n*R\n\x1cSsoCloudSettingOperationType\x12\x07\n\x03SET\x10\x00\x12\x07\n\x03GET\x10\x01\x12\n\n\x06\x44\x45LETE\x10\x02\x12\x14\n\x10RESET_TO_DEFAULT\x10\x03*\xd0\x02\n\nSsoIdpType\x12\r\n\tXX_UNUSED\x10\x00\x12\x0b\n\x07GENERIC\x10\x01\x12\x06\n\x02\x46\x35\x10\x02\x12\n\n\x06GOOGLE\x10\x03\x12\x08\n\x04OKTA\x10\x04\x12\x08\n\x04\x41\x44\x46S\x10\x05\x12\t\n\x05\x41ZURE\x10\x06\x12\x0c\n\x08ONELOGIN\x10\x07\x12\x07\n\x03\x41WS\x10\x08\x12\x0c\n\x08\x43\x45NTRIFY\x10\t\x12\x07\n\x03\x44UO\x10\n\x12\x07\n\x03IBM\x10\x0b\x12\r\n\tJUMPCLOUD\x10\x0c\x12\x08\n\x04PING\x10\r\x12\x0b\n\x07PINGONE\x10\x0e\x12\x07\n\x03RSA\x10\x0f\x12\x0e\n\nSECUREAUTH\x10\x10\x12\n\n\x06THALES\x10\x11\x12\t\n\x05\x41UTH0\x10\x12\x12\n\n\x06\x42\x45YOND\x10\x13\x12\x08\n\x04HYPR\x10\x14\x12\n\n\x06PUREID\x10\x15\x12\x07\n\x03SDO\x10\x16\x12\t\n\x05TRAIT\x10\x17\x12\x0c\n\x08TRANSMIT\x10\x18\x12\x0b\n\x07TRUSONA\x10\x19\x12\x0c\n\x08VERIDIUM\x10\x1a\x12\x07\n\x03\x43\x41S\x10\x1b\x42$\n\x18\x63om.keepersecurity.protoB\x08SsoCloudb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0essocloud.proto\x12\x08SsoCloud\x1a\x10\x41PIRequest.proto\"\xd5\x01\n\x14SsoCloudSettingValue\x12\x11\n\tsettingId\x18\x01 \x01(\x04\x12\x13\n\x0bsettingName\x18\x02 \x01(\t\x12\r\n\x05label\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\t\x12%\n\tvalueType\x18\x05 \x01(\x0e\x32\x12.SsoCloud.DataType\x12\x14\n\x0clastModified\x18\x07 \x01(\t\x12\x12\n\nisFromFile\x18\x08 \x01(\x08\x12\x12\n\nisEditable\x18\t \x01(\x08\x12\x12\n\nisRequired\x18\n \x01(\x08\"\x89\x01\n\x15SsoCloudSettingAction\x12\x11\n\tsettingId\x18\x01 \x01(\x04\x12\x13\n\x0bsettingName\x18\x02 \x01(\t\x12\x39\n\toperation\x18\x03 \x01(\x0e\x32&.SsoCloud.SsoCloudSettingOperationType\x12\r\n\x05value\x18\x04 \x01(\t\"\xe1\x01\n\x1cSsoCloudConfigurationRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x37\n\x13ssoAuthProtocolType\x18\x04 \x01(\x0e\x32\x1a.SsoCloud.AuthProtocolType\x12>\n\x15ssoCloudSettingAction\x18\x05 \x03(\x0b\x32\x1f.SsoCloud.SsoCloudSettingAction\"d\n\x13SsoSharedConfigItem\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoServiceProviderId\x18\x02 \x01(\x04\x12\x11\n\tssoNodeId\x18\x03 \x01(\x04\"\xad\x02\n\x1dSsoCloudConfigurationResponse\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x14\n\x0c\x65nterpriseId\x18\x03 \x01(\x04\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x10\n\x08protocol\x18\x05 \x01(\t\x12\x14\n\x0clastModified\x18\x06 \x01(\t\x12<\n\x14ssoCloudSettingValue\x18\x07 \x03(\x0b\x32\x1e.SsoCloud.SsoCloudSettingValue\x12\x10\n\x08isShared\x18\x08 \x01(\x08\x12\x34\n\rsharedConfigs\x18\t \x03(\x0b\x32\x1d.SsoCloud.SsoSharedConfigItem\"E\n\x11SsoIdpTypeRequest\x12\x14\n\x0cssoIdpTypeId\x18\x01 \x01(\r\x12\x0b\n\x03tag\x18\x02 \x01(\t\x12\r\n\x05label\x18\x03 \x01(\t\"F\n\x12SsoIdpTypeResponse\x12\x14\n\x0cssoIdpTypeId\x18\x01 \x01(\x05\x12\x0b\n\x03tag\x18\x02 \x01(\x05\x12\r\n\x05label\x18\x03 \x01(\x05\"6\n\x16SsoCloudSAMLLogRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\"\xdc\x01\n\x14SsoCloudSAMLLogEntry\x12\x12\n\nserverTime\x18\x01 \x01(\t\x12\x11\n\tdirection\x18\x02 \x01(\t\x12\x13\n\x0bmessageType\x18\x03 \x01(\t\x12\x15\n\rmessageIssued\x18\x04 \x01(\t\x12\x14\n\x0c\x66romEntityId\x18\x05 \x01(\t\x12\x12\n\nsamlStatus\x18\x06 \x01(\t\x12\x12\n\nrelayState\x18\x07 \x01(\t\x12\x13\n\x0bsamlContent\x18\x08 \x01(\t\x12\x10\n\x08isSigned\x18\t \x01(\x08\x12\x0c\n\x04isOK\x18\n \x01(\x08\"f\n\x17SsoCloudSAMLLogResponse\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12-\n\x05\x65ntry\x18\x02 \x03(\x0b\x32\x1e.SsoCloud.SsoCloudSAMLLogEntry\"b\n$SsoCloudServiceProviderUpdateRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\"]\n\x1aSsoCloudIdpMetadataRequest\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\x0c\"\x9b\x01\n!SsoCloudIdpMetadataSupportRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x1c\n\x14ssoSpConfigurationId\x18\x02 \x01(\x04\x12\x17\n\x0fssoEnterpriseId\x18\x03 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x04 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x05 \x01(\x0c\"F\n&SsoCloudConfigurationValidationRequest\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x03(\x04\"]\n\x11ValidationContent\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x14\n\x0cisSuccessful\x18\x02 \x01(\x08\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x03(\t\"a\n\'SsoCloudConfigurationValidationResponse\x12\x36\n\x11validationContent\x18\x01 \x03(\x0b\x32\x1b.SsoCloud.ValidationContent\"O\n/SsoCloudServiceProviderConfigurationListRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\"u\n\x15\x43onfigurationListItem\x12\x1c\n\x14ssoSpConfigurationId\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\nisSelected\x18\x03 \x01(\x08\x12\x1c\n\x14ssoServiceProviderId\x18\x04 \x03(\x04\"n\n0SsoCloudServiceProviderConfigurationListResponse\x12:\n\x11\x63onfigurationItem\x18\x01 \x03(\x0b\x32\x1f.SsoCloud.ConfigurationListItem\"\xbf\x01\n\x0fSsoCloudRequest\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x15\n\rclientVersion\x18\x02 \x01(\t\x12\x10\n\x08\x65mbedded\x18\x03 \x01(\x08\x12\x0c\n\x04json\x18\x04 \x01(\x08\x12\x0c\n\x04\x64\x65st\x18\x05 \x01(\t\x12\x14\n\x0cidpSessionId\x18\x06 \x01(\t\x12\x12\n\nforceLogin\x18\x07 \x01(\x08\x12\x10\n\x08username\x18\x08 \x01(\t\x12\x10\n\x08\x64\x65tached\x18\t \x01(\x08\"\xc9\x01\n\x10SsoCloudResponse\x12\x0f\n\x07\x63ommand\x18\x01 \x01(\t\x12\x19\n\x11messageSessionUid\x18\x02 \x01(\x0c\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12\x1b\n\x13\x65ncryptedLoginToken\x18\x04 \x01(\x0c\x12\x14\n\x0cproviderName\x18\x05 \x01(\t\x12\x14\n\x0cidpSessionId\x18\x06 \x01(\t\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x07 \x01(\x0c\x12\x12\n\nerrorToken\x18\x08 \x01(\t\"Z\n\x12SsoCloudLogRequest\x12\x1c\n\x14ssoServiceProviderId\x18\x01 \x01(\x04\x12\x13\n\x0bserviceName\x18\x02 \x01(\t\x12\x11\n\tserviceId\x18\x03 \x01(\r\"\x88\x02\n\x0eSamlRelayState\x12\x19\n\x11messageSessionUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x10\n\x08\x65mbedded\x18\x03 \x01(\x08\x12\x0c\n\x04json\x18\x04 \x01(\x08\x12\x0e\n\x06\x64\x65stId\x18\x05 \x01(\r\x12\r\n\x05keyId\x18\x06 \x01(\x05\x12<\n\x11supportedLanguage\x18\x07 \x01(\x0e\x32!.Authentication.SupportedLanguage\x12\x10\n\x08\x63hecksum\x18\x08 \x01(\x04\x12\x16\n\x0eisGeneratedUid\x18\t \x01(\x08\x12\x10\n\x08\x64\x65viceId\x18\n \x01(\x03\x12\x10\n\x08\x64\x65tached\x18\x0b \x01(\x08\"q\n\x1eSsoCloudMigrationStatusRequest\x12\x0e\n\x06nodeId\x18\x01 \x01(\x04\x12\x12\n\nfullStatus\x18\x02 \x01(\x08\x12\x1c\n\x14includeMigratedUsers\x18\x03 \x01(\x08\x12\r\n\x05limit\x18\x04 \x01(\x05\"\xe8\x02\n\x1fSsoCloudMigrationStatusResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0e\n\x06nodeId\x18\x03 \x01(\x04\x12\x14\n\x0cssoConnectId\x18\x04 \x01(\x04\x12\x16\n\x0essoConnectName\x18\x05 \x01(\t\x12\x19\n\x11ssoConnectCloudId\x18\x06 \x01(\x04\x12\x1b\n\x13ssoConnectCloudName\x18\x07 \x01(\t\x12\x17\n\x0ftotalUsersCount\x18\x08 \x01(\r\x12\x1a\n\x12usersMigratedCount\x18\t \x01(\r\x12:\n\rmigratedUsers\x18\n \x03(\x0b\x32#.SsoCloud.SsoCloudMigrationUserInfo\x12<\n\x0funmigratedUsers\x18\x0b \x03(\x0b\x32#.SsoCloud.SsoCloudMigrationUserInfo\"`\n\x19SsoCloudMigrationUserInfo\x12\x0e\n\x06userId\x18\x01 \x01(\r\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08\x66ullName\x18\x03 \x01(\t\x12\x12\n\nisMigrated\x18\x04 \x01(\x08*&\n\x10\x41uthProtocolType\x12\t\n\x05SAML2\x10\x00\x12\x07\n\x03JWT\x10\x01*\x80\x02\n\x08\x44\x61taType\x12\x07\n\x03\x41NY\x10\x00\x12\x0b\n\x07\x42OOLEAN\x10\x01\x12\x0b\n\x07INTEGER\x10\x02\x12\n\n\x06STRING\x10\x03\x12\t\n\x05\x42YTES\x10\x04\x12\x07\n\x03URL\x10\x05\x12.\n*com_keepersecurity_proto_SsoCloud_DataType\x10\x06\x12\x36\n2com_keepersecurity_proto_SsoCloud_AuthProtocolType\x10\x07\x12\x30\n,com_keepersecurity_proto_SsoCloud_SsoIdpType\x10\x08\x12\x08\n\x04LONG\x10\t\x12\r\n\tTIMESTAMP\x10\n*R\n\x1cSsoCloudSettingOperationType\x12\x07\n\x03SET\x10\x00\x12\x07\n\x03GET\x10\x01\x12\n\n\x06\x44\x45LETE\x10\x02\x12\x14\n\x10RESET_TO_DEFAULT\x10\x03*\xd0\x02\n\nSsoIdpType\x12\r\n\tXX_UNUSED\x10\x00\x12\x0b\n\x07GENERIC\x10\x01\x12\x06\n\x02\x46\x35\x10\x02\x12\n\n\x06GOOGLE\x10\x03\x12\x08\n\x04OKTA\x10\x04\x12\x08\n\x04\x41\x44\x46S\x10\x05\x12\t\n\x05\x41ZURE\x10\x06\x12\x0c\n\x08ONELOGIN\x10\x07\x12\x07\n\x03\x41WS\x10\x08\x12\x0c\n\x08\x43\x45NTRIFY\x10\t\x12\x07\n\x03\x44UO\x10\n\x12\x07\n\x03IBM\x10\x0b\x12\r\n\tJUMPCLOUD\x10\x0c\x12\x08\n\x04PING\x10\r\x12\x0b\n\x07PINGONE\x10\x0e\x12\x07\n\x03RSA\x10\x0f\x12\x0e\n\nSECUREAUTH\x10\x10\x12\n\n\x06THALES\x10\x11\x12\t\n\x05\x41UTH0\x10\x12\x12\n\n\x06\x42\x45YOND\x10\x13\x12\x08\n\x04HYPR\x10\x14\x12\n\n\x06PUREID\x10\x15\x12\x07\n\x03SDO\x10\x16\x12\t\n\x05TRAIT\x10\x17\x12\x0c\n\x08TRANSMIT\x10\x18\x12\x0b\n\x07TRUSONA\x10\x19\x12\x0c\n\x08VERIDIUM\x10\x1a\x12\x07\n\x03\x43\x41S\x10\x1b\x42$\n\x18\x63om.keepersecurity.protoB\x08SsoCloudb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -26,13 +34,13 @@ _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\010SsoCloud' _globals['_AUTHPROTOCOLTYPE']._serialized_start=3826 - _globals['_AUTHPROTOCOLTYPE']._serialized_end=3855 - _globals['_DATATYPE']._serialized_start=3858 - _globals['_DATATYPE']._serialized_end=4114 - _globals['_SSOCLOUDSETTINGOPERATIONTYPE']._serialized_start=4116 - _globals['_SSOCLOUDSETTINGOPERATIONTYPE']._serialized_end=4198 - _globals['_SSOIDPTYPE']._serialized_start=4201 - _globals['_SSOIDPTYPE']._serialized_end=4537 + _globals['_AUTHPROTOCOLTYPE']._serialized_end=3864 + _globals['_DATATYPE']._serialized_start=3867 + _globals['_DATATYPE']._serialized_end=4123 + _globals['_SSOCLOUDSETTINGOPERATIONTYPE']._serialized_start=4125 + _globals['_SSOCLOUDSETTINGOPERATIONTYPE']._serialized_end=4207 + _globals['_SSOIDPTYPE']._serialized_start=4210 + _globals['_SSOIDPTYPE']._serialized_end=4546 _globals['_SSOCLOUDSETTINGVALUE']._serialized_start=47 _globals['_SSOCLOUDSETTINGVALUE']._serialized_end=260 _globals['_SSOCLOUDSETTINGACTION']._serialized_start=263 diff --git a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi index d399dd53..ac2bf794 100644 --- a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi @@ -3,13 +3,15 @@ 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 class AuthProtocolType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () SAML2: _ClassVar[AuthProtocolType] + JWT: _ClassVar[AuthProtocolType] class DataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -63,6 +65,7 @@ class SsoIdpType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): VERIDIUM: _ClassVar[SsoIdpType] CAS: _ClassVar[SsoIdpType] SAML2: AuthProtocolType +JWT: AuthProtocolType ANY: DataType BOOLEAN: DataType INTEGER: DataType @@ -127,7 +130,7 @@ class SsoCloudSettingValue(_message.Message): isFromFile: bool isEditable: bool isRequired: bool - def __init__(self, settingId: _Optional[int] = ..., settingName: _Optional[str] = ..., label: _Optional[str] = ..., value: _Optional[str] = ..., valueType: _Optional[_Union[DataType, str]] = ..., lastModified: _Optional[str] = ..., isFromFile: bool = ..., isEditable: bool = ..., isRequired: bool = ...) -> None: ... + def __init__(self, settingId: _Optional[int] = ..., settingName: _Optional[str] = ..., label: _Optional[str] = ..., value: _Optional[str] = ..., valueType: _Optional[_Union[DataType, str]] = ..., lastModified: _Optional[str] = ..., isFromFile: _Optional[bool] = ..., isEditable: _Optional[bool] = ..., isRequired: _Optional[bool] = ...) -> None: ... class SsoCloudSettingAction(_message.Message): __slots__ = ("settingId", "settingName", "operation", "value") @@ -185,7 +188,7 @@ class SsoCloudConfigurationResponse(_message.Message): ssoCloudSettingValue: _containers.RepeatedCompositeFieldContainer[SsoCloudSettingValue] isShared: bool sharedConfigs: _containers.RepeatedCompositeFieldContainer[SsoSharedConfigItem] - def __init__(self, ssoServiceProviderId: _Optional[int] = ..., ssoSpConfigurationId: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., name: _Optional[str] = ..., protocol: _Optional[str] = ..., lastModified: _Optional[str] = ..., ssoCloudSettingValue: _Optional[_Iterable[_Union[SsoCloudSettingValue, _Mapping]]] = ..., isShared: bool = ..., sharedConfigs: _Optional[_Iterable[_Union[SsoSharedConfigItem, _Mapping]]] = ...) -> None: ... + def __init__(self, ssoServiceProviderId: _Optional[int] = ..., ssoSpConfigurationId: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., name: _Optional[str] = ..., protocol: _Optional[str] = ..., lastModified: _Optional[str] = ..., ssoCloudSettingValue: _Optional[_Iterable[_Union[SsoCloudSettingValue, _Mapping]]] = ..., isShared: _Optional[bool] = ..., sharedConfigs: _Optional[_Iterable[_Union[SsoSharedConfigItem, _Mapping]]] = ...) -> None: ... class SsoIdpTypeRequest(_message.Message): __slots__ = ("ssoIdpTypeId", "tag", "label") @@ -235,7 +238,7 @@ class SsoCloudSAMLLogEntry(_message.Message): samlContent: str isSigned: bool isOK: bool - def __init__(self, serverTime: _Optional[str] = ..., direction: _Optional[str] = ..., messageType: _Optional[str] = ..., messageIssued: _Optional[str] = ..., fromEntityId: _Optional[str] = ..., samlStatus: _Optional[str] = ..., relayState: _Optional[str] = ..., samlContent: _Optional[str] = ..., isSigned: bool = ..., isOK: bool = ...) -> None: ... + def __init__(self, serverTime: _Optional[str] = ..., direction: _Optional[str] = ..., messageType: _Optional[str] = ..., messageIssued: _Optional[str] = ..., fromEntityId: _Optional[str] = ..., samlStatus: _Optional[str] = ..., relayState: _Optional[str] = ..., samlContent: _Optional[str] = ..., isSigned: _Optional[bool] = ..., isOK: _Optional[bool] = ...) -> None: ... class SsoCloudSAMLLogResponse(_message.Message): __slots__ = ("ssoServiceProviderId", "entry") @@ -291,7 +294,7 @@ class ValidationContent(_message.Message): ssoSpConfigurationId: int isSuccessful: bool errorMessage: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., isSuccessful: bool = ..., errorMessage: _Optional[_Iterable[str]] = ...) -> None: ... + def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., isSuccessful: _Optional[bool] = ..., errorMessage: _Optional[_Iterable[str]] = ...) -> None: ... class SsoCloudConfigurationValidationResponse(_message.Message): __slots__ = ("validationContent",) @@ -315,7 +318,7 @@ class ConfigurationListItem(_message.Message): name: str isSelected: bool ssoServiceProviderId: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., name: _Optional[str] = ..., isSelected: bool = ..., ssoServiceProviderId: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., name: _Optional[str] = ..., isSelected: _Optional[bool] = ..., ssoServiceProviderId: _Optional[_Iterable[int]] = ...) -> None: ... class SsoCloudServiceProviderConfigurationListResponse(_message.Message): __slots__ = ("configurationItem",) @@ -343,7 +346,7 @@ class SsoCloudRequest(_message.Message): forceLogin: bool username: str detached: bool - def __init__(self, messageSessionUid: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., embedded: bool = ..., json: bool = ..., dest: _Optional[str] = ..., idpSessionId: _Optional[str] = ..., forceLogin: bool = ..., username: _Optional[str] = ..., detached: bool = ...) -> None: ... + def __init__(self, messageSessionUid: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., embedded: _Optional[bool] = ..., json: _Optional[bool] = ..., dest: _Optional[str] = ..., idpSessionId: _Optional[str] = ..., forceLogin: _Optional[bool] = ..., username: _Optional[str] = ..., detached: _Optional[bool] = ...) -> None: ... class SsoCloudResponse(_message.Message): __slots__ = ("command", "messageSessionUid", "email", "encryptedLoginToken", "providerName", "idpSessionId", "encryptedSessionToken", "errorToken") @@ -399,7 +402,7 @@ class SamlRelayState(_message.Message): isGeneratedUid: bool deviceId: int detached: bool - def __init__(self, messageSessionUid: _Optional[bytes] = ..., username: _Optional[str] = ..., embedded: bool = ..., json: bool = ..., destId: _Optional[int] = ..., keyId: _Optional[int] = ..., supportedLanguage: _Optional[_Union[_APIRequest_pb2.SupportedLanguage, str]] = ..., checksum: _Optional[int] = ..., isGeneratedUid: bool = ..., deviceId: _Optional[int] = ..., detached: bool = ...) -> None: ... + def __init__(self, messageSessionUid: _Optional[bytes] = ..., username: _Optional[str] = ..., embedded: _Optional[bool] = ..., json: _Optional[bool] = ..., destId: _Optional[int] = ..., keyId: _Optional[int] = ..., supportedLanguage: _Optional[_Union[_APIRequest_pb2.SupportedLanguage, str]] = ..., checksum: _Optional[int] = ..., isGeneratedUid: _Optional[bool] = ..., deviceId: _Optional[int] = ..., detached: _Optional[bool] = ...) -> None: ... class SsoCloudMigrationStatusRequest(_message.Message): __slots__ = ("nodeId", "fullStatus", "includeMigratedUsers", "limit") @@ -411,7 +414,7 @@ class SsoCloudMigrationStatusRequest(_message.Message): fullStatus: bool includeMigratedUsers: bool limit: int - def __init__(self, nodeId: _Optional[int] = ..., fullStatus: bool = ..., includeMigratedUsers: bool = ..., limit: _Optional[int] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., fullStatus: _Optional[bool] = ..., includeMigratedUsers: _Optional[bool] = ..., limit: _Optional[int] = ...) -> None: ... class SsoCloudMigrationStatusResponse(_message.Message): __slots__ = ("success", "message", "nodeId", "ssoConnectId", "ssoConnectName", "ssoConnectCloudId", "ssoConnectCloudName", "totalUsersCount", "usersMigratedCount", "migratedUsers", "unmigratedUsers") @@ -437,7 +440,7 @@ class SsoCloudMigrationStatusResponse(_message.Message): usersMigratedCount: int migratedUsers: _containers.RepeatedCompositeFieldContainer[SsoCloudMigrationUserInfo] unmigratedUsers: _containers.RepeatedCompositeFieldContainer[SsoCloudMigrationUserInfo] - def __init__(self, success: bool = ..., message: _Optional[str] = ..., nodeId: _Optional[int] = ..., ssoConnectId: _Optional[int] = ..., ssoConnectName: _Optional[str] = ..., ssoConnectCloudId: _Optional[int] = ..., ssoConnectCloudName: _Optional[str] = ..., totalUsersCount: _Optional[int] = ..., usersMigratedCount: _Optional[int] = ..., migratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ..., unmigratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ...) -> None: ... + def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., nodeId: _Optional[int] = ..., ssoConnectId: _Optional[int] = ..., ssoConnectName: _Optional[str] = ..., ssoConnectCloudId: _Optional[int] = ..., ssoConnectCloudName: _Optional[str] = ..., totalUsersCount: _Optional[int] = ..., usersMigratedCount: _Optional[int] = ..., migratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ..., unmigratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ...) -> None: ... class SsoCloudMigrationUserInfo(_message.Message): __slots__ = ("userId", "email", "fullName", "isMigrated") @@ -449,4 +452,4 @@ class SsoCloudMigrationUserInfo(_message.Message): email: str fullName: str isMigrated: bool - def __init__(self, userId: _Optional[int] = ..., email: _Optional[str] = ..., fullName: _Optional[str] = ..., isMigrated: bool = ...) -> None: ... + def __init__(self, userId: _Optional[int] = ..., email: _Optional[str] = ..., fullName: _Optional[str] = ..., isMigrated: _Optional[bool] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/version_pb2.py b/keepersdk-package/src/keepersdk/proto/version_pb2.py index ae1347f8..ee97004f 100644 --- a/keepersdk-package/src/keepersdk/proto/version_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/version_pb2.py @@ -2,13 +2,21 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: version.proto -# Protobuf Python Version: 5.29.3 +# Protobuf Python Version: 7.34.1 """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, + 7, + 34, + 1, + '', + 'version.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() diff --git a/keepersdk-package/src/keepersdk/utils.py b/keepersdk-package/src/keepersdk/utils.py index e8e3f70d..6cb42c8d 100644 --- a/keepersdk-package/src/keepersdk/utils.py +++ b/keepersdk-package/src/keepersdk/utils.py @@ -2,6 +2,7 @@ import logging import math import re +import sys import time from typing import Iterator, Callable, Optional from urllib.parse import urlparse @@ -291,3 +292,87 @@ def get_default_path(): default_path = Path.home().joinpath('.keeper') default_path.mkdir(parents=True, exist_ok=True) return default_path + + +def get_ssl_cert_file(): + """Get SSL certificate file path, preferring system CA store for corporate environments like Zscaler""" + import ssl + import platform + import certifi + import os + + # Allow user to override via environment variable + user_cert_file = os.getenv('KEEPER_SSL_CERT_FILE') + if user_cert_file: + if user_cert_file.lower() == 'system': + pass # Continue with system detection below + elif user_cert_file.lower() == 'certifi': + return certifi.where() + elif user_cert_file.lower() == 'none' or user_cert_file.lower() == 'false': + return False # Disable SSL verification + elif os.path.exists(user_cert_file): + return user_cert_file + else: + # Don't use logging here as it can interfere with main logging config + print(f"Warning: SSL cert file specified in KEEPER_SSL_CERT_FILE not found: {user_cert_file}", file=sys.stderr) + + # Try to use system CA store first for corporate environments + try: + # On macOS, try Homebrew certificates first (better for corporate environments like Zscaler) + if platform.system() == 'Darwin': + system_ca_paths = [ + '/opt/homebrew/etc/ca-certificates/cert.pem', # Homebrew CA bundle (best for Zscaler) + '/usr/local/etc/ssl/cert.pem', # Homebrew SSL (older location) + '/etc/ssl/cert.pem', # macOS system CA bundle + ] + for ca_path in system_ca_paths: + if os.path.exists(ca_path): + return ca_path + + # On Linux/Unix systems + elif platform.system() == 'Linux': + system_ca_paths = [ + '/etc/ssl/certs/ca-certificates.crt', # Debian/Ubuntu + '/etc/pki/tls/certs/ca-bundle.crt', # RHEL/CentOS + '/etc/ssl/ca-bundle.pem', # OpenSUSE + '/etc/ssl/cert.pem', # Generic + ] + for ca_path in system_ca_paths: + if os.path.exists(ca_path): + return ca_path + + # Try to get default SSL context locations + try: + default_locations = ssl.get_default_verify_paths() + if default_locations.cafile and os.path.exists(default_locations.cafile): + return default_locations.cafile + if default_locations.capath and os.path.exists(default_locations.capath): + return default_locations.capath + except: + pass + + except Exception: + pass + + # Fall back to certifi if system CA not available + return certifi.where() + +def ssl_aware_request(method, url, **kwargs): + """Make an SSL-aware HTTP request using system CA certificates when available""" + import requests + + # Only set verify if not already specified + if 'verify' not in kwargs: + cert_file = get_ssl_cert_file() + if cert_file is False: + kwargs['verify'] = False + elif cert_file: + kwargs['verify'] = cert_file + # If cert_file is None, let requests use its default + + return requests.request(method, url, **kwargs) + + +def ssl_aware_get(url, **kwargs): + """SSL-aware GET request using system CA certificates when available""" + return ssl_aware_request('GET', url, **kwargs) \ No newline at end of file diff --git a/keepersdk-package/src/keepersdk/vault/record_management.py b/keepersdk-package/src/keepersdk/vault/record_management.py index 2c87b566..c3c9f44c 100644 --- a/keepersdk-package/src/keepersdk/vault/record_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_management.py @@ -468,7 +468,7 @@ def notify_on_warning(message: str) -> None: record_uid: str if len(record_uid_to_move) > 0: for record_uid in record_uid_to_move: - folders = vault_utils.get_folders_for_record(vault.vault_data, record_uid) + folders = vault_utils.get_folders_for_move(vault.vault_data, record_uid) if record_uid in dst_folder.subfolders: notify_on_warning(f'Destination folder already contains record \"{record_uid}\".') else: @@ -486,7 +486,7 @@ def notify_on_warning(message: str) -> None: 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)) + selected_folder_uid = '' folders = [x for x in folders if x.folder_uid != selected_folder_uid] if selected_folder_uid not in record_to_move: diff --git a/keepersdk-package/src/keepersdk/vault/vault_data.py b/keepersdk-package/src/keepersdk/vault/vault_data.py index 7c17947a..4e4a2851 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_data.py +++ b/keepersdk-package/src/keepersdk/vault/vault_data.py @@ -796,7 +796,7 @@ def load_keeper_record(record: storage_types.StorageRecord, record_key: bytes) - if record.version in {0, 1, 2}: data_bytes = crypto.decrypt_aes_v1(record.data, record_key) data_dict = json.loads(data_bytes.decode()) - elif record.version in {3, 4, 5}: + elif record.version in {3, 4, 5, 6}: data_bytes = crypto.decrypt_aes_v2(record.data, record_key) data_dict = json.loads(data_bytes.decode()) else: diff --git a/keepersdk-package/src/keepersdk/vault/vault_utils.py b/keepersdk-package/src/keepersdk/vault/vault_utils.py index ea54ef6d..e698202a 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_utils.py +++ b/keepersdk-package/src/keepersdk/vault/vault_utils.py @@ -71,6 +71,23 @@ def record_exists(f: vault_types.Folder) -> None: return result +def get_folders_for_move(vault: vault_data.VaultData, record_uid: str) -> List[vault_types.Folder]: + """ + Folders to use as the move source, matching ``get_folders_for_record`` when possible. + + The folder tree only includes a record where there is a ``folder_records`` link after + :meth:`VaultData.build_folders`. Records created by some endpoints (e.g. PAM + ``pam/add_configuration_record``) can appear in the vault on sync without a + ``userFolderRecord`` link yet, so :func:`get_folders_for_record` is empty even though + the record exists. For ``move`` those records should still be treated as coming from + the user root (``folder_uid`` of the root folder, ``''``). + """ + folders = get_folders_for_record(vault, record_uid) + if not folders and vault.get_record(record_uid) is not None: + return [vault.root_folder] + return folders + + def load_available_teams(auth: keeper_auth.KeeperAuth) -> Iterable[vault_types.TeamInfo]: rq = { 'command': 'get_available_teams' } rs = auth.execute_auth_command(rq) From 9b34d815b3fddc7851e4ba1eecb3a5b9e253bf77 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 11 May 2026 17:07:05 +0530 Subject: [PATCH 14/23] msp billing-report, legacy-report, copy-role and switch-to-msp commands added --- keepercli-package/src/keepercli/cli.py | 9 +- .../src/keepercli/commands/msp.py | 173 +++++ .../src/keepercli/register_commands.py | 4 + .../src/keepersdk/enterprise/msp_auth.py | 595 +++++++++++++++++- 4 files changed, 778 insertions(+), 3 deletions(-) diff --git a/keepercli-package/src/keepercli/cli.py b/keepercli-package/src/keepercli/cli.py index c3e8f79c..2b7e24f6 100644 --- a/keepercli-package/src/keepercli/cli.py +++ b/keepercli-package/src/keepercli/cli.py @@ -149,8 +149,13 @@ def get_prompt() -> str: result = do_command(command, context, commands) error_no = 0 if isinstance(result, KeeperParams): - context_stack.append(context) - context = result + if len(context_stack) > 0 and result is context_stack[-1]: + context_to_release = context + context = context_stack.pop() + context_to_release.clear_session() + else: + context_stack.append(context) + context = result elif isinstance(result, str): prompt_utils.output_text(result) except base.CommandError as ce: diff --git a/keepercli-package/src/keepercli/commands/msp.py b/keepercli-package/src/keepercli/commands/msp.py index e4271908..6924db41 100644 --- a/keepercli-package/src/keepercli/commands/msp.py +++ b/keepercli-package/src/keepercli/commands/msp.py @@ -9,6 +9,7 @@ from ..params import KeeperParams _MSP_PLAN_CHOICES = [x[1] for x in enterprise_constants.MSP_PLANS] +_MSP_LEGACY_RANGES = ['today', 'yesterday', 'last_7_days', 'last_30_days', 'month_to_date', 'last_month', 'year_to_date', 'last_year'] class MspDownCommand(base.ArgparseCommand): @@ -319,6 +320,177 @@ def execute(self, context: KeeperParams, **kwargs): return mc_id +class MspCopyRoleCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-copy-role', + description='Copy one or more MSP roles (with enforcements) to managed companies.', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument( + '-r', '--role', dest='role', action='append', + help='Source role name or role ID (can be repeated)', + ) + parser.add_argument( + 'mc', action='store', nargs='+', + help='Managed company identifier(s): name or id', + ) + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + + role_inputs = kwargs.get('role') + mc_inputs = kwargs.get('mc') + if not isinstance(role_inputs, list) or len(role_inputs) == 0: + raise base.CommandError('Source role parameter is required') + if not isinstance(mc_inputs, list) or len(mc_inputs) == 0: + raise base.CommandError('Managed company parameter is required') + + try: + synced = msp_auth.msp_copy_role( + enterprise_loader, + roles=[str(x) for x in role_inputs], + managed_companies=[str(x) for x in mc_inputs], + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + api.get_logger().info('Roles synced to %d managed compan%s', len(synced), 'y' if len(synced) == 1 else 'ies') + return sorted(synced) + + +class MspBillingReportCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-billing-report', + parents=[base.report_output_parser], + description='Generate MSP billing reports.', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--month', dest='month', action='store', metavar='YYYY-MM', + help='Month for billing report (e.g. 2022-02)') + parser.add_argument('-d', '--show-date', dest='show_date', action='store_true', + help='Breakdown report by date') + parser.add_argument('-c', '--show-company', dest='show_company', action='store_true', + help='Breakdown report by managed company') + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + + try: + report = msp_auth.msp_billing_report( + enterprise_loader, + month=kwargs.get('month'), + show_date=bool(kwargs.get('show_date')), + show_company=bool(kwargs.get('show_company')), + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + headers = list(report.headers) + fmt = kwargs.get('format') + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + return report_utils.dump_report_data( + [list(r) for r in report.rows], + headers, + fmt=fmt, + filename=kwargs.get('output'), + title=report.title, + ) + + +class MspLegacyReportCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='msp-legacy-report', + parents=[base.report_output_parser], + description='Generate MSP legacy billing report.', + ) + self.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + predefined = parser.add_argument_group('Pre-defined date ranges') + predefined.add_argument( + '--range', dest='range', choices=_MSP_LEGACY_RANGES, default='last_30_days', + help='Pre-defined date ranges to run the report.', + ) + custom = parser.add_argument_group('Custom date ranges') + custom.add_argument( + '--from', dest='from_date', + help='Run report from this date: YYYY-MM-DD or Unix timestamp.', + ) + custom.add_argument( + '--to', dest='to_date', + help='Run report until this date: YYYY-MM-DD or Unix timestamp.', + ) + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + enterprise_loader = context.enterprise_loader + + try: + report = msp_auth.msp_legacy_report( + enterprise_loader, + range_name=str(kwargs.get('range') or 'last_30_days'), + from_date=kwargs.get('from_date'), + to_date=kwargs.get('to_date'), + ) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + headers = list(report.headers) + fmt = kwargs.get('format') + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + return report_utils.dump_report_data( + [list(r) for r in report.rows], + headers, + fmt=fmt, + filename=kwargs.get('output'), + title=report.title, + ) + + +class SwitchToMspCommand(base.ArgparseCommand): + parser = argparse.ArgumentParser(prog='switch-to-msp', description='Switch back to MSP tenant context') + + def __init__(self): + super().__init__(SwitchToMspCommand.parser) + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + base.require_enterprise_admin(context) + logger = api.get_logger() + + msp_context = context.environment_variables.get('__msp_context__') + if not isinstance(msp_context, KeeperParams): + raise base.CommandError('Already MSP') + + try: + msp_auth.switch_to_msp(msp_context.enterprise_loader) + except sdk_errors.KeeperError as e: + raise base.CommandError(str(e)) from e + + logger.info('Switched back to MSP') + return msp_context + + class SwitchToManagedCompanyCommand(base.ArgparseCommand): parser = argparse.ArgumentParser(prog='switch-to-mc', description='Switch to a managed company context') parser.add_argument('mc_id', type=int, help='Managed company ID') @@ -343,6 +515,7 @@ def execute(self, context: KeeperParams, **kwargs): mc_context = KeeperParams(context.keeper_config) mc_context.set_auth(mc_auth, tree_key=tree_key, skip_vault=True) + mc_context.environment_variables['__msp_context__'] = context logger.info('Successfully switched to managed company %s', mc_id) logger.info('Use "q" to return to the previous context') diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index dbe4494b..bbc0be13 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -114,7 +114,11 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('msp-update', msp.MspUpdateCommand(), base.CommandScope.Enterprise, 'mu') commands.register_command('msp-remove', msp.MspRemoveCommand(), base.CommandScope.Enterprise, 'mrm') commands.register_command('msp-convert-node', msp.MspConvertNodeCommand(), base.CommandScope.Enterprise) + commands.register_command('msp-copy-role', msp.MspCopyRoleCommand(), base.CommandScope.Enterprise) + commands.register_command('msp-billing-report', msp.MspBillingReportCommand(), base.CommandScope.Enterprise, 'mbr') + commands.register_command('msp-legacy-report', msp.MspLegacyReportCommand(), base.CommandScope.Enterprise, 'mlr') commands.register_command('switch-to-mc', msp.SwitchToManagedCompanyCommand(), base.CommandScope.Enterprise) + commands.register_command('switch-to-msp', msp.SwitchToMspCommand(), base.CommandScope.Enterprise) commands.register_command('team-approve', enterprise_team.TeamApproveCommand(), base.CommandScope.Enterprise) commands.register_command('user-report', user_report.UserReportCommand(), base.CommandScope.Enterprise, 'ur') commands.register_command('security-audit-report', security_audit_report.SecurityAuditReportCommand(), base.CommandScope.Enterprise, 'sar') diff --git a/keepersdk-package/src/keepersdk/enterprise/msp_auth.py b/keepersdk-package/src/keepersdk/enterprise/msp_auth.py index 6126f9a5..e39efe19 100644 --- a/keepersdk-package/src/keepersdk/enterprise/msp_auth.py +++ b/keepersdk-package/src/keepersdk/enterprise/msp_auth.py @@ -1,7 +1,9 @@ +import calendar +import datetime import json from dataclasses import dataclass from enum import Enum -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union from urllib.parse import urlunparse from . import enterprise_types @@ -23,6 +25,13 @@ _CMD_ENTERPRISE_REGISTRATION_BY_MSP = 'enterprise_registration_by_msp' _CMD_ENTERPRISE_UPDATE_BY_MSP = 'enterprise_update_by_msp' _CMD_ENTERPRISE_REMOVE_BY_MSP = 'enterprise_remove_by_msp' +_CMD_ENTERPRISE_ALLOCATE_IDS = 'enterprise_allocate_ids' +_CMD_QUERY_ENTERPRISE = 'query_enterprise' +_CMD_ROLE_ADD = 'role_add' +_CMD_ROLE_ENFORCEMENT_ADD = 'role_enforcement_add' +_CMD_ROLE_ENFORCEMENT_UPDATE = 'role_enforcement_update' +_CMD_ROLE_ENFORCEMENT_REMOVE = 'role_enforcement_remove' +_CMD_GET_MC_LICENSE_ADJUSTMENT_LOG = 'get_mc_license_adjustment_log' _REST_LOGIN_TO_MC = 'authentication/login_to_mc' _REST_NODE_TO_MANAGED_COMPANY = 'enterprise/node_to_managed_company' _REST_USER_DATA_KEY_BY_NODE = 'enterprise/get_enterprise_user_data_key_by_node' @@ -42,6 +51,17 @@ _ENCRYPTED_BY_DATA_KEY = 'encrypted_by_data_key' _USER_DATA_KEY_TYPE_ID_ECC = 4 _UNKNOWN_USER_LABEL = '?' +class LegacyDateRange(str, Enum): + """Preset relative date ranges for MSP legacy reports.""" + + TODAY = 'today' + YESTERDAY = 'yesterday' + LAST_7_DAYS = 'last_7_days' + LAST_30_DAYS = 'last_30_days' + MONTH_TO_DATE = 'month_to_date' + LAST_MONTH = 'last_month' + YEAR_TO_DATE = 'year_to_date' + LAST_YEAR = 'last_year' @dataclass(frozen=True) @@ -53,6 +73,20 @@ class MspInfoReport: message: Optional[str] = None +@dataclass(frozen=True) +class MspBillingReport: + headers: Tuple[str, ...] + rows: Tuple[Tuple[Any, ...], ...] + title: str + + +@dataclass(frozen=True) +class MspLegacyReport: + headers: Tuple[str, ...] + rows: Tuple[Tuple[Any, ...], ...] + title: Optional[str] = None + + def login_to_managed_company(loader: enterprise_types.IEnterpriseLoader, mc_enterprise_id: int) -> Tuple[keeper_auth.KeeperAuth, bytes]: auth = loader.keeper_auth tree_key = loader.enterprise_data.enterprise_info.tree_key @@ -85,6 +119,11 @@ def msp_down(loader: enterprise_types.IEnterpriseLoader, *, reset: bool = False) return loader.load(reset=reset) +def switch_to_msp(loader: enterprise_types.IEnterpriseLoader) -> Set[int]: + """Refresh MSP enterprise data when switching back from MC context.""" + return msp_down(loader, reset=False) + + def decrypt_managed_company_tree_key(encrypted_tree_key_b64: str, msp_tree_key: bytes) -> Optional[bytes]: """Decrypt a managed company tree key blob using the MSP enterprise tree key (AES-GCM v2).""" if not encrypted_tree_key_b64: @@ -426,6 +465,348 @@ def msp_info( return _msp_info_managed_companies_report(enterprise_data, mcs, verbose=verbose) +def _billing_is_plan_id(msp_id: int) -> bool: + return 0 < msp_id < 100 + + +def _billing_is_storage_plan_id(msp_id: int) -> bool: + return 100 < msp_id < 10000 + + +def _billing_is_addon_id(msp_id: int) -> bool: + return msp_id > 10000 + + +def _billing_count_id(msp_id: int) -> int: + if _billing_is_plan_id(msp_id): + return msp_id + if _billing_is_storage_plan_id(msp_id): + return msp_id // 100 + if _billing_is_addon_id(msp_id): + return msp_id // 10000 + return 0 + + +def _merge_billing_units(unit_dicts: List[Optional[Dict[int, Any]]]) -> Dict[int, Tuple[int, int]]: + merged: Dict[int, Tuple[int, int]] = {} + for units in unit_dicts: + if not isinstance(units, dict): + continue + for unit, count in units.items(): + if not isinstance(unit, int): + continue + if isinstance(count, int): + qty, days = count, 1 + elif isinstance(count, tuple) and len(count) >= 2: + qty, days = int(count[0]), int(count[1]) + else: + continue + q0, d0 = merged.get(unit, (0, 0)) + merged[unit] = (q0 + qty, d0 + days) + return merged + + +def _fetch_daily_billing_snapshots( + auth: keeper_auth.KeeperAuth, + *, + year: int, + month: int, +) -> Tuple[Dict[Tuple[int, int], Dict[int, int]], Dict[int, str]]: + url = _bi_enterprise_console_url(auth, 'reporting/daily_snapshot') + rq = BI_pb2.ReportingDailySnapshotRequest() + rq.year = year + rq.month = month + rs = auth.execute_auth_rest(url, rq, response_type=BI_pb2.ReportingDailySnapshotResponse) + if not rs: + raise errors.KeeperError('No response received from daily snapshot API') + + company_lookup: Dict[int, str] = {x.id: x.name for x in rs.mcEnterprises} + snapshots: Dict[Tuple[int, int], Dict[int, int]] = {} + for record in rs.records: + units: Dict[int, int] = {} + if record.maxLicenseCount > 0: + if record.maxBasePlanId > 0: + units[record.maxBasePlanId] = record.maxLicenseCount + if record.maxFilePlanTypeId > 0: + units[record.maxFilePlanTypeId * 100] = record.maxLicenseCount + for addon in record.addons: + if addon.maxAddonId > 0: + units[addon.maxAddonId * 10000] = addon.units + ds = datetime.datetime.fromtimestamp(record.date / 1000.0, tz=datetime.timezone.utc) + snapshots[(record.mcEnterpriseId, ds.date().toordinal())] = units + return snapshots, company_lookup + + +def _billing_bounding_snapshots( + period_snapshots: Dict[Tuple[int, int], Dict[int, int]], + mc_id: Optional[int] = None, +) -> Tuple[Optional[Dict[int, Any]], Optional[Dict[int, Any]]]: + by_mc_dates: Dict[int, List[int]] = {} + for this_mc, date_no in period_snapshots.keys(): + if mc_id is not None and this_mc != mc_id: + continue + by_mc_dates.setdefault(this_mc, []).append(date_no) + if not by_mc_dates: + return None, None + + if mc_id is not None: + dates = by_mc_dates.get(mc_id) or [] + if not dates: + return None, None + start_key = (mc_id, min(dates)) + end_key = (mc_id, max(dates)) + return period_snapshots.get(start_key), period_snapshots.get(end_key) + + start_list: List[Dict[int, int]] = [] + end_list: List[Dict[int, int]] = [] + for company_id, dates in by_mc_dates.items(): + start_data = period_snapshots.get((company_id, min(dates))) + end_data = period_snapshots.get((company_id, max(dates))) + if start_data: + start_list.append(start_data) + if end_data: + end_list.append(end_data) + return _merge_billing_units(start_list), _merge_billing_units(end_list) + + +def _billing_reported_days(period_snapshots: Dict[Tuple[int, int], Dict[int, int]]) -> int: + dates = [x[1] for x in period_snapshots.keys()] + return max(dates) - min(dates) + 1 if dates else 30 + + +def _billing_max_product_count( + period_snapshots: Dict[Tuple[int, int], Dict[int, int]], + product: int, + mc_id: Optional[int] = None, +) -> int: + daily_totals: Dict[int, int] = {} + for (company_id, date_no), counts in period_snapshots.items(): + if mc_id is not None and company_id != mc_id: + continue + daily_totals[date_no] = daily_totals.get(date_no, 0) + int(counts.get(product) or 0) + return max(daily_totals.values()) if daily_totals else 0 + + +def msp_billing_report( + loader: enterprise_types.IEnterpriseLoader, + *, + month: Optional[str] = None, + show_date: bool = False, + show_company: bool = False, +) -> MspBillingReport: + auth = loader.keeper_auth + + if month: + year_part, sep, month_part = month.partition('-') + if sep != '-': + raise errors.KeeperError(f'Given month "{month}" is not valid. Expected YYYY-MM') + try: + year = int(year_part) + month_no = int(month_part) + except Exception: + raise errors.KeeperError(f'Given month "{month}" is not valid. Expected YYYY-MM') from None + else: + now = datetime.datetime.now() + year = now.year + month_no = now.month - 1 + if month_no < 1: + month_no = 12 + year -= 1 + if month_no < 1 or month_no > 12: + raise errors.KeeperError(f'Given month "{month}" is not valid. Expected YYYY-MM') + + daily_counts, company_lookup = _fetch_daily_billing_snapshots(auth, year=year, month=month_no) + merged_counts: Dict[Tuple[int, int], Dict[int, Tuple[int, int]]] = {} + for (mc_id, date_no), units in daily_counts.items(): + key = (mc_id if show_company else 0, date_no if show_date else 0) + merged_counts[key] = _merge_billing_units([merged_counts.get(key), units]) + + headers: List[str] = [] + if show_date: + headers.append('date') + if show_company: + headers.extend(['company', 'company_id']) + headers.extend(['product', 'licenses', 'rate']) + if not show_date: + headers.extend(['avg_per_day', 'initial_licenses', 'final_licenses', 'max_licenses']) + + plan_lookup = {x[0]: x for x in enterprise_constants.MSP_PLANS} + storage_lookup = {x[0]: x for x in enterprise_constants.MSP_FILE_PLANS} + addon_lookup: Dict[int, Tuple[Any, ...]] = {} + addons = {x[0]: x for x in enterprise_constants.MSP_ADDONS} + for addon_id, addon_name in _fetch_msp_addon_id_to_name(auth).items(): + if addon_name in addons: + addon_lookup[addon_id] = addons[addon_name] + pricing = _fetch_mc_pricing(auth) + + rows: List[Tuple[Any, ...]] = [] + for (mc_id, date_no), counts in sorted(merged_counts.items(), key=lambda x: (x[0][1], x[0][0])): + day_str = str(datetime.date.fromordinal(date_no)) if show_date else '' + company_name = company_lookup.get(mc_id, '') if show_company else '' + start_snapshot, end_snapshot = (None, None) if show_date else _billing_bounding_snapshots( + daily_counts, mc_id=mc_id if show_company else None) + for product in sorted(counts.keys()): + count_id = _billing_count_id(product) + if show_company: + count, days = counts[product] + else: + count = counts[product][0] + days = _billing_reported_days(daily_counts) + + product_name = str(product) + rate_text = '' + if _billing_is_plan_id(product): + plan = plan_lookup.get(count_id) + if plan: + product_name = plan[2] + rate = pricing.get('mc_base_plans', {}).get(plan[1]) + if rate: + rate_text = _price_text_short(rate) + elif _billing_is_storage_plan_id(product): + storage_plan = storage_lookup.get(count_id) + if storage_plan: + product_name = storage_plan[2] + rate = pricing.get('mc_file_plans', {}).get(storage_plan[1]) + if rate: + rate_text = _price_text_short(rate) + elif _billing_is_addon_id(product): + addon = addon_lookup.get(count_id) + if addon: + product_name = addon[1] + rate = pricing.get('mc_addons', {}).get(addon[0]) + if rate: + rate_text = _price_text_short(rate) + + row: List[Any] = [] + if show_date: + row.append(day_str) + if show_company: + row.extend([company_name, mc_id]) + row.extend([product_name, count, rate_text]) + + if not show_date: + avg_per_day = round(count / days, 2) if days else 0 + start_raw = 0 if start_snapshot is None else (start_snapshot.get(product) or 0) + end_raw = 0 if end_snapshot is None else (end_snapshot.get(product) or 0) + start_count = start_raw[0] if isinstance(start_raw, tuple) else start_raw + end_count = end_raw[0] if isinstance(end_raw, tuple) else end_raw + max_count = _billing_max_product_count( + daily_counts, product, mc_id if show_company else None) + row.extend([avg_per_day, start_count, end_count, max_count]) + rows.append(tuple(row)) + + title = f'Consumption Billing Statement: {calendar.month_name[month_no]} {year}' + return MspBillingReport(headers=tuple(headers), rows=tuple(rows), title=title) + + +def _legacy_date_range_to_dates( + range_name: Union[str, LegacyDateRange], +) -> Tuple[datetime.datetime, datetime.datetime]: + if isinstance(range_name, LegacyDateRange): + rng = range_name + else: + try: + rng = LegacyDateRange(range_name) + except ValueError: + supported = ', '.join(m.value for m in LegacyDateRange) + raise errors.KeeperError( + f'Given range {range_name} is not supported. Supported ranges: {supported}') from None + + current_time = datetime.datetime.now() + today_start_dt = current_time.replace(hour=0, minute=0, second=0, microsecond=0) + today_end_dt = current_time.replace(hour=23, minute=59, second=59, microsecond=0) + + def last_day_of_month(dt: datetime.datetime) -> datetime.datetime: + year = dt.year + month = int(dt.strftime('%m')) % 12 + 1 + ldom = calendar.monthrange(year, month)[1] + return dt.replace(hour=23, minute=59, second=59, microsecond=0, day=ldom) + + td = datetime.timedelta + last_month_num = current_time.month - 1 if current_time.month > 1 else 12 + last_month_dt = current_time.replace(month=last_month_num) + month_start_last = current_time.replace( + month=last_month_num, day=1, hour=0, minute=0, second=0, microsecond=0) + prev_year = today_start_dt.year - 1 + + handlers: Dict[LegacyDateRange, Callable[[], Tuple[datetime.datetime, datetime.datetime]]] = { + LegacyDateRange.TODAY: lambda: (today_start_dt, today_end_dt), + LegacyDateRange.YESTERDAY: lambda: ( + today_start_dt - td(days=1), today_end_dt - td(days=1)), + LegacyDateRange.LAST_7_DAYS: lambda: (today_start_dt - td(days=7), today_end_dt), + LegacyDateRange.LAST_30_DAYS: lambda: (today_start_dt - td(days=30), today_end_dt), + LegacyDateRange.MONTH_TO_DATE: lambda: (today_start_dt.replace(day=1), today_end_dt), + LegacyDateRange.LAST_MONTH: lambda: (month_start_last, last_day_of_month(last_month_dt)), + LegacyDateRange.YEAR_TO_DATE: lambda: ( + today_start_dt.replace(day=1, month=1), today_end_dt), + LegacyDateRange.LAST_YEAR: lambda: ( + today_start_dt.replace(year=prev_year, day=1, month=1), + today_start_dt.replace( + year=prev_year, day=31, month=12, hour=23, minute=59, second=59, microsecond=0)), + } + return handlers[rng]() + + +def _parse_legacy_date_str(value: str, *, is_end: bool) -> datetime.datetime: + v = (value or '').strip() + if not v: + raise errors.KeeperError('Date value is empty') + try: + return datetime.datetime.fromtimestamp(int(v)) + except Exception: + pass + suffix = '23:59:59' if is_end else '00:00:00' + try: + return datetime.datetime.strptime(f'{v} {suffix}', '%Y-%m-%d %H:%M:%S') + except Exception: + raise errors.KeeperError(f'Date "{value}" is invalid. Expected YYYY-MM-DD or unix timestamp') from None + + +def msp_legacy_report( + loader: enterprise_types.IEnterpriseLoader, + *, + range_name: Union[str, LegacyDateRange] = LegacyDateRange.LAST_30_DAYS, + from_date: Optional[str] = None, + to_date: Optional[str] = None, +) -> MspLegacyReport: + """Generate the legacy MSP license adjustment report.""" + auth = loader.keeper_auth + + if from_date and to_date: + from_dt = _parse_legacy_date_str(from_date, is_end=False) + to_dt = _parse_legacy_date_str(to_date, is_end=True) + else: + from_dt, to_dt = _legacy_date_range_to_dates(range_name) + + rq = { + 'command': _CMD_GET_MC_LICENSE_ADJUSTMENT_LOG, + 'from': int(from_dt.timestamp() * 1000), + 'to': int(to_dt.timestamp() * 1000), + } + rs = auth.execute_auth_command(rq) + + headers = ( + 'id', 'time', 'company_id', 'company_name', 'status', + 'number_of_allocations', 'plan', 'transaction_notes', 'price_estimate', + ) + rows: List[Tuple[Any, ...]] = [] + for log in rs.get('log', []) if isinstance(rs, dict) else []: + if not isinstance(log, dict): + continue + rows.append(( + log.get('id'), + log.get('date'), + log.get('enterprise_id'), + log.get('enterprise_name'), + log.get('status'), + log.get('new_number_of_seats'), + log.get('new_product_type'), + log.get('note'), + log.get('price'), + )) + return MspLegacyReport(headers=headers, rows=tuple(rows), title=None) + + def _new_mc_encrypted_registration_fields(mc_tree_key: bytes, msp_tree_key: bytes) -> Dict[str, Any]: role_json = json.dumps({_JSON_KEY_DISPLAYNAME: _DISPLAYNAME_KEEPER_ADMIN}).encode() root_json = json.dumps({_JSON_KEY_DISPLAYNAME: _DISPLAYNAME_ROOT}).encode() @@ -1177,3 +1558,215 @@ def msp_convert_node( auth.execute_auth_rest(_REST_NODE_TO_MANAGED_COMPANY, mc_rq, response_type=None) msp_down(loader, reset=False) return mc_id + + +def _find_roles_by_name_or_id( + enterprise_data: enterprise_types.IEnterpriseData, + name_or_id: str, +) -> List[enterprise_types.Role]: + token = (name_or_id or '').strip() + if token.isdigit(): + role = enterprise_data.roles.get_entity(int(token)) + return [role] if role is not None else [] + role_name = token.casefold() + return [x for x in enterprise_data.roles.get_all_entities() if x.name.casefold() == role_name] + + +def _to_enforcement_map_for_roles( + role_enforcements: List[enterprise_types.RoleEnforcement], +) -> Dict[int, Dict[str, str]]: + result: Dict[int, Dict[str, str]] = {} + for enf in role_enforcements: + if enf.role_id not in result: + result[enf.role_id] = {} + result[enf.role_id][enf.enforcement_type.lower()] = enf.value + return result + + +def _extract_mc_enterprise_payload(rs: Dict[str, Any]) -> Dict[str, Any]: + enterprise = rs.get('enterprise') + if isinstance(enterprise, dict): + return enterprise + return rs + + +def _mc_payload_root_node_id(enterprise_payload: Dict[str, Any]) -> Optional[int]: + for node in enterprise_payload.get('nodes', []) or []: + if not node.get('parent_id'): + node_id = node.get('node_id') + if isinstance(node_id, int): + return node_id + return None + + +def _mc_payload_role_name(role_payload: Dict[str, Any]) -> str: + data = role_payload.get('data') + if isinstance(data, dict): + display_name = data.get(_JSON_KEY_DISPLAYNAME) + if isinstance(display_name, str): + return display_name + return '' + + +def _allocate_enterprise_id(auth: keeper_auth.KeeperAuth) -> int: + rs = auth.execute_auth_command({'command': _CMD_ENTERPRISE_ALLOCATE_IDS, 'number_requested': 1}) + base_id = int(rs.get('base_id', 0)) + allocated = int(rs.get('number_allocated', 0)) + if allocated < 1: + raise errors.KeeperError('Unable to allocate enterprise id') + return base_id + 1 + + +def _coerce_msp_copy_enforcement_value(name: str, value: Any) -> Any: + enforcement_type = enterprise_constants.ENFORCEMENTS.get(name.lower()) + if not enforcement_type: + return value + if enforcement_type == 'long': + try: + return int(value) + except Exception as err: + raise errors.KeeperError(f'Enforcement {name}: invalid integer value: {value}') from err + if enforcement_type == 'boolean': + if isinstance(value, bool): + return value + return str(value).lower() == 'true' + if enforcement_type == 'account_share': + return None + if enforcement_type in {'record_types', 'json', 'jsonarray'}: + if isinstance(value, (dict, list)): + return value + return json.loads(str(value)) + return value + + +def msp_copy_role( + loader: enterprise_types.IEnterpriseLoader, + *, + roles: List[str], + managed_companies: List[str], +) -> Set[int]: + """Copy role enforcements from MSP to one or more managed companies. + + Roles are matched by id or case-insensitive exact name in the MSP enterprise. For each target MC, + role names are matched case-insensitively; missing roles are created. Enforcements are then synchronized: + add/update to match source and remove extras from destination role. + """ + enterprise_data = loader.enterprise_data + msp_tree_key = enterprise_data.enterprise_info.tree_key + logger = utils.get_logger() + + role_inputs = [str(x).strip() for x in (roles or []) if str(x).strip()] + if len(role_inputs) == 0: + raise errors.KeeperError('Source role parameter is required') + mc_inputs = [str(x).strip() for x in (managed_companies or []) if str(x).strip()] + if len(mc_inputs) == 0: + raise errors.KeeperError('Managed company parameter is required') + + source_roles: Dict[int, enterprise_types.Role] = {} + for role_token in role_inputs: + matched_roles = _find_roles_by_name_or_id(enterprise_data, role_token) + if len(matched_roles) == 1: + source_roles[matched_roles[0].role_id] = matched_roles[0] + elif len(matched_roles) > 1: + raise errors.KeeperError(f'There are more than one roles with name "{role_token}". Use Role ID') + else: + raise errors.KeeperError(f'Role "{role_token}" not found') + + src_role_enforcements = _to_enforcement_map_for_roles(list(enterprise_data.role_enforcements.get_all_links())) + unique_mcs: Dict[int, enterprise_types.ManagedCompany] = {} + for mc_input in mc_inputs: + mc_filter = _parse_managed_company_filter(mc_input) + if mc_filter is None: + continue + mc = _find_managed_company(enterprise_data, mc_filter) + if mc is None: + raise errors.KeeperError(f'Managed Company "{mc_input}" not found') + unique_mcs[mc.mc_enterprise_id] = mc + + synced_mc_ids: Set[int] = set() + for mc in unique_mcs.values(): + mc_id = mc.mc_enterprise_id + mc_auth, mc_tree_key = login_to_managed_company(loader, mc_id) + mc_rs = mc_auth.execute_auth_command({'command': _CMD_QUERY_ENTERPRISE}) + if not isinstance(mc_rs, dict): + raise errors.KeeperError(f'MC {mc_id}: query_enterprise response is invalid') + mc_payload = _extract_mc_enterprise_payload(mc_rs) + root_node_id = _mc_payload_root_node_id(mc_payload) + if not isinstance(root_node_id, int): + raise errors.KeeperError(f'MC {mc_id}: root node is not found') + + dst_roles = list(mc_payload.get('roles') or []) + dst_role_enforcements: Dict[int, Dict[str, Any]] = {} + for item in mc_payload.get('role_enforcements') or []: + role_id = item.get('role_id') + enforcements = item.get('enforcements') + if isinstance(role_id, int) and isinstance(enforcements, dict): + dst_role_enforcements[role_id] = dict(enforcements) + + mc_rqs: List[Dict[str, Any]] = [] + for src_role in source_roles.values(): + src_role_id = src_role.role_id + role_name = src_role.name or '' + if not role_name: + continue + + matches = [r for r in dst_roles if _mc_payload_role_name(r).casefold() == role_name.casefold()] + if len(matches) > 1: + logger.warning('MC #%s: There are more than one roles with name "%s". Skipping', mc_id, role_name) + continue + if len(matches) == 1: + dst_role_id = int(matches[0].get('role_id') or 0) + if dst_role_id <= 0: + logger.warning('MC #%s: Role "%s" has invalid role id. Skipping', mc_id, role_name) + continue + else: + dst_role_id = _allocate_enterprise_id(mc_auth) + role_data = json.dumps({_JSON_KEY_DISPLAYNAME: role_name}).encode('utf-8') + mc_rqs.append({ + 'command': _CMD_ROLE_ADD, + 'role_id': dst_role_id, + 'node_id': root_node_id, + 'encrypted_data': utils.base64_url_encode(crypto.encrypt_aes_v1(role_data, mc_tree_key)), + 'visible_below': src_role.visible_below, + 'new_user_inherit': src_role.new_user_inherit, + }) + dst_roles.append({'role_id': dst_role_id, 'data': {_JSON_KEY_DISPLAYNAME: role_name}}) + + src_enforcements = dict(src_role_enforcements.get(src_role_id) or {}) + stale_dst_enforcements = dict(dst_role_enforcements.get(dst_role_id) or {}) + for enforcement_name, src_value in src_enforcements.items(): + if enforcement_name in stale_dst_enforcements: + dst_value = stale_dst_enforcements.pop(enforcement_name) + if src_value == dst_value: + continue + command = _CMD_ROLE_ENFORCEMENT_UPDATE + else: + command = _CMD_ROLE_ENFORCEMENT_ADD + try: + value = _coerce_msp_copy_enforcement_value(enforcement_name, src_value) + if value is None: + continue + rq: Dict[str, Any] = { + 'command': command, + 'role_id': dst_role_id, + 'enforcement': enforcement_name, + } + if not isinstance(value, bool): + rq['value'] = value + mc_rqs.append(rq) + except Exception as err: + logger.warning('Role %s: Enforcement %s: %s', role_name, enforcement_name, err) + + for enforcement_name in stale_dst_enforcements.keys(): + mc_rqs.append({ + 'command': _CMD_ROLE_ENFORCEMENT_REMOVE, + 'role_id': dst_role_id, + 'enforcement': enforcement_name, + }) + + if mc_rqs: + mc_auth.execute_batch(mc_rqs) + logger.info('MC %s: Roles are in sync', mc_id) + synced_mc_ids.add(mc_id) + + return synced_mc_ids From 784a53606962506542fa9a566930322fe4e0b075 Mon Sep 17 00:00:00 2001 From: mtyagi-ks Date: Tue, 26 May 2026 13:09:34 +0530 Subject: [PATCH 15/23] PAM commands bug fixes --- .../src/keepercli/commands/pam/pam_config.py | 135 ++++++---- .../keepercli/commands/pam/pam_connection.py | 9 +- .../src/keepercli/commands/pam/pam_rbi.py | 152 ++++++++---- .../keepercli/commands/pam/pam_rotation.py | 23 +- .../src/keepercli/commands/record_edit.py | 8 +- .../src/keepercli/helpers/record_utils.py | 36 ++- .../src/keepercli/helpers/router_utils.py | 21 +- .../keepersdk/helpers/keeper_dag/constants.py | 46 ++-- .../keepersdk/helpers/tunnel/tunnel_graph.py | 52 +++- .../src/keepersdk/vault/record_management.py | 3 +- .../keepersdk/vault/share_management_utils.py | 233 ++++++++++++++++-- 11 files changed, 545 insertions(+), 173 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/pam/pam_config.py b/keepercli-package/src/keepercli/commands/pam/pam_config.py index 066dc892..d7faf391 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_config.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_config.py @@ -14,19 +14,17 @@ from keepersdk.helpers import config_utils from keepersdk.vault import vault_online, vault_utils, vault_record, record_management from keepersdk.helpers.pam_config_facade import PamConfigurationRecordFacade -from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG, tunnel_utils +from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG, TriStateSetting, tunnel_utils from keepersdk.helpers.keeper_dag import dag_utils +from keepersdk.helpers.keeper_dag.constants import PamConfigurationRecordType, PAM_CONFIGURATIONS from .. import record_edit logger = api.get_logger() -# PAM Configuration record types -PAM_CONFIG_RECORD_TYPES = ( - 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', - 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration' -) +# PAM Configuration record types (vault record type name strings) +PAM_CONFIG_RECORD_TYPES = PAM_CONFIGURATIONS class PAMConfigListCommand(base.ArgparseCommand): @@ -72,8 +70,9 @@ def _validate_vault_and_permissions(self, context: KeeperParams): def _print_tunneling_config(self, vault: vault_online.VaultOnline, config_uid: str): """Prints tunneling configuration for a specific PAM configuration.""" - encrypted_session_token, encrypted_transmission_key, _ = tunnel_utils.get_keeper_tokens(vault) - tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid, is_config=True) + encrypted_session_token, encrypted_transmission_key, transmission_key = tunnel_utils.get_keeper_tokens(vault) + tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid, + is_config=True, transmission_key=transmission_key) tmp_dag.print_tunneling_config(config_uid, None) def _list_single_configuration(self, vault: vault_online.VaultOnline, config_uid: str, @@ -125,12 +124,13 @@ def _list_all_configurations(self, vault: vault_online.VaultOnline, is_verbose: def _load_and_validate_configuration(self, vault: vault_online.VaultOnline, config_uid: str, format_type: str): """Loads and validates a PAM configuration record.""" - configuration = vault.vault_data.load_record(config_uid) - if not configuration: + info = vault.vault_data.get_record(config_uid) + if not info or info.version != 6 or info.record_type not in PAM_CONFIG_RECORD_TYPES: return self._handle_error(format_type, f'Configuration {config_uid} not found') - if configuration.version != 6 or not isinstance(configuration, vault_record.TypedRecord): - return self._handle_error(format_type, f'{config_uid} is not PAM Configuration') + configuration = vault.vault_data.load_record(config_uid) + if not configuration or not isinstance(configuration, vault_record.TypedRecord): + return self._handle_error(format_type, f'Configuration {config_uid} not found') return configuration @@ -174,6 +174,23 @@ def _build_list_headers(self, is_verbose: bool, format_type: str): headers.append('Fields') return headers + @staticmethod + def _field_values_for_display(field): + """Normalize TypedField.get_external_value() to display strings.""" + raw = field.get_external_value() + if raw is None: + raw = field.value if isinstance(field.value, list) else [] + items = raw if isinstance(raw, list) else [raw] + values = [] + for item in items: + if item is None or item == '': + continue + if isinstance(item, (dict, list)): + values.append(json.dumps(item)) + else: + values.append(str(item)) + return values + def _extract_config_fields(self, record, is_verbose: bool): """Extracts field data from a configuration record.""" fields_data = {} if is_verbose else [] @@ -182,7 +199,7 @@ def _extract_config_fields(self, record, is_verbose: bool): if field.type in ('pamResources', 'fileRef'): continue - values = list(field.get_external_value()) + values = self._field_values_for_display(field) if not values: continue @@ -190,7 +207,7 @@ def _extract_config_fields(self, record, is_verbose: bool): if field.type == 'schedule': field_name = 'Default Schedule' - value_str = ', '.join(field.get_external_value()) + value_str = ', '.join(values) if is_verbose: fields_data[field_name] = value_str else: @@ -262,7 +279,7 @@ def _format_single_config_json(self, configuration, facade, shared_folder): if field.type in ('pamResources', 'fileRef'): continue - values = list(field.get_external_value()) + values = self._field_values_for_display(field) if not values: continue @@ -290,7 +307,7 @@ def _format_single_config_table(self, configuration, facade, shared_folder): if field.type in ('pamResources', 'fileRef'): continue - values = list(field.get_external_value()) + values = self._field_values_for_display(field) if not values: continue @@ -298,7 +315,7 @@ def _format_single_config_table(self, configuration, facade, shared_folder): if field.type == 'schedule': field_name = 'Default Schedule' - table.append([field_name, values]) + table.append([field_name, ', '.join(values)]) report_utils.dump_report_data(table, header, no_header=True, right_align=(0,)) @@ -467,17 +484,17 @@ def _parse_type_specific_properties(self, vault: vault_online.VaultOnline, recor """Parses properties specific to each configuration type.""" record_type = record.record_type - if record_type == 'pamNetworkConfiguration': + if record_type == PamConfigurationRecordType.NETWORK: self._parse_network_properties(extra_properties, kwargs) - elif record_type == 'pamAwsConfiguration': + elif record_type == PamConfigurationRecordType.AWS: self._parse_aws_properties(extra_properties, kwargs) - elif record_type == 'pamGcpConfiguration': + elif record_type == PamConfigurationRecordType.GCP: self._parse_gcp_properties(extra_properties, kwargs) - elif record_type == 'pamAzureConfiguration': + elif record_type == PamConfigurationRecordType.AZURE: self._parse_azure_properties(extra_properties, kwargs) - elif record_type == 'pamDomainConfiguration': + elif record_type == PamConfigurationRecordType.DOMAIN: self._parse_domain_properties(vault, record, extra_properties, kwargs) - elif record_type == 'pamOciConfiguration': + elif record_type == PamConfigurationRecordType.OCI: self._parse_oci_properties(extra_properties, kwargs) def _parse_network_properties(self, extra_properties: list, kwargs: dict): @@ -658,16 +675,37 @@ def verify_required(self, record: vault_record.TypedRecord): if custom.required: custom.required = False + def _configure_tunneling(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, + admin_cred_ref: str, kwargs: dict): + """Configures tunneling settings for the configuration.""" + encrypted_session_token, encrypted_transmission_key, transmission_key = tunnel_utils.get_keeper_tokens(vault) + tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, + record_uid=record.record_uid, is_config=True, transmission_key=transmission_key) + + tmp_dag.edit_tunneling_config( + kwargs.get('connections'), + kwargs.get('tunneling'), + kwargs.get('rotation'), + kwargs.get('recording'), + kwargs.get('typescriptrecording'), + kwargs.get('remotebrowserisolation') + ) + + if admin_cred_ref: + tmp_dag.link_user_to_config_with_options(admin_cred_ref, is_admin=TriStateSetting.ON) + + tmp_dag.print_tunneling_config(record.record_uid, None) + # Configuration type mapping CONFIG_TYPE_TO_RECORD_TYPE = { - 'aws': 'pamAwsConfiguration', - 'azure': 'pamAzureConfiguration', - 'local': 'pamNetworkConfiguration', - 'network': 'pamNetworkConfiguration', - 'gcp': 'pamGcpConfiguration', - 'domain': 'pamDomainConfiguration', - 'oci': 'pamOciConfiguration' + 'aws': PamConfigurationRecordType.AWS, + 'azure': PamConfigurationRecordType.AZURE, + 'local': PamConfigurationRecordType.NETWORK, + 'network': PamConfigurationRecordType.NETWORK, + 'gcp': PamConfigurationRecordType.GCP, + 'domain': PamConfigurationRecordType.DOMAIN, + 'oci': PamConfigurationRecordType.OCI, } common_parser = argparse.ArgumentParser(add_help=False) @@ -845,7 +883,7 @@ def _extract_pam_resources(self, record: vault_record.TypedRecord, kwargs: dict) shared_folder_uid = value.get('folderUid') admin_cred_ref = None - if record.record_type == 'pamDomainConfiguration' and not kwargs.get('force_domain_admin', False): + if record.record_type == PamConfigurationRecordType.DOMAIN and not kwargs.get('force_domain_admin', False): admin_cred_ref = value.get('adminCredentialRef') return gateway_uid, shared_folder_uid, admin_cred_ref @@ -875,27 +913,6 @@ def _create_and_configure_record(self, vault: vault_online.VaultOnline, record: if gateway_uid: self._set_configuration_controller(vault, record.record_uid, gateway_uid) - def _configure_tunneling(self, vault: vault_online.VaultOnline, record: vault_record.TypedRecord, - admin_cred_ref: str, kwargs: dict): - """Configures tunneling settings for the configuration.""" - encrypted_session_token, encrypted_transmission_key, _ = tunnel_utils.get_keeper_tokens(vault) - tmp_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, - record_uid=record.record_uid, is_config=True) - - tmp_dag.edit_tunneling_config( - kwargs.get('connections'), - kwargs.get('tunneling'), - kwargs.get('rotation'), - kwargs.get('recording'), - kwargs.get('typescriptrecording'), - kwargs.get('remotebrowserisolation') - ) - - if admin_cred_ref: - tmp_dag.link_user_to_config_with_options(admin_cred_ref, is_admin='on') - - tmp_dag.print_tunneling_config(record.record_uid, None) - def _set_configuration_controller(self, vault: vault_online.VaultOnline, config_uid: str, gateway_uid: str): """Sets the controller for the PAM configuration.""" pcc = pam_pb2.PAMConfigurationController() @@ -952,6 +969,20 @@ def execute(self, context: KeeperParams, **kwargs): record_management.update_record(vault, configuration) self._update_controller_and_folder_if_changed(vault, configuration, orig_gateway_uid, orig_shared_folder_uid) + + target_keys = { + 'connections', 'tunneling', 'rotation', 'recording', + 'typescriptrecording', 'remotebrowserisolation' + } + if all(kwargs.get(k) is None for k in target_keys): + admin_cred_ref = None + if configuration.record_type == PamConfigurationRecordType.DOMAIN and not kwargs.get('force_domain_admin'): + pam_field = configuration.get_typed_field('pamResources') + if pam_field: + value = pam_field.get_default_value(dict) + if isinstance(value, dict): + admin_cred_ref = value.get('adminCredentialRef') + self._configure_tunneling(vault, configuration, admin_cred_ref, kwargs) self._log_warnings() vault.sync_down() diff --git a/keepercli-package/src/keepercli/commands/pam/pam_connection.py b/keepercli-package/src/keepercli/commands/pam/pam_connection.py index 85e8eae0..5a3dc9d6 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_connection.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_connection.py @@ -98,7 +98,8 @@ def execute(self, context: KeeperParams, **kwargs): encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(vault) if record_type in "pamNetworkConfiguration pamAwsConfiguration pamAzureConfiguration".split(): - tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, record_uid, is_config=True) + tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, record_uid, + is_config=True, transmission_key=transmission_key) tdag.edit_tunneling_config(connections=_connections, session_recording=_recording, typescript_recording=_typescript_recording) if not kwargs.get("silent", False): tdag.print_tunneling_config(record_uid, None) else: @@ -193,8 +194,10 @@ def execute(self, context: KeeperParams, **kwargs): existing_config_uid = get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) - tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid) - old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid) + tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid, + transmission_key=transmission_key) + old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid, + transmission_key=transmission_key) if config_uid and existing_config_uid != config_uid: old_dag.remove_from_dag(record_uid) diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py index 10f6ff58..3388bb66 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py @@ -1,21 +1,93 @@ import os import argparse +from typing import Optional from keepersdk import utils +from keepersdk.errors import KeeperApiError from keepersdk.helpers.keeper_dag import dag_utils from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid -from keepersdk.vault import record_management, vault_record +from keepersdk.vault import record_management, vault_online, vault_record from .. import base from ... import api +from ...helpers import record_utils from ...params import KeeperParams - choices = ['on', 'off', 'default'] +_PAM_CONFIG_RECORD_TYPES = ( + 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', + 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration', +) + logger = api.get_logger() +def _bootstrap_rbi_record(record: vault_record.TypedRecord) -> bool: + """Ensure trafficEncryptionSeed and pamRemoteBrowserSettings exist on the RBI record.""" + dirty = False + traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') + if not traffic_encryption_key or not traffic_encryption_key.value: + seed = os.urandom(32) + base64_seed = utils.base64_url_encode(seed) + record_seed = vault_record.TypedField.create_field('trafficEncryptionSeed', base64_seed, required=False) + if traffic_encryption_key: + traffic_encryption_key.value = [base64_seed] + else: + record.fields.append(record_seed) + dirty = True + + rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') + if not rbs_fld: + rbsettings = {'connection': {'protocol': 'http', 'httpCredentialsUid': ''}} + pam_rbsettings = vault_record.TypedField.create_field('pamRemoteBrowserSettings', required=False) + pam_rbsettings.value = [rbsettings] + record.fields.append(pam_rbsettings) + dirty = True + elif not rbs_fld.value: + rbs_fld.value.append({'connection': {'protocol': 'http'}}) # type: ignore + dirty = True + return dirty + + +def _save_rbi_record(vault: vault_online.VaultOnline, record: vault_record.TypedRecord) -> None: + """Persist RBI record body changes with a fresh revision (sync + retry on out-of-sync).""" + vault.sync_down() + try: + record_management.update_record(vault, record) + except KeeperApiError as err: + if 'OUT_OF_SYNC' not in str(err): + raise + vault.sync_down() + try: + record_management.update_record(vault, record) + except KeeperApiError as err2: + raise base.CommandError( + f'Record "{record.record_uid}" is out of sync with the server. ' + 'Run sync-down --force and retry.' + ) from err2 + vault.sync_down() + + +def _resolve_pam_config_record( + context: KeeperParams, + config_ref: str, +) -> Optional[vault_record.TypedRecord]: + """Resolve a PAM configuration by UID or title (vault index version 6, not TypedRecord.version).""" + if not config_ref or context.vault is None: + return None + vault = context.vault + info = vault.vault_data.get_record(config_ref) + if not info: + info = record_utils.try_resolve_single_record(config_ref, context) + if not info or info.version != 6 or info.record_type not in _PAM_CONFIG_RECORD_TYPES: + return None + loaded = vault.vault_data.load_record(info.record_uid) + if isinstance(loaded, vault_record.TypedRecord): + return loaded + return None + + class PAMRbiEditCommand(base.ArgparseCommand): def __init__(self): @@ -123,7 +195,10 @@ def execute(self, context: KeeperParams, **kwargs): vault = context.vault - record = vault.vault_data.load_record(record_name) + record_info = record_utils.try_resolve_single_record(record_name, context) + if not record_info: + raise base.CommandError(f'Record \"{record_name}\" not found.') + record = vault.vault_data.load_record(record_info.record_uid) if not record: raise base.CommandError(f'Record \"{record_name}\" not found.') if not isinstance(record, vault_record.TypedRecord): @@ -136,28 +211,7 @@ def execute(self, context: KeeperParams, **kwargs): "cannot be set up for RBI connections. " f"RBI connection records must be of type: pamRemoteBrowser") - dirty = False - traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') - if not traffic_encryption_key or not traffic_encryption_key.value: - seed = os.urandom(32) - base64_seed = utils.base64_url_encode(seed) - record_seed = vault_record.TypedField.create_field('trafficEncryptionSeed', base64_seed, required=False) - if traffic_encryption_key: - traffic_encryption_key.value = [base64_seed] - else: - record.fields.append(record_seed) - dirty = True - - rbs_fld = record.get_typed_field('pamRemoteBrowserSettings') - if not rbs_fld: - rbsettings = {'connection': {'protocol': 'http', 'httpCredentialsUid': ''}} - pam_rbsettings = vault_record.TypedField.create_field('pamRemoteBrowserSettings', required=False) - pam_rbsettings.value = [rbsettings] - record.fields.append(pam_rbsettings) - dirty = True - elif not rbs_fld.value: - rbs_fld.value.append({'connection': {'protocol': 'http'}}) # type: ignore - dirty = True + dirty = _bootstrap_rbi_record(record) if autofill: af_rec = vault.vault_data.load_record(autofill) @@ -315,8 +369,7 @@ def update_connection_int(field_name, value): update_connection_int('audioSampleRate', audio_sample_rate) if dirty: - record_management.update_record(vault, record) - vault.sync_down() + _save_rbi_record(vault, record) traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') if not traffic_encryption_key: @@ -337,35 +390,42 @@ def update_connection_int(field_name, value): # config parameter is optional and may be (auto)resolved from RBI record cfg_rec = None if config_name: - cfg_rec = vault.vault_data.load_record(config_name) - msg = ("not found" if cfg_rec is None else "not the right type" - if not isinstance(cfg_rec, vault_record.TypedRecord) or cfg_rec.version != 6 else "") - if msg: - logger.warning(f'PAM Config record "{config_name}" {msg}') - cfg_rec = None + cfg_rec = _resolve_pam_config_record(context, config_name) + if not cfg_rec: + logger.warning( + f'PAM Config record "{config_name}" not found or not a valid PAM configuration type' + ) if not cfg_rec: logger.debug(f"PAM Config - using config from record {record_uid}") - cfg_rec = vault.vault_data.load_record(existing_config_uid) - msg = ("not found" if cfg_rec is None else "not the right type" - if not isinstance(cfg_rec, vault_record.TypedRecord) or cfg_rec.version != 6 else "") - if msg: - logger.warning(f'PAM Config record "{existing_config_uid}" {msg}') - cfg_rec = None + if existing_config_uid: + cfg_rec = _resolve_pam_config_record(context, existing_config_uid) + if not cfg_rec: + logger.warning( + f'PAM Config record "{existing_config_uid}" not found or not a valid PAM configuration type' + ) config_uid = cfg_rec.record_uid if cfg_rec else None if not config_uid: raise base.CommandError(f'PAM Config record not found.') - tdag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, config_uid) - if tdag is None or not tdag.linking_dag.has_graph: - raise base.CommandError(f"No valid PAM Configuration UID set. " - "This must be set or supplied for connections to work. " - "The ConfigUID can be found by running " - f"'pam config list'") + tdag = TunnelDAG( + vault, encrypted_session_token, encrypted_transmission_key, config_uid, + is_config=True, transmission_key=transmission_key, + ) + if not tdag.linking_dag.has_graph: + raise base.CommandError( + f'No PAM Configuration DAG found for {config_uid}. ' + 'Initialize tunnel settings on the config first, e.g.\n' + f' pam config edit {config_uid} --connections on ' + '--remote-browser-isolation on --connections-recording on' + ) if config_uid: if existing_config_uid and existing_config_uid != config_uid: - old_dag = TunnelDAG(vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid) + old_dag = TunnelDAG( + vault, encrypted_session_token, encrypted_transmission_key, existing_config_uid, + is_config=True, transmission_key=transmission_key, + ) old_dag.remove_from_dag(record_uid) logger.debug(f'Updated existing PAM Config UID from: {existing_config_uid} to: {config_uid}') tdag.link_resource_to_config(record_uid) diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py index cbae422c..f01916e4 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py @@ -33,6 +33,11 @@ def __init__(self): help='Verbose output') super().__init__(parser) + def _validate_vault_and_permissions(self, context: KeeperParams): + if not context.vault: + raise ValueError("Vault is not initialized, login to initialize the vault.") + base.require_enterprise_admin(context) + def execute(self, context: KeeperParams, **kwargs): self._validate_vault_and_permissions(context) vault = context.vault @@ -48,8 +53,11 @@ def execute(self, context: KeeperParams, **kwargs): enterprise_all_controllers = list(gateway_utils.get_all_gateways(vault)) enterprise_controllers_connected_resp = router_utils.router_get_connected_gateways(vault) - enterprise_controllers_connected_uids_bytes = \ - [x.controllerUid for x in enterprise_controllers_connected_resp.controllers] + if enterprise_controllers_connected_resp: + enterprise_controllers_connected_uids_bytes = [ + x.controllerUid for x in enterprise_controllers_connected_resp.controllers] + else: + enterprise_controllers_connected_uids_bytes = [] all_pam_config_records = record_utils.pam_configurations_get_all(vault) table = [] @@ -77,8 +85,10 @@ def execute(self, context: KeeperParams, **kwargs): (ctr for ctr in enterprise_all_controllers if ctr.controllerUid == controller_uid), None) configuration_uid = s.configurationUid configuration_uid_str = utils.base64_url_encode(configuration_uid) - pam_configuration = next((pam_config for pam_config in all_pam_config_records if - pam_config.get('record_uid') == configuration_uid_str), None) + pam_configuration = next( + (pam_config for pam_config in all_pam_config_records + if pam_config.record_uid == configuration_uid_str), + None) is_controller_online = any( (poc for poc in enterprise_controllers_connected_uids_bytes if poc == controller_uid)) @@ -135,10 +145,7 @@ def execute(self, context: KeeperParams, **kwargs): f"[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified]") else: - pam_data_decrypted = record_utils.pam_decrypt_configuration_data(pam_configuration) - pam_config_name = pam_data_decrypted.get('title') - pam_config_type = pam_data_decrypted.get('type') - row.append(f"{pam_config_name} ({pam_config_type})") + row.append(f"{pam_configuration.title} ({pam_configuration.record_type})") if is_verbose: row.append(f'{utils.base64_url_encode(configuration_uid)}') diff --git a/keepercli-package/src/keepercli/commands/record_edit.py b/keepercli-package/src/keepercli/commands/record_edit.py index ee75d8bf..156eebaa 100644 --- a/keepercli-package/src/keepercli/commands/record_edit.py +++ b/keepercli-package/src/keepercli/commands/record_edit.py @@ -1166,11 +1166,17 @@ def _find_target_object(self, context: KeeperParams, uid_or_title: str): def _find_record(self, vault: vault_online.VaultOnline, uid_or_title: str): """Find a record by UID or title.""" - return next( + record = next( (r for r in vault.vault_data.records() if r.record_uid == uid_or_title or r.title == uid_or_title), None ) + if record: + return record + if record_utils.is_record_uid(uid_or_title): + share_management_utils.try_load_record_on_demand(vault, uid_or_title) + return vault.vault_data.get_record(uid_or_title) + return None def _find_shared_folder(self, vault: vault_online.VaultOnline, uid_or_title: str): """Find a shared folder by UID or name.""" diff --git a/keepercli-package/src/keepercli/helpers/record_utils.py b/keepercli-package/src/keepercli/helpers/record_utils.py index 02b8ba72..3fd6b3e5 100644 --- a/keepercli-package/src/keepercli/helpers/record_utils.py +++ b/keepercli-package/src/keepercli/helpers/record_utils.py @@ -9,10 +9,11 @@ from urllib import parse from keepersdk import crypto, utils +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS from keepersdk.proto.enterprise_pb2 import GetSharingAdminsRequest, GetSharingAdminsResponse from keepersdk.proto.router_pb2 import RouterRotationInfo from keepersdk.proto.pam_pb2 import PAMGenericUidRequest -from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils +from keepersdk.vault import vault_online, vault_record, vault_types, vault_utils, share_management_utils from ..commands.base import CommandError from ..params import KeeperParams @@ -24,6 +25,21 @@ GET_SHARE_ADMINS = 'enterprise/get_sharing_admins' +def is_record_uid(value: str) -> bool: + if not value: + return False + try: + return len(utils.base64_url_decode(value)) == 16 + except Exception: + return False + + +def try_load_record_on_demand(vault: Optional[vault_online.VaultOnline], record_uid: str) -> bool: + if vault is None: + return False + return share_management_utils.try_load_record_on_demand(vault, record_uid) + + def try_resolve_single_record(record_name: Optional[str], context: KeeperParams) -> Optional[vault_record.KeeperRecordInfo]: if context.vault is None: raise CommandError('Vault is not initialized. Login to initialize the vault.') @@ -34,6 +50,12 @@ def try_resolve_single_record(record_name: Optional[str], context: KeeperParams) if record_info: return record_info + if is_record_uid(record_name): + try_load_record_on_demand(context.vault, record_name) + record_info = context.vault.vault_data.get_record(record_name) + if record_info: + return record_info + folder, name = folder_utils.try_resolve_path(context, record_name) if name: name = name.casefold() @@ -159,11 +181,15 @@ def record_rotation_get(vault: vault_online.VaultOnline, record_uid_bytes: bytes return rotation_info_rs -def pam_configurations_get_all(vault: vault_online.VaultOnline): - all_records = vault.vault_data.records() - all_v6_records = [rec for rec in list(all_records) if rec.version == 6] +PAM_CONFIGURATION_RECORD_TYPES = PAM_CONFIGURATIONS - return all_v6_records + +def pam_configurations_get_all(vault: vault_online.VaultOnline): + return list(vault.vault_data.find_records( + criteria=None, + record_type=PAM_CONFIGURATION_RECORD_TYPES, + record_version=6, + )) def pam_decrypt_configuration_data(pam_config_v6_record): diff --git a/keepercli-package/src/keepercli/helpers/router_utils.py b/keepercli-package/src/keepercli/helpers/router_utils.py index 8dc4df3c..e8d0f2d9 100644 --- a/keepercli-package/src/keepercli/helpers/router_utils.py +++ b/keepercli-package/src/keepercli/helpers/router_utils.py @@ -20,6 +20,13 @@ VERIFY_SSL = bool(os.environ.get("VERIFY_SSL", "TRUE") == "TRUE") +def _router_base_url(keeper_endpoint) -> str: + """Full router base URL (scheme + host), matching keepersdk execute_router_rest.""" + if 'ROUTER_URL' in os.environ: + return os.environ['ROUTER_URL'].rstrip('/') + return f'https://{keeper_endpoint.get_router_server()}' + + def router_get_connected_gateways(vault: vault_online.VaultOnline) -> Optional[pam_pb2.PAMOnlineControllers]: """Get connected gateways from the router.""" rs = vault.keeper_auth.keeper_endpoint.execute_router_rest( @@ -44,14 +51,14 @@ def router_send_action_to_gateway(context: KeeperParams, gateway_action: Gateway destination_gateway_uid_str=None, gateway_timeout=15000, transmission_key=None, encrypted_transmission_key=None, encrypted_session_token=None): - krouter_host = context.auth.keeper_endpoint.get_router_server() + krouter_url = _router_base_url(context.auth.keeper_endpoint) try: router_enterprise_controllers_connected = \ [x.controllerUid for x in router_get_connected_gateways(context.vault).controllers] except requests.exceptions.ConnectionError as errc: - logging.info(f"Looks like router is down. Router URL [{krouter_host}]") + logging.info(f"Looks like router is down. Router URL [{krouter_url}]") return except Exception as e: raise e @@ -148,7 +155,7 @@ def router_send_action_to_gateway(context: KeeperParams, gateway_action: Gateway def router_send_message_to_gateway(context: KeeperParams, transmission_key, rq_proto, encrypted_transmission_key=None, encrypted_session_token=None): - krouter_host = context.auth.keeper_endpoint.get_router_server() + krouter_url = _router_base_url(context.auth.keeper_endpoint) if not encrypted_transmission_key: server_public_key = endpoint.SERVER_PUBLIC_KEYS[context.auth.keeper_endpoint.server_key_id] @@ -166,7 +173,7 @@ def router_send_message_to_gateway(context: KeeperParams, transmission_key, rq_p encrypted_session_token = crypto.encrypt_aes_v2(context.auth.auth_context.session_token, transmission_key) rs = requests.post( - krouter_host+"/api/user/send_controller_message", + krouter_url + "/api/user/send_controller_message", verify=VERIFY_SSL, headers={ @@ -388,7 +395,7 @@ def router_get_rotation_schedules(vault: vault_online, proto_request): def _post_request_to_router(vault: vault_online.VaultOnline, path, rq_proto=None, rs_type=None, method='post', raw_without_status_check_response=False, query_params=None, transmission_key=None, encrypted_transmission_key=None, encrypted_session_token=None): - krouter_host = vault.keeper_auth.keeper_endpoint.get_router_server() + krouter_url = _router_base_url(vault.keeper_auth.keeper_endpoint) path = '/api/user/' + path if not transmission_key: @@ -414,7 +421,7 @@ def _post_request_to_router(vault: vault_online.VaultOnline, path, rq_proto=None try: rs = requests.request(method, - krouter_host + path, + krouter_url + path, params=query_params, verify=VERIFY_SSL, headers={ @@ -424,7 +431,7 @@ def _post_request_to_router(vault: vault_online.VaultOnline, path, rq_proto=None data=encrypted_payload if rq_proto else None ) except ConnectionError as e: - raise KeeperApiError('-1', f"KRouter is not reachable on '{krouter_host}'. Error: ${e}") + raise KeeperApiError('-1', f"KRouter is not reachable on '{krouter_url}'. Error: ${e}") except Exception as ex: raise ex diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py index 3865348a..dec0bdb7 100644 --- a/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/constants.py @@ -1,3 +1,5 @@ +from enum import StrEnum + # This should the relationship between Keeper Vault record RECORD_LINK_GRAPH_ID = 0 @@ -25,25 +27,35 @@ PAM_MACHINE ] -PAM_DOMAIN_CONFIGURATION = "pamDomainConfiguration" -PAM_AZURE_CONFIGURATION = "pamAzureConfiguration" -PAM_AWS_CONFIGURATION = "pamAwsConfiguration" -PAM_NETWORK_CONFIGURATION = "pamNetworkConfiguration" -PAM_GCP_CONFIGURATION = "pamGcpConfiguration" - -PAM_CONFIGURATIONS = [ - PAM_DOMAIN_CONFIGURATION, - PAM_AZURE_CONFIGURATION, - PAM_AWS_CONFIGURATION, - PAM_NETWORK_CONFIGURATION, - PAM_GCP_CONFIGURATION -] +class PamConfigurationRecordType(StrEnum): + AWS = "pamAwsConfiguration" + AZURE = "pamAzureConfiguration" + GCP = "pamGcpConfiguration" + DOMAIN = "pamDomainConfiguration" + NETWORK = "pamNetworkConfiguration" + OCI = "pamOciConfiguration" -DOMAIN_USER_CONFIGS = [ - PAM_DOMAIN_CONFIGURATION, - PAM_AZURE_CONFIGURATION -] +PAM_AWS_CONFIGURATION = PamConfigurationRecordType.AWS +PAM_AZURE_CONFIGURATION = PamConfigurationRecordType.AZURE +PAM_GCP_CONFIGURATION = PamConfigurationRecordType.GCP +PAM_DOMAIN_CONFIGURATION = PamConfigurationRecordType.DOMAIN +PAM_NETWORK_CONFIGURATION = PamConfigurationRecordType.NETWORK +PAM_OCI_CONFIGURATION = PamConfigurationRecordType.OCI + +PAM_CONFIGURATIONS = ( + PamConfigurationRecordType.AWS, + PamConfigurationRecordType.AZURE, + PamConfigurationRecordType.GCP, + PamConfigurationRecordType.DOMAIN, + PamConfigurationRecordType.NETWORK, + PamConfigurationRecordType.OCI, +) + +DOMAIN_USER_CONFIGS = ( + PamConfigurationRecordType.DOMAIN, + PamConfigurationRecordType.AZURE, +) VERTICES_SORT_MAP = { PAM_USER: {"order": 1, "sort": "sort_infra_name", "item": "DiscoveryUser", "key": "user"}, diff --git a/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py index 0ebc36c9..99276b39 100644 --- a/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py +++ b/keepersdk-package/src/keepersdk/helpers/tunnel/tunnel_graph.py @@ -1,4 +1,6 @@ import logging +from enum import Enum +from typing import Optional, Union from . import tunnel_utils @@ -12,6 +14,33 @@ logger = logging.getLogger(__name__) + +class TriStateSetting(str, Enum): + """Tri-state CLI values for ACL/tunnel options (maps to on/off/default).""" + + ON = 'on' + OFF = 'off' + DEFAULT = 'default' + + +AclOptionValue = Optional[Union[bool, TriStateSetting]] + + +def _acl_edge_option_value(value: AclOptionValue) -> Optional[Union[bool, str]]: + """Resolve bool (explicit flag) or TriStateSetting to ACL edge content values.""" + if value is None: + return None + if isinstance(value, bool): + return value + if isinstance(value, TriStateSetting): + return { + TriStateSetting.ON: True, + TriStateSetting.OFF: False, + TriStateSetting.DEFAULT: '', + }[value] + raise TypeError(f'Invalid ACL option value: {value!r}') + + def get_vertex_content(vertex): return_content = None if vertex is None: @@ -61,7 +90,8 @@ def ensure_resource_meta_v1(content): class TunnelDAG: - def __init__(self, vault: vault_online.VaultOnline, encrypted_session_token, encrypted_transmission_key, record_uid: str, is_config=False): + def __init__(self, vault: vault_online.VaultOnline, encrypted_session_token, encrypted_transmission_key, + record_uid: str, is_config=False, transmission_key=None): config_uid = None if not is_config: config_uid = tunnel_utils.get_config_uid(vault, encrypted_session_token, encrypted_transmission_key, record_uid) @@ -74,6 +104,7 @@ def __init__(self, vault: vault_online.VaultOnline, encrypted_session_token, enc self.encrypted_transmission_key = encrypted_transmission_key self.conn = Connection(vault=vault, encrypted_transmission_key=self.encrypted_transmission_key, encrypted_session_token=self.encrypted_session_token, + transmission_key=transmission_key, use_write_protobuf=True ) self.linking_dag = DAG(conn=self.conn, record=self.record, graph_id=0, write_endpoint=PamEndpoints.PAM) @@ -264,25 +295,26 @@ def link_user_to_config(self, user_uid): config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid) self.link_user(user_uid, config_vertex, belongs_to=True, is_iam_user=True) - def link_user_to_config_with_options(self, user_uid, is_admin=None, belongs_to=None, is_iam_user=None): + def link_user_to_config_with_options( + self, + user_uid: str, + is_admin: AclOptionValue = None, + belongs_to: AclOptionValue = None, + is_iam_user: AclOptionValue = None, + ): config_vertex = self.linking_dag.get_vertex(self.record.record_uid) if config_vertex is None: config_vertex = self.linking_dag.add_vertex(uid=self.record.record_uid) - # self.link_user(user_uid, config_vertex, is_admin, belongs_to, is_iam_user) source_vertex = config_vertex user_vertex = self.linking_dag.get_vertex(user_uid) if user_vertex is None: user_vertex = self.linking_dag.add_vertex(uid=user_uid, vertex_type=RefType.PAM_USER) - # switching to 3-state on/off/default: on/true, off/false, - # None = Keep existing, 'default' = Reset to default (remove from dict) - states = {'on': True, 'off': False, 'default': '', 'none': None} - content = { - "belongs_to": states.get(str(belongs_to).lower()), - "is_admin": states.get(str(is_admin).lower()), - "is_iam_user": states.get(str(is_iam_user).lower()) + "belongs_to": _acl_edge_option_value(belongs_to), + "is_admin": _acl_edge_option_value(is_admin), + "is_iam_user": _acl_edge_option_value(is_iam_user), } if user_vertex.vertex_type != RefType.PAM_USER: user_vertex.vertex_type = RefType.PAM_USER diff --git a/keepersdk-package/src/keepersdk/vault/record_management.py b/keepersdk-package/src/keepersdk/vault/record_management.py index c3c9f44c..271ec1be 100644 --- a/keepersdk-package/src/keepersdk/vault/record_management.py +++ b/keepersdk-package/src/keepersdk/vault/record_management.py @@ -192,7 +192,8 @@ def update_record(vault: vault_online.VaultOnline, record: Union[PasswordRecord, ur = record_pb2.RecordUpdate() ur.record_uid = record_uid_bytes ur.client_modified_time = utils.current_milli_time() - ur.revision = record_info.revision + storage_rec = vault.vault_data.storage.records.get_entity(record.record_uid) + ur.revision = storage_rec.revision if storage_rec else record_info.revision data = vault_extensions.extract_typed_record_data(record, vault.vault_data.get_record_type_by_name(record.record_type)) ur.data = crypto.encrypt_aes_v2(vault_extensions.get_padded_json_bytes(data), record_key) diff --git a/keepersdk-package/src/keepersdk/vault/share_management_utils.py b/keepersdk-package/src/keepersdk/vault/share_management_utils.py index f30ccbf3..cf9a55e7 100644 --- a/keepersdk-package/src/keepersdk/vault/share_management_utils.py +++ b/keepersdk-package/src/keepersdk/vault/share_management_utils.py @@ -1,12 +1,13 @@ import datetime import itertools +import json import logging from re import findall from typing import Optional, Dict, List, Any, Generator, Iterable, Set, Tuple, Union from .. import crypto, utils from ..proto import enterprise_pb2, folder_pb2, record_pb2 -from ..vault import vault_online, vault_record, vault_types, vault_utils +from . import vault_data, storage_types, vault_online, vault_record, vault_types, vault_utils, sync_down from ..enterprise import enterprise_data @@ -162,7 +163,7 @@ def load_records_in_shared_folder( vault: vault_online.VaultOnline, shared_folder_uid: str, record_uids: Optional[Set[str]] = None -) -> None: +) -> Set[str]: try: shared_folder = _find_shared_folder(vault, shared_folder_uid) if not shared_folder: @@ -176,7 +177,12 @@ def load_records_in_shared_folder( candidates = record_uids or record_keys.keys() record_set = {uid for uid in candidates if uid in record_keys and uid not in record_cache} - _load_records_in_batches(vault, record_set, record_keys) + loaded = _load_records_in_batches(vault, record_set, record_keys, shared_folder_uid) # SF uid + if loaded: + changes = vault_data.RebuildTask(is_full_sync=False) + changes.add_records(loaded) + vault.vault_data.rebuild_data(changes) + return loaded except ShareNotFoundError: raise @@ -184,6 +190,83 @@ def load_records_in_shared_folder( raise ShareManagementError(f"Failed to load records in shared folder: {e}") from e +def try_load_record_on_demand(vault: vault_online.VaultOnline, record_uid: str) -> bool: + """ + Fetch and persist a record via vault/get_records_details when it is not in the + local vault index (shared-folder metadata, personal record keys, or API key). + """ + if not record_uid or vault.vault_data.get_record(record_uid): + return vault.vault_data.get_record(record_uid) is not None + + for shared_folder_uid in _shared_folder_uids_for_record(vault, record_uid): + try: + load_records_in_shared_folder(vault, shared_folder_uid, {record_uid}) + except ShareManagementError as e: + logger.debug('On-demand load for record "%s" in SF "%s": %s', record_uid, shared_folder_uid, e) + if vault.vault_data.get_record(record_uid): + return True + + plain_key = _get_plaintext_record_key_from_storage(vault, record_uid) + record_keys = {record_uid: plain_key} if plain_key else {} + encrypter_uid = vault.vault_data.storage.personal_scope_uid + loaded = _load_records_in_batches(vault, {record_uid}, record_keys, encrypter_uid) + if loaded: + changes = vault_data.RebuildTask(is_full_sync=False) + changes.add_records(loaded) + vault.vault_data.rebuild_data(changes) + return vault.vault_data.get_record(record_uid) is not None + + +def _get_plaintext_record_key_from_storage( + vault: vault_online.VaultOnline, record_uid: str +) -> Optional[bytes]: + for link in vault.vault_data.storage.record_keys.get_links_by_subject(record_uid): + key = vault.vault_data.decrypt_record_key(link) + if key: + return key + return None + + +def _resolve_record_key_for_details( + vault: vault_online.VaultOnline, + record_data, + record_uid: str, + record_keys: Dict[str, bytes], +) -> Optional[bytes]: + _process_record_owner_key(record_data, record_uid, record_keys) + if record_uid in record_keys and record_keys[record_uid]: + return record_keys[record_uid] + if not record_data.recordKey: + return None + try: + return sync_down.decrypt_keeper_key( + vault.keeper_auth.auth_context, + record_data.recordKey, + record_data.recordKeyType, + ) + except Exception as e: + logger.debug('Cannot resolve record key for "%s" from API: %s', record_uid, e) + return None + + +def _shared_folder_uids_for_record(vault: vault_online.VaultOnline, record_uid: str) -> List[str]: + sf_uids: List[str] = [] + seen: Set[str] = set() + for link in vault.vault_data.storage.record_keys.get_links_by_subject(record_uid): + if (link.key_type == storage_types.StorageKeyType.SharedFolderKey_AES_Any + and link.encrypter_uid not in seen): + seen.add(link.encrypter_uid) + sf_uids.append(link.encrypter_uid) + for sf_info in vault.vault_data.shared_folders(): + if sf_info.shared_folder_uid in seen: + continue + sf = vault.vault_data.load_shared_folder(shared_folder_uid=sf_info.shared_folder_uid) + if sf and any(r.record_uid == record_uid for r in sf.record_permissions): + seen.add(sf_info.shared_folder_uid) + sf_uids.append(sf_info.shared_folder_uid) + return sf_uids + + 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(): @@ -244,8 +327,14 @@ def _build_record_details_request(record_uids: set) -> record_pb2.GetRecordDataW return request -def _load_records_in_batches(vault: vault_online.VaultOnline, record_set: set, record_keys: dict): +def _load_records_in_batches( + vault: vault_online.VaultOnline, + record_set: set, + record_keys: dict, + key_encrypter_uid: str, +) -> Set[str]: + loaded: Set[str] = set() while record_set: request = _build_record_details_request(record_set) record_set.clear() @@ -260,7 +349,10 @@ def _load_records_in_batches(vault: vault_online.VaultOnline, record_set: set, r logger.warning("No record data received from API") break - _process_record_batch(vault, response, record_keys, record_set) + batch_loaded = _process_record_batch(vault, response, record_keys, record_set, key_encrypter_uid) + loaded.update(batch_loaded) + record_set.difference_update({x.record_uid for x in vault.vault_data.records()}) + return loaded def _process_record_owner_key(record_data, record_uid: str, record_keys: dict): @@ -274,29 +366,37 @@ def _process_record_owner_key(record_data, record_uid: str, record_keys: dict): ) -def _process_record_batch(vault: vault_online.VaultOnline, response, record_keys: dict, record_set: set): +def _process_record_batch( + vault: vault_online.VaultOnline, + response, + record_keys: dict, + record_set: set, + key_encrypter_uid: str, +) -> Set[str]: + batch_records: List[dict] = [] for record_info in response.recordDataWithAccessInfo: record_uid = utils.base64_url_encode(record_info.recordUid) record_data = record_info.recordData try: - _process_record_owner_key(record_data, record_uid, record_keys) - - if record_uid not in record_keys: + record_key = _resolve_record_key_for_details(vault, record_data, record_uid, record_keys) + if not record_key: continue - - record_key = record_keys[record_uid] + record_keys[record_uid] = record_key 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) + _handle_record_versions(record, record_data, version) _add_share_permissions(record, record_info) - record_set.add(record_uid) + record_set.update(_collect_typed_record_ref_uids(record)) + batch_records.append(record) except Exception as e: logger.debug('Error decrypting record "%s": %s', record_uid, e) + return _persist_loaded_records(vault, batch_records, key_encrypter_uid) + def _create_record_dict(record_uid: str, record_data, record_key: bytes, version: int) -> dict: """Create record dictionary from API data.""" @@ -329,13 +429,103 @@ def _process_v2_extra_data(record: dict, record_data, record_key: bytes): record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) -def _process_v3_record_references(vault: vault_online.VaultOnline, record: dict, record_set: set): +def _collect_typed_record_ref_uids(record: dict) -> Set[str]: + version = record.get('version', 0) + if version < V3_VERSION or version == V4_VERSION: + return set() + data_unencrypted = record.get('data_unencrypted') + if not data_unencrypted: + return set() + try: + data_dict = json.loads(data_unencrypted.decode()) + except Exception: + return set() + extra_dict = None + extra_unencrypted = record.get('extra_unencrypted') + if extra_unencrypted: + try: + extra_dict = json.loads(extra_unencrypted.decode()) + except Exception: + extra_dict = None + typed = vault_record.TypedRecord() + typed.record_uid = record['record_uid'] + typed.version = version + typed.load_record_data(data_dict, extra_dict) + refs: Set[str] = set() + for ref in itertools.chain(typed.fields, typed.custom): + if ref.type.endswith('Ref') and isinstance(ref.value, list): + refs.update(ref.value) + return refs + + +def _encrypted_field_to_bytes(value: Union[str, bytes]) -> bytes: + if isinstance(value, bytes): + return value + if isinstance(value, str): + return utils.base64_url_decode(value) + return b'' + + +def _dict_to_storage_record(record: dict) -> storage_types.StorageRecord: + sr = storage_types.StorageRecord() + sr.record_uid = record['record_uid'] + sr.revision = record.get('revision', 0) + sr.version = record.get('version', 0) + sr.modified_time = record.get('client_modified_time', 0) + sr.shared = record.get('shared', False) + sr.data = _encrypted_field_to_bytes(record['data']) + if record.get('extra'): + sr.extra = _encrypted_field_to_bytes(record['extra']) + return sr - 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) + +def _ensure_record_key_link( + vault: vault_online.VaultOnline, + record: dict, + key_encrypter_uid: str, +) -> None: + record_uid = record['record_uid'] + if list(vault.vault_data.storage.record_keys.get_links_by_subject(record_uid)): + return + record_key = record.get('record_key_unencrypted') + if not record_key: + return + srk = storage_types.StorageRecordKey() + srk.record_uid = record_uid + srk.encrypter_uid = key_encrypter_uid + personal_uid = vault.vault_data.storage.personal_scope_uid + if key_encrypter_uid == personal_uid: + srk.key_type = storage_types.StorageKeyType.UserClientKey_AES_GCM + srk.record_key = crypto.encrypt_aes_v2( + record_key, vault.keeper_auth.auth_context.client_key + ) + else: + srk.key_type = storage_types.StorageKeyType.SharedFolderKey_AES_Any + sf = vault.vault_data._shared_folders.get(key_encrypter_uid) + if not sf: + return + srk.record_key = crypto.encrypt_aes_v2(record_key, sf.shared_folder_key) + vault.vault_data.storage.record_keys.put_links([srk]) + + +def _persist_loaded_records( + vault: vault_online.VaultOnline, + records: List[dict], + key_encrypter_uid: str, +) -> Set[str]: + if not records: + return set() + storage_records: List[storage_types.StorageRecord] = [] + loaded: Set[str] = set() + for record in records: + if not record.get('data_unencrypted'): + continue + storage_records.append(_dict_to_storage_record(record)) + _ensure_record_key_link(vault, record, key_encrypter_uid) + loaded.add(record['record_uid']) + if storage_records: + vault.vault_data.storage.records.put_entities(storage_records) + return loaded def _process_v4_record_metadata(record: dict, record_data): @@ -353,16 +543,13 @@ def _process_record_owner_info(record: dict, record_data): record['link_key'] = utils.base64_url_encode(record_data.recordKey) -def _handle_record_versions(vault: vault_online.VaultOnline, record: dict, record_data, version: int, record_set: set): +def _handle_record_versions(record: dict, record_data, version: int) -> None: record_key = record['record_key_unencrypted'] record['data_unencrypted'] = _decrypt_record_data(record_data, record_key, version) if version <= MAX_V2_VERSION: _process_v2_extra_data(record, record_data, record_key) - - if version == V3_VERSION: - _process_v3_record_references(vault, record, record_set) elif version == V4_VERSION: _process_v4_record_metadata(record, record_data) From 72cdbf65a0d62459b6d9fde7bfba48f6a55017c6 Mon Sep 17 00:00:00 2001 From: mtyagi-ks Date: Mon, 1 Jun 2026 17:56:26 +0530 Subject: [PATCH 16/23] Secrets Manager App update and share update commands --- .../secrets_manager/update_app.py | 535 +++++++++++++++++ .../secrets_manager/update_app_shares.py | 544 ++++++++++++++++++ .../src/keepercli/commands/secrets_manager.py | 67 ++- .../src/keepersdk/vault/ksm_management.py | 139 ++++- 4 files changed, 1279 insertions(+), 6 deletions(-) create mode 100644 examples/sdk_examples/secrets_manager/update_app.py create mode 100644 examples/sdk_examples/secrets_manager/update_app_shares.py diff --git a/examples/sdk_examples/secrets_manager/update_app.py b/examples/sdk_examples/secrets_manager/update_app.py new file mode 100644 index 00000000..7edc429c --- /dev/null +++ b/examples/sdk_examples/secrets_manager/update_app.py @@ -0,0 +1,535 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def update_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAuth): + """Rename a Secrets Manager application.""" + conn = sqlite3.Connection('file::memory:', uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + try: + app_uid_or_name = "" + new_name = "" + print(f"\nUpdating app '{app_uid_or_name}' to name '{new_name}'...") + + app_uid, old_name, updated_name = ksm_management.update_secrets_manager_app( + vault=vault, + uid_or_name=app_uid_or_name, + new_name=new_name, + ) + print(f'Application "{old_name}" renamed to "{updated_name}" (UID: {app_uid})') + + except Exception as e: + print(f'Error updating Secrets Manager application: {e}') + + vault.close() + keeper_auth_context.close() + + +def main(): + """Main function to orchestrate login and update a Secrets Manager application.""" + keeper_auth_context, _ = login() + if keeper_auth_context: + update_secrets_manager_application(keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/secrets_manager/update_app_shares.py b/examples/sdk_examples/secrets_manager/update_app_shares.py new file mode 100644 index 00000000..b623f0cd --- /dev/null +++ b/examples/sdk_examples/secrets_manager/update_app_shares.py @@ -0,0 +1,544 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def update_secrets_manager_share_permissions(keeper_auth_context: keeper_auth.KeeperAuth): + """Update share permissions on secrets in a Secrets Manager application.""" + conn = sqlite3.Connection('file::memory:', uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, 'utf-8') + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + + try: + app_uid_or_name = "" + secret_uids = ["", ""] + is_editable = True # True for editable, False for read-only + perm = 'editable' if is_editable else 'read-only' + print(f'\nUpdating {len(secret_uids)} share(s) to {perm} on app "{app_uid_or_name}"...') + + updated = ksm_management.update_secrets_manager_app_shares( + vault=vault, + uid_or_name=app_uid_or_name, + secret_uids=secret_uids, + is_editable=is_editable, + ) + + if updated: + print(f'Successfully updated share permissions to {perm}:') + for uid in updated: + print(f' {uid}') + else: + print('No shares were updated (secrets may not be shared with the app yet).') + + except Exception as e: + print(f'Error updating Secrets Manager share permissions: {e}') + + vault.close() + keeper_auth_context.close() + + +def main(): + """Main function to orchestrate login and update Secrets Manager share permissions.""" + keeper_auth_context, _ = login() + if keeper_auth_context: + update_secrets_manager_share_permissions(keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/keepercli-package/src/keepercli/commands/secrets_manager.py b/keepercli-package/src/keepercli/commands/secrets_manager.py index f7a6536f..6d7d0771 100644 --- a/keepercli-package/src/keepercli/commands/secrets_manager.py +++ b/keepercli-package/src/keepercli/commands/secrets_manager.py @@ -41,6 +41,7 @@ class SecretsManagerCommand(Enum): GET = "get" ADD = 'add' CREATE = "create" + UPDATE = "update" REMOVE = "remove" SHARE = "share" UNSHARE = "unshare" @@ -75,6 +76,10 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser): parser.add_argument( '--admin', action='store_true', help='Allow share recipient to manage application' ) + parser.add_argument( + '--name', '-n', dest='new_name', action='store', + help='New application name (update command only)' + ) def execute(self, context: KeeperParams, **kwargs) -> None: self._validate_vault(context) @@ -110,11 +115,14 @@ def _get_command_handler(self, context: KeeperParams, command: str, kwargs: dict email = kwargs.get('email') is_admin = kwargs.get('admin', False) + new_name = kwargs.get('new_name') + command_handlers = { SecretsManagerCommand.LIST.value: lambda: self.list_app(vault=vault), SecretsManagerCommand.GET.value: lambda: self.get_app(vault=vault, uid_or_name=uid_or_name), SecretsManagerCommand.CREATE.value: lambda: self._handle_create_app(context, vault, uid_or_name, force), SecretsManagerCommand.ADD.value: lambda: self._handle_create_app(context, vault, uid_or_name, force), + SecretsManagerCommand.UPDATE.value: lambda: self._handle_update_app(context, vault, uid_or_name, new_name), SecretsManagerCommand.REMOVE.value: lambda: self.remove_app(vault=vault, uid_or_name=uid_or_name, force=force), SecretsManagerCommand.SHARE.value: lambda: self._handle_share_app(context, uid_or_name, email, is_admin, unshare=False), SecretsManagerCommand.UNSHARE.value: lambda: self._handle_share_app(context, uid_or_name, email, is_admin, unshare=True) @@ -127,6 +135,12 @@ def _handle_create_app(self, context: KeeperParams, vault, name: str, force: boo self.create_app(vault=vault, name=name, force=force) context.vault_down() + def _handle_update_app(self, context: KeeperParams, vault, uid_or_name: str, new_name: Optional[str]) -> None: + """Handle app rename and sync vault.""" + if not new_name: + raise ValueError('New application name is required. Use --name="New App Name"') + self.update_app(vault=vault, uid_or_name=uid_or_name, new_name=new_name) + def _handle_share_app(self, context: KeeperParams, uid_or_name: str, email: Optional[str], is_admin: bool, unshare: bool) -> None: """Handle app sharing/unsharing and sync vault.""" @@ -170,6 +184,12 @@ def create_app(self, vault: vault_online.VaultOnline, name: str, force: Optional 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 update_app(self, vault: vault_online.VaultOnline, uid_or_name: str, new_name: str): + app_uid, old_name, updated_name = ksm_management.update_secrets_manager_app( + vault=vault, uid_or_name=uid_or_name, new_name=new_name) + logger.info( + f'Application "{old_name}" was successfully renamed to "{updated_name}" (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): @@ -490,12 +510,21 @@ def __init__(self): 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}" + choices=[ + SecretsManagerCommand.ADD.value, + SecretsManagerCommand.REMOVE.value, + SecretsManagerCommand.UPDATE.value, + ], + help=f"One of: {SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}, " + f"{SecretsManagerCommand.UPDATE.value}" ) parser.add_argument( '--editable', '-e', action='store_true', required=False, - help='Is this share going to be editable or not' + help='Grant editable access (add or update)' + ) + parser.add_argument( + '--readonly', '-r', action='store_true', required=False, + help='Set share to read-only (update only)' ) parser.add_argument( '--app', '-a', type=str, action='store', help='Application Name or UID' @@ -519,6 +548,8 @@ def execute(self, context: KeeperParams, **kwargs) -> None: 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.UPDATE.value: + self._handle_update_share(context, app_uid, secret_uids, kwargs) elif command == SecretsManagerCommand.REMOVE.value: SecretsManagerShareCommand.remove_share( vault=context.vault, @@ -526,7 +557,10 @@ def execute(self, context: KeeperParams, **kwargs) -> None: secret_uids=secret_uids ) else: - available_commands = f"{SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}" + available_commands = ( + f"{SecretsManagerCommand.ADD.value}, {SecretsManagerCommand.REMOVE.value}, " + f"{SecretsManagerCommand.UPDATE.value}" + ) raise base.CommandError(f"Unknown command '{command}'. Available commands: {available_commands}") def _get_app_uid_from_kwargs(self, vault, app_uid_or_name: Optional[str]) -> str: @@ -553,6 +587,31 @@ def _find_ksm_application(self, vault: vault_online.VaultOnline, app_uid_or_name None ) + def _handle_update_share(self, context: KeeperParams, app_uid: str, secret_uids: List[str], kwargs: dict) -> None: + """Handle updating editable permissions on existing shares.""" + if not secret_uids: + raise ValueError('Secret UID(s) are required. Use --secret="uid1 uid2"') + + is_editable = kwargs.get('editable', False) + is_readonly = kwargs.get('readonly', False) + if not is_editable and not is_readonly: + raise ValueError('Specify either --editable or --readonly') + if is_editable and is_readonly: + raise ValueError('Cannot specify both --editable and --readonly') + + updated_uids = ksm_management.update_secrets_manager_app_shares( + vault=context.vault, + uid_or_name=app_uid, + secret_uids=secret_uids, + is_editable=is_editable, + ) + if updated_uids: + perm = 'editable' if is_editable else 'read-only' + logger.info(f'\nSuccessfully updated share permissions to {perm} for app uid={app_uid}:') + for uid in updated_uids: + logger.info(f'\t{uid}') + context.vault_down() + def _handle_add_share(self, context: KeeperParams, app_uid: str, secret_uids: List[str], is_editable: bool) -> None: """Handle adding shares to a KSM application.""" if not context.vault: diff --git a/keepersdk-package/src/keepersdk/vault/ksm_management.py b/keepersdk-package/src/keepersdk/vault/ksm_management.py index 1d5999ed..566a0573 100644 --- a/keepersdk-package/src/keepersdk/vault/ksm_management.py +++ b/keepersdk-package/src/keepersdk/vault/ksm_management.py @@ -15,8 +15,11 @@ GetAppInfoResponse, RemoveAppClientsRequest, Device, AddAppClientRequest, AppShareAdd, AddAppSharesRequest, RemoveAppSharesRequest ) +from ..errors import KeeperApiError from ..proto.enterprise_pb2 import GENERAL -from ..proto.record_pb2 import ApplicationAddRequest +from ..proto import record_pb2 +from ..proto.record_pb2 import ApplicationAddRequest, RecordUpdate, RecordsUpdateRequest +from . import vault_extensions URL_GET_SUMMARY_API = 'vault/get_applications_summary' URL_GET_APP_INFO_API = 'vault/get_app_info' @@ -27,6 +30,7 @@ SHARE_ADD_URL = 'vault/app_share_add' SHARE_REMOVE_URL = 'vault/app_share_remove' +RECORDS_UPDATE_URL = 'vault/records_update' CLIENT_SHORT_ID_LENGTH = 8 @@ -113,6 +117,63 @@ def get_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str) - ) +def update_secrets_manager_app(vault: vault_online.VaultOnline, uid_or_name: str, new_name: str) -> Tuple[str, str, str]: + """Rename a KSM application (v5 app record title). Returns (app_uid, old_name, new_name).""" + if not new_name or not new_name.strip(): + raise ValueError('New application name is required') + + new_name = new_name.strip() + record = vault.vault_data.get_record(uid_or_name) + if not record: + records = list(vault.vault_data.find_records(criteria=uid_or_name, record_type=None, record_version=5)) + if len(records) > 1: + raise ValueError(f'Multiple applications found with the name: {uid_or_name}, use UID to identify the application') + if not records: + raise ValueError(f'Application with the name {uid_or_name} not found') + app_info = records[0] + else: + app_info = record + if app_info.version != 5: + raise ValueError(f'Record "{uid_or_name}" is not a Secrets Manager application') + app_uid = app_info.record_uid + + old_name = app_info.title + record_key = vault.vault_data.get_record_key(app_uid) + if not record_key: + raise ValueError(f'Could not resolve record key for application {app_uid}') + + data_dict = { + 'title': new_name, + 'type': app_info.record_type or 'app', + } + + record_uid_bytes = utils.base64_url_decode(app_uid) + update = RecordUpdate() + update.record_uid = record_uid_bytes + update.client_modified_time = utils.current_milli_time() + update.revision = app_info.revision + update.data = crypto.encrypt_aes_v2(vault_extensions.get_padded_json_bytes(data_dict), record_key) + + request = RecordsUpdateRequest() + request.client_time = utils.current_milli_time() + request.records.append(update) + + response = vault.keeper_auth.execute_auth_rest( + RECORDS_UPDATE_URL, request, response_type=record_pb2.RecordsModifyResponse) + if response is None: + raise KeeperApiError('unknown', 'vault/records_update returned no response') + + status = next((x for x in response.records if record_uid_bytes == x.record_uid), None) + if status is None: + raise KeeperApiError('unknown', f'No status returned for record {app_uid}') + if status.status != record_pb2.RecordModifyResult.RS_SUCCESS: + code = record_pb2.RecordModifyResult.Name(status.status) + raise KeeperApiError(code, status.message) + + vault.sync_down() + return app_uid, old_name, new_name + + 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) @@ -148,6 +209,27 @@ def create_secrets_manager_app(vault: vault_online.VaultOnline, name: str, force return app_uid_str +def update_secrets_manager_app_shares(vault: vault_online.VaultOnline, uid_or_name: str, + secret_uids: List[str], is_editable: bool) -> List[str]: + """Update editable permissions on record/SF shares already in a KSM app.""" + + record = vault.vault_data.get_record(uid_or_name) + if not record: + records = list(vault.vault_data.find_records(criteria=uid_or_name, record_type=None, record_version=5)) + if len(records) > 1: + raise ValueError(f'Multiple applications found with the name: {uid_or_name}, use UID to identify the application') + if not records: + raise ValueError(f'Application with the name {uid_or_name} not found') + app_info = records[0] + else: + app_info = record + if app_info.version != 5: + raise ValueError(f'Record "{uid_or_name}" is not a Secrets Manager application') + + return KSMShareManagement.update_secrets_in_ksm_app( + vault, app_info.record_uid, secret_uids, is_editable) + + 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) @@ -880,4 +962,57 @@ def remove_secrets_from_ksm_app(vault: vault_online.VaultOnline, app_uid: str, 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) \ No newline at end of file + vault.keeper_auth.execute_auth_rest(rest_endpoint=SHARE_REMOVE_URL, request=request) + + @staticmethod + def update_secrets_in_ksm_app(vault: vault_online.VaultOnline, app_uid: str, secret_uids: List[str], + is_editable: bool) -> List[str]: + """Update editable vs read-only on secrets already shared with the app (remove + re-add).""" + if not secret_uids: + raise ValueError('At least one secret UID is required') + + master_key = vault.vault_data.get_record_key(app_uid) + if not master_key: + raise ValueError(f'Could not resolve record key for application {app_uid}') + + app_infos = get_app_info(vault=vault, app_uid=app_uid) + if not app_infos: + raise ValueError(f'Could not retrieve application info for UID: {app_uid}') + + existing_shares = { + utils.base64_url_encode(share.secretUid): share + for share in getattr(app_infos[0], 'shares', []) + } + + uids_to_update = [] + for uid in secret_uids: + if uid not in existing_shares: + logging.warning( + 'Secret "%s" is not currently shared with this application. ' + 'Use share add first.', uid) + continue + current_share = existing_shares[uid] + if current_share.editable == is_editable: + perm = 'editable' if is_editable else 'read-only' + logging.info('Secret "%s" is already %s. No change needed.', uid, perm) + continue + uids_to_update.append(uid) + + if not uids_to_update: + logging.warning('No share permissions to update.') + return [] + + KSMShareManagement.remove_secrets_from_ksm_app(vault, app_uid, uids_to_update) + + app_shares = [] + for uid in uids_to_update: + share_info = KSMShareManagement._process_secret(vault, uid, master_key, is_editable) + if share_info: + app_shares.append(share_info['app_share']) + + if not app_shares: + raise ValueError('No valid secrets found to update. Run sync-down and try again.') + + KSMShareManagement._send_share_request(vault, app_uid, app_shares) + vault.sync_down() + return uids_to_update \ No newline at end of file From 80f1637fc3d7f4253c5d98bf4c5ee3b8b4953134 Mon Sep 17 00:00:00 2001 From: mtyagi-ks Date: Mon, 8 Jun 2026 14:42:12 +0530 Subject: [PATCH 17/23] password report command bug fixes --- .../src/keepercli/commands/password_report.py | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/password_report.py b/keepercli-package/src/keepercli/commands/password_report.py index 5ccbddd4..3883ec70 100644 --- a/keepercli-package/src/keepercli/commands/password_report.py +++ b/keepercli-package/src/keepercli/commands/password_report.py @@ -8,7 +8,8 @@ from ..params import KeeperParams from keepersdk import utils -from keepersdk.vault import vault_record, vault_extensions +from keepersdk.proto import client_pb2 +from keepersdk.vault import vault_record, vault_extensions, vault_types, vault_utils logger = api.get_logger() @@ -105,6 +106,27 @@ def _resolve_folder_uid(self, context: KeeperParams, path_or_uid: Optional[str]) return folder.folder_uid or '' + def _get_record_uids_in_folder_tree(self, context: KeeperParams, folder_uid: str) -> set: + """Return record UIDs under folder_uid, or entire vault when folder_uid is empty.""" + record_uids: set = set() + + def add_records(folder: vault_types.Folder) -> None: + record_uids.update(folder.records) + + folder = context.vault.vault_data.root_folder + if folder_uid: + folder = context.vault.vault_data.get_folder(folder_uid) + if not folder: + raise base.CommandError(f'Folder {folder_uid} not found') + + vault_utils.traverse_folder_tree( + context.vault.vault_data, + folder, + add_records + ) + + return record_uids + def _extract_password_from_record(self, record: Any) -> str: """Extract password from a vault record. @@ -157,53 +179,29 @@ def _truncate_text(self, text: str, max_length: int = DEFAULT_TRUNCATION_LENGTH) 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 + """Count how many vault records use each password (for reuse column in verbose mode).""" + password_count: Dict[str, int] = {} + for record_uid in context.vault.vault_data._records: + info = context.vault.vault_data.get_record(record_uid) + if not info or info.version not in SUPPORTED_RECORD_VERSIONS: + continue + record = context.vault.vault_data.load_record(record_uid) + if not record: + continue + password = self._extract_password_from_record(record) + 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 + """Get BreachWatch status label for a record (reuse count is computed separately).""" + bw_info = context.vault.vault_data.get_breach_watch_record(record_uid) + if not bw_info or bw_info.total <= 0: + return '', None + try: + return client_pb2.BWStatus.Name(bw_info.status), None + except ValueError: + return str(bw_info.status), None 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. @@ -234,9 +232,8 @@ def execute(self, context: KeeperParams, **kwargs: Any) -> Any: 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 + record_uids = self._get_record_uids_in_folder_tree(context, folder_uid) + report_table = [] report_header = ['record_uid', 'title', 'description', 'length', 'lower', 'upper', 'digits', 'special'] breach_watch_plugin = context.vault.breach_watch_plugin() @@ -250,9 +247,12 @@ def execute(self, context: KeeperParams, **kwargs: Any) -> Any: password_usage_count = {} output_format = kwargs.get('format') - for record_uid in records: + for record_uid in record_uids: + info = context.vault.vault_data.get_record(record_uid) + if not info or info.version not in SUPPORTED_RECORD_VERSIONS: + continue record = context.vault.vault_data.load_record(record_uid) - if not record or record.version not in SUPPORTED_RECORD_VERSIONS: + if not record: continue password = self._extract_password_from_record(record) From 8fc969cbe5beaa0066a6ee8f6700a6880c819a05 Mon Sep 17 00:00:00 2001 From: mtyagi-ks Date: Mon, 8 Jun 2026 17:07:33 +0530 Subject: [PATCH 18/23] device-management list, rename, action logout and action remove commands added --- .../device_management/list_devices.py | 534 +++++++++++++++++ .../device_management/logout_device.py | 540 +++++++++++++++++ .../device_management/remove_device.py | 540 +++++++++++++++++ .../device_management/rename_device.py | 541 ++++++++++++++++++ .../keepercli/commands/device_management.py | 149 +++++ .../commands/pam/discovery/__init__.py | 6 +- .../commands/pam/discovery/discover.py | 4 +- .../src/keepercli/commands/pam/pam_config.py | 20 +- .../src/keepercli/commands/pam/pam_rbi.py | 8 +- .../keepercli/commands/pam/pam_rotation.py | 4 +- .../src/keepercli/register_commands.py | 5 + .../authentication/device_management.py | 264 +++++++++ .../keepersdk/proto/DeviceManagement_pb2.py | 80 +++ .../keepersdk/proto/DeviceManagement_pb2.pyi | 300 ++++++++++ .../unit_tests/test_device_management.py | 80 +++ 15 files changed, 3050 insertions(+), 25 deletions(-) create mode 100644 examples/sdk_examples/device_management/list_devices.py create mode 100644 examples/sdk_examples/device_management/logout_device.py create mode 100644 examples/sdk_examples/device_management/remove_device.py create mode 100644 examples/sdk_examples/device_management/rename_device.py create mode 100644 keepercli-package/src/keepercli/commands/device_management.py create mode 100644 keepersdk-package/src/keepersdk/authentication/device_management.py create mode 100644 keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.pyi create mode 100644 keepersdk-package/unit_tests/test_device_management.py diff --git a/examples/sdk_examples/device_management/list_devices.py b/examples/sdk_examples/device_management/list_devices.py new file mode 100644 index 00000000..5a8b31cb --- /dev/null +++ b/examples/sdk_examples/device_management/list_devices.py @@ -0,0 +1,534 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +from keepersdk.authentication import device_management + + +def print_devices_table(devices): + if not devices: + print('\nNo devices found.') + return + print(f'\nUser Devices ({len(devices)} found)') + print('=' * 100) + print(f"{'ID':<4} {'Name':<24} {'Client Type':<14} {'Login Status':<14} {'Last Accessed':<20}") + print('-' * 100) + for d in devices: + last = d.last_accessed.strftime('%Y-%m-%d %H:%M:%S') if d.last_accessed else 'N/A' + print( + f"{d.list_index:<4} {d.name[:23]:<24} " + f"{d.client_type[:13]:<14} {d.login_status[:13]:<14} {last:<20}" + ) + print('-' * 100) + + +def list_user_devices_example(keeper_auth_context): + """List active devices for the logged-in user.""" + try: + devices = device_management.list_user_devices(keeper_auth_context) + print_devices_table(devices) + except Exception as e: + print(f'Error listing devices: {e}') + keeper_auth_context.close() + + +def main(): + keeper_auth_context, _ = login() + if keeper_auth_context: + list_user_devices_example(keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/device_management/logout_device.py b/examples/sdk_examples/device_management/logout_device.py new file mode 100644 index 00000000..ef82f1df --- /dev/null +++ b/examples/sdk_examples/device_management/logout_device.py @@ -0,0 +1,540 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +from keepersdk.authentication import device_management + + +def print_devices_table(devices): + if not devices: + print('\nNo devices found.') + return + print(f'\nUser Devices ({len(devices)} found)') + print('=' * 100) + print(f"{'ID':<4} {'Name':<24} {'Client Type':<14} {'Login Status':<14} {'Last Accessed':<20}") + print('-' * 100) + for d in devices: + last = d.last_accessed.strftime('%Y-%m-%d %H:%M:%S') if d.last_accessed else 'N/A' + print( + f"{d.list_index:<4} {d.name[:23]:<24} " + f"{d.client_type[:13]:<14} {d.login_status[:13]:<14} {last:<20}" + ) + print('-' * 100) + + +def main(): + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + device_identifiers = [''] + + try: + print(f'Logging out {len(device_identifiers)} device(s)...') + for name in device_management.logout_user_devices( + keeper_auth_context, device_identifiers + ): + print(f"Device '{name}' successfully logged out") + print('\nUpdated device list:') + print_devices_table(device_management.list_user_devices(keeper_auth_context)) + except Exception as e: + print(f'Error logging out devices: {e}') + finally: + keeper_auth_context.close() + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/device_management/remove_device.py b/examples/sdk_examples/device_management/remove_device.py new file mode 100644 index 00000000..fae1e308 --- /dev/null +++ b/examples/sdk_examples/device_management/remove_device.py @@ -0,0 +1,540 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +from keepersdk.authentication import device_management + + +def print_devices_table(devices): + if not devices: + print('\nNo devices found.') + return + print(f'\nUser Devices ({len(devices)} found)') + print('=' * 100) + print(f"{'ID':<4} {'Name':<24} {'Client Type':<14} {'Login Status':<14} {'Last Accessed':<20}") + print('-' * 100) + for d in devices: + last = d.last_accessed.strftime('%Y-%m-%d %H:%M:%S') if d.last_accessed else 'N/A' + print( + f"{d.list_index:<4} {d.name[:23]:<24} " + f"{d.client_type[:13]:<14} {d.login_status[:13]:<14} {last:<20}" + ) + print('-' * 100) + + +def main(): + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + device_identifiers = [''] + + try: + print(f'Removing {len(device_identifiers)} device(s)...') + for name in device_management.remove_user_devices( + keeper_auth_context, device_identifiers + ): + print(f"Device '{name}' successfully removed") + print('\nUpdated device list:') + print_devices_table(device_management.list_user_devices(keeper_auth_context)) + except Exception as e: + print(f'Error removing devices: {e}') + finally: + keeper_auth_context.close() + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/device_management/rename_device.py b/examples/sdk_examples/device_management/rename_device.py new file mode 100644 index 00000000..a9baa35f --- /dev/null +++ b/examples/sdk_examples/device_management/rename_device.py @@ -0,0 +1,541 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import sqlite_storage, vault_online, ksm_management + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +from keepersdk.authentication import device_management + + +def print_devices_table(devices): + if not devices: + print('\nNo devices found.') + return + print(f'\nUser Devices ({len(devices)} found)') + print('=' * 100) + print(f"{'ID':<4} {'Name':<24} {'Client Type':<14} {'Login Status':<14} {'Last Accessed':<20}") + print('-' * 100) + for d in devices: + last = d.last_accessed.strftime('%Y-%m-%d %H:%M:%S') if d.last_accessed else 'N/A' + print( + f"{d.list_index:<4} {d.name[:23]:<24} " + f"{d.client_type[:13]:<14} {d.login_status[:13]:<14} {last:<20}" + ) + print('-' * 100) + + +def main(): + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + device_identifier = '' + new_device_name = '' + + try: + print(f"Renaming device '{device_identifier}' to '{new_device_name}'...") + old_name, updated = device_management.rename_user_device( + keeper_auth_context, device_identifier, new_device_name + ) + print(f'Device renamed from "{old_name}" to "{updated}"') + print('\nUpdated device list:') + print_devices_table(device_management.list_user_devices(keeper_auth_context)) + except Exception as e: + print(f'Error renaming device: {e}') + finally: + keeper_auth_context.close() + + +if __name__ == '__main__': + main() diff --git a/keepercli-package/src/keepercli/commands/device_management.py b/keepercli-package/src/keepercli/commands/device_management.py new file mode 100644 index 00000000..b19e5d3d --- /dev/null +++ b/keepercli-package/src/keepercli/commands/device_management.py @@ -0,0 +1,149 @@ +import argparse +from datetime import datetime +from tkinter.constants import S +from typing import List, Optional + +from keepersdk.authentication import device_management + +from . import base +from .. import api +from ..helpers import report_utils +from ..params import KeeperParams + + +logger = api.get_logger() + + +def _format_timestamp(dt: Optional[datetime]) -> str: + if not dt: + return 'N/A' + return dt.strftime('%Y-%m-%d %H:%M:%S') + + +def _sdk_error(exc: Exception) -> base.CommandError: + return base.CommandError(str(exc)) + + +class DeviceListCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='device-list', + description='List all active devices for the current user', + parents=[base.json_output_parser] + ) + DeviceListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + try: + devices = device_management.list_user_devices(context.auth) + except ValueError as e: + raise _sdk_error(e) from e + + if not devices: + logger.info('No devices found.') + return + + fmt = kwargs.get('format') or 'table' + output = kwargs.get('output') + + headers = ['id', 'name', 'client_type', 'login_status', 'last_accessed'] + rows: List[List] = [] + for d in devices: + rows.append([ + d.list_index, + d.name, + d.client_type, + d.login_status, + _format_timestamp(d.last_accessed), + ]) + + return report_utils.dump_report_data( + rows, headers, fmt=fmt, filename=output, title=f'User Devices ({len(rows)} found)' + ) + + +class DeviceRenameCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='device-rename', + description='Rename a device for the current user', + ) + DeviceRenameCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('device', help='Device ID (from device-list) or device name substring') + parser.add_argument('new_name', help='New name for the device') + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + device_identifier = (kwargs.get('device') or '').strip() + new_name = (kwargs.get('new_name') or '').strip() + + try: + old_name, updated_name = device_management.rename_user_device( + context.auth, device_identifier, new_name + ) + logger.info("Device name updated from '%s' to '%s'", old_name, updated_name) + except ValueError as e: + raise _sdk_error(e) from e + + +class DeviceRemoveCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='device-remove', + description='Logout and remove the current user from one or more devices', + ) + DeviceRemoveCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('devices', nargs='+', help='Device ID (from device-list) or device name substring') + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + device_identifiers = kwargs.get('devices') or [] + try: + for name in device_management.remove_user_devices(context.auth, device_identifiers): + logger.info("Device '%s' successfully removed", name) + except ValueError as e: + raise _sdk_error(e) from e + + +class DeviceLogoutCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='device-logout', + description='Logout the current user from one or more devices', + ) + DeviceLogoutCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('devices', nargs='+', help='Device ID (from device-list) or device name substring') + parser.error = base.ArgparseCommand.raise_parse_exception + parser.exit = base.ArgparseCommand.suppress_exit + + def execute(self, context: KeeperParams, **kwargs): + base.require_login(context) + device_identifiers = kwargs.get('devices') or [] + try: + for name in device_management.logout_user_devices(context.auth, device_identifiers): + logger.info("Device '%s' successfully logged out", name) + except ValueError as e: + raise _sdk_error(e) from e diff --git a/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py b/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py index 7d745e67..61382748 100644 --- a/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py +++ b/keepercli-package/src/keepercli/commands/pam/discovery/__init__.py @@ -10,7 +10,7 @@ from ....commands import base from ....helpers import router_utils from ....helpers.gateway_utils import get_all_gateways -from ..pam_config import PAM_CONFIG_RECORD_TYPES +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS from keepersdk.vault import ksm_management, vault_record, vault_online from keepersdk.helpers.pam_config_facade import PamConfigurationRecordFacade @@ -72,7 +72,7 @@ def find_gateway(vault: vault_online.VaultOnline, find_func: Callable, gateways: gateways = GatewayContext.all_gateways(vault) configuration_records = list(vault.vault_data.find_records( - criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + criteria=None, record_type=PAM_CONFIGURATIONS, record_version=6)) for configuration_record in configuration_records: payload = find_func( configuration_record=configuration_record @@ -135,7 +135,7 @@ def from_gateway(vault: vault_online.VaultOnline, gateway: str, configuration_ui If there is only one gateway, then that gateway is used. """ configuration_records = list(vault.vault_data.find_records( - criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + criteria=None, record_type=PAM_CONFIGURATIONS, record_version=6)) if configuration_uid: logger.debug(f"find the gateway with configuration record {configuration_uid}") diff --git a/keepercli-package/src/keepercli/commands/pam/discovery/discover.py b/keepercli-package/src/keepercli/commands/pam/discovery/discover.py index cfca05fa..f65f2775 100644 --- a/keepercli-package/src/keepercli/commands/pam/discovery/discover.py +++ b/keepercli-package/src/keepercli/commands/pam/discovery/discover.py @@ -14,7 +14,7 @@ from .__init__ import GatewayContext, MultiConfigurationException, multi_conf_msg, PAMGatewayActionDiscoverCommandBase from ..pam_dto import GatewayAction, GatewayActionDiscoverJobStartInputs, GatewayActionDiscoverJobStart, GatewayActionDiscoverJobRemoveInputs, GatewayActionDiscoverJobRemove from .rule_commands import PAMGatewayActionDiscoverRuleAddCommand, PAMGatewayActionDiscoverRuleListCommand, PAMGatewayActionDiscoverRuleRemoveCommand, PAMGatewayActionDiscoverRuleUpdateCommand -from ..pam_config import PAM_CONFIG_RECORD_TYPES +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS from keepersdk.helpers.pam_user_record_facade import PamUserRecordFacade from keepersdk.helpers.keeper_dag.jobs import Jobs @@ -278,7 +278,7 @@ def execute(self, context: KeeperParams, **kwargs): # We are going to query the gateway for any updated status. configuration_records = list(vault.vault_data.find_records( - criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6)) + criteria=None, record_type=PAM_CONFIGURATIONS, record_version=6)) for configuration_record in configuration_records: gateway_context = GatewayContext.from_configuration_uid( diff --git a/keepercli-package/src/keepercli/commands/pam/pam_config.py b/keepercli-package/src/keepercli/commands/pam/pam_config.py index d7faf391..4bacc2e4 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_config.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_config.py @@ -23,10 +23,6 @@ logger = api.get_logger() -# PAM Configuration record types (vault record type name strings) -PAM_CONFIG_RECORD_TYPES = PAM_CONFIGURATIONS - - class PAMConfigListCommand(base.ArgparseCommand): def __init__(self): @@ -125,7 +121,7 @@ def _list_all_configurations(self, vault: vault_online.VaultOnline, is_verbose: def _load_and_validate_configuration(self, vault: vault_online.VaultOnline, config_uid: str, format_type: str): """Loads and validates a PAM configuration record.""" info = vault.vault_data.get_record(config_uid) - if not info or info.version != 6 or info.record_type not in PAM_CONFIG_RECORD_TYPES: + if not info or info.version != 6 or info.record_type not in PAM_CONFIGURATIONS: return self._handle_error(format_type, f'Configuration {config_uid} not found') configuration = vault.vault_data.load_record(config_uid) @@ -156,7 +152,7 @@ def _load_shared_folder(self, vault: vault_online.VaultOnline, folder_uid: str): def _find_pam_configurations(self, vault: vault_online.VaultOnline): """Finds all PAM configuration records.""" for record in vault.vault_data.find_records(criteria='', record_type=None, record_version=6): - if record.record_type in PAM_CONFIG_RECORD_TYPES: + if record.record_type in PAM_CONFIGURATIONS: yield record else: logger.warning(f'Following configuration has unsupported type: UID: %s, Title: %s', @@ -974,7 +970,7 @@ def execute(self, context: KeeperParams, **kwargs): 'connections', 'tunneling', 'rotation', 'recording', 'typescriptrecording', 'remotebrowserisolation' } - if all(kwargs.get(k) is None for k in target_keys): + if target_keys.isdisjoint(kwargs): admin_cred_ref = None if configuration.record_type == PamConfigurationRecordType.DOMAIN and not kwargs.get('force_domain_admin'): pam_field = configuration.get_typed_field('pamResources') @@ -997,14 +993,14 @@ def _find_configuration(self, vault: vault_online.VaultOnline, config_name: str) if not config_name: return None info = vault.vault_data.get_record(config_name) - if info and info.version == 6 and info.record_type in PAM_CONFIG_RECORD_TYPES: + if info and info.version == 6 and info.record_type in PAM_CONFIGURATIONS: loaded = vault.vault_data.load_record(config_name) if loaded and isinstance(loaded, vault_record.TypedRecord): return loaded name_lower = config_name.casefold() for record in vault.vault_data.find_records( criteria=None, - record_type=PAM_CONFIG_RECORD_TYPES, + record_type=PAM_CONFIGURATIONS, record_version=6): if record.record_uid == config_name or record.title.casefold() == name_lower: loaded = vault.vault_data.load_record(record.record_uid) @@ -1020,7 +1016,7 @@ def _validate_configuration(self, vault: vault_online.VaultOnline, configuration raise base.CommandError(f'PAM configuration "{config_name}" not found') # Storage format is on KeeperRecordInfo, not TypedRecord.version() (that method returns 3). info = vault.vault_data.get_record(configuration.record_uid) - if not info or info.version != 6 or info.record_type not in PAM_CONFIG_RECORD_TYPES: + if not info or info.version != 6 or info.record_type not in PAM_CONFIGURATIONS: raise base.CommandError(f'PAM configuration "{config_name}" not found') def _update_record_type_if_needed(self, vault: vault_online.VaultOnline, configuration: vault_record.TypedRecord, @@ -1121,12 +1117,12 @@ def _find_configuration_uid(self, vault: vault_online.VaultOnline, config_name: if not config_name: return None info = vault.vault_data.get_record(config_name) - if info and info.version == 6 and info.record_type in PAM_CONFIG_RECORD_TYPES: + if info and info.version == 6 and info.record_type in PAM_CONFIGURATIONS: return config_name name_lower = config_name.casefold() for record in vault.vault_data.find_records( criteria=None, - record_type=PAM_CONFIG_RECORD_TYPES, + record_type=PAM_CONFIGURATIONS, record_version=6): if record.record_uid == config_name or record.title.casefold() == name_lower: return record.record_uid diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py index 3388bb66..7bec570d 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rbi.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rbi.py @@ -5,6 +5,7 @@ from keepersdk import utils from keepersdk.errors import KeeperApiError from keepersdk.helpers.keeper_dag import dag_utils +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS from keepersdk.helpers.tunnel.tunnel_graph import TunnelDAG from keepersdk.helpers.tunnel.tunnel_utils import get_keeper_tokens, get_config_uid from keepersdk.vault import record_management, vault_online, vault_record @@ -15,11 +16,6 @@ from ...params import KeeperParams choices = ['on', 'off', 'default'] -_PAM_CONFIG_RECORD_TYPES = ( - 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', - 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration', -) - logger = api.get_logger() @@ -80,7 +76,7 @@ def _resolve_pam_config_record( info = vault.vault_data.get_record(config_ref) if not info: info = record_utils.try_resolve_single_record(config_ref, context) - if not info or info.version != 6 or info.record_type not in _PAM_CONFIG_RECORD_TYPES: + if not info or info.version != 6 or info.record_type not in PAM_CONFIGURATIONS: return None loaded = vault.vault_data.load_record(info.record_uid) if isinstance(loaded, vault_record.TypedRecord): diff --git a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py index f01916e4..a47bd3cc 100644 --- a/keepercli-package/src/keepercli/commands/pam/pam_rotation.py +++ b/keepercli-package/src/keepercli/commands/pam/pam_rotation.py @@ -15,7 +15,7 @@ from ... import api, prompt_utils from ...params import KeeperParams from ...helpers import gateway_utils, router_utils, report_utils, folder_utils, record_utils -from .pam_config import PAM_CONFIG_RECORD_TYPES +from keepersdk.helpers.keeper_dag.constants import PAM_CONFIGURATIONS logger = api.get_logger() @@ -896,7 +896,7 @@ def add_folders(folder: vault_types.Folder): pam_configurations = {} for x in vault.vault_data.find_records( - criteria=None, record_type=PAM_CONFIG_RECORD_TYPES, record_version=6): + criteria=None, record_type=PAM_CONFIGURATIONS, record_version=6): loaded = vault.vault_data.load_record(x.record_uid) if loaded and isinstance(loaded, vault_record.TypedRecord): pam_configurations[x.record_uid] = loaded diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index bbc0be13..20c4323f 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -13,6 +13,7 @@ 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 .commands import device_management from .biometric import BiometricCommand from .commands import account_commands, two_fa commands.register_command('server', @@ -22,6 +23,10 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS 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('device-list', device_management.DeviceListCommand(), base.CommandScope.Account) + commands.register_command('device-rename', device_management.DeviceRenameCommand(), base.CommandScope.Account) + commands.register_command('device-remove', device_management.DeviceRemoveCommand(), base.CommandScope.Account) + commands.register_command('device-logout', device_management.DeviceLogoutCommand(), base.CommandScope.Account) commands.register_command('whoami', account_commands.WhoamiCommand(), base.CommandScope.Account) commands.register_command('reset-password', account_commands.ResetPasswordCommand(), base.CommandScope.Account) commands.register_command('2fa', two_fa.TwoFaCommand(), base.CommandScope.Account) diff --git a/keepersdk-package/src/keepersdk/authentication/device_management.py b/keepersdk-package/src/keepersdk/authentication/device_management.py new file mode 100644 index 00000000..a829597f --- /dev/null +++ b/keepersdk-package/src/keepersdk/authentication/device_management.py @@ -0,0 +1,264 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' List[UserDeviceInfo]: + """Return all devices for the current user, sorted by last access (newest first).""" + devices = _fetch_devices(auth) + return [_to_user_device_info(i, d) for i, d in enumerate(devices, start=1)] + + +def rename_user_device( + auth: keeper_auth.KeeperAuth, + device_identifier: str, + new_name: str, +) -> Tuple[str, str]: + """ + Rename a device by list index (from list_user_devices) or unique name substring. + + Returns: + (old_name, new_name) on success. + + Raises: + ValueError: validation, not found, or API failure. + """ + _validate_identifier(device_identifier) + sanitized = _sanitize_device_name(new_name) + if not sanitized: + raise ValueError('Device name contains only invalid characters') + + devices = _fetch_devices(auth) + if not devices: + raise ValueError('No devices found') + + resolved = _resolve_device(devices, device_identifier) + if not resolved: + raise ValueError('No matching device found (or ambiguous device name)') + + device_token, device = resolved + old_name = device.deviceName or 'N/A' + + rq = DeviceManagement_pb2.DeviceRenameRequest() + dr = rq.deviceRename.add() + dr.encryptedDeviceToken = device_token + dr.deviceNewName = sanitized + + rs = auth.execute_auth_rest( + rest_endpoint=URL_DEVICE_USER_RENAME, + request=rq, + response_type=DeviceManagement_pb2.DeviceRenameResponse, + ) + if not rs or not rs.deviceRenameResult: + raise ValueError('No response returned from device rename') + + for r in rs.deviceRenameResult: + if r.deviceActionStatus == DeviceManagement_pb2.SUCCESS: + return old_name, sanitized + status = DeviceManagement_pb2.DeviceActionStatus.Name(r.deviceActionStatus) + raise ValueError(f'Device rename failed: {status}') + + raise ValueError('No response returned from device rename') + + +def logout_user_devices( + auth: keeper_auth.KeeperAuth, + device_identifiers: List[str], +) -> List[str]: + """ + Log out the current user from one or more devices. + + Args: + device_identifiers: List index strings ('1', '2', ...) or unique name substrings. + + Returns: + Names of devices successfully logged out. + + Raises: + ValueError: validation, not found, or API failure. + """ + return _execute_device_action(auth, device_identifiers, DeviceManagement_pb2.DA_LOGOUT) + + +def remove_user_devices( + auth: keeper_auth.KeeperAuth, + device_identifiers: List[str], +) -> List[str]: + """ + Log out and remove the current user from one or more devices. + + Returns: + Names of devices successfully removed. + + Raises: + ValueError: validation, not found, or API failure. + """ + return _execute_device_action(auth, device_identifiers, DeviceManagement_pb2.DA_REMOVE) + + +def _fetch_devices(auth: keeper_auth.KeeperAuth) -> List[DeviceManagement_pb2.Device]: + rs = auth.execute_auth_rest( + rest_endpoint=URL_DEVICE_USER_LIST, + request=None, + response_type=DeviceManagement_pb2.DeviceUserResponse, + ) + if not rs: + return [] + devices: List[DeviceManagement_pb2.Device] = [] + for group in rs.deviceGroups: + devices.extend(list(group.devices)) + devices.sort(key=lambda d: d.lastModifiedTime or 0, reverse=True) + return devices + + +def _to_user_device_info(index: int, device: DeviceManagement_pb2.Device) -> UserDeviceInfo: + return UserDeviceInfo( + list_index=index, + name=device.deviceName or 'N/A', + client_type=_client_type_name(device.clientType), + login_status=_login_state_name(device.loginState), + last_accessed=_timestamp_to_datetime(device.lastModifiedTime), + ) + + +def _validate_identifier(identifier: str) -> None: + if not identifier or not identifier.strip(): + raise ValueError('Device identifier cannot be empty') + if re.search(r'[<>"\'\x00-\x1f\x7f-\x9f]', identifier): + raise ValueError(f'Invalid device identifier: {identifier}') + + +def _sanitize_device_name(name: str) -> str: + return re.sub(r'[<>"\'\x00-\x1f\x7f-\x9f]', '', name).strip() + + +def _resolve_device( + devices: List[DeviceManagement_pb2.Device], identifier: str +) -> Optional[Tuple[bytes, DeviceManagement_pb2.Device]]: + ident = identifier.strip() + if ident.isdigit(): + idx = int(ident) + if 1 <= idx <= len(devices): + d = devices[idx - 1] + return d.encryptedDeviceToken, d + return None + ident_l = ident.lower() + matches = [d for d in devices if (d.deviceName or '').lower().find(ident_l) >= 0] + if len(matches) == 1: + d = matches[0] + return d.encryptedDeviceToken, d + return None + + +def _resolve_devices( + devices: List[DeviceManagement_pb2.Device], identifiers: List[str] +) -> List[Tuple[bytes, DeviceManagement_pb2.Device]]: + if not identifiers: + raise ValueError('At least one device identifier is required') + resolved: List[Tuple[bytes, DeviceManagement_pb2.Device]] = [] + for identifier in identifiers: + _validate_identifier(identifier) + match = _resolve_device(devices, identifier) + if not match: + raise ValueError( + f'No matching device found for "{identifier}" (or ambiguous device name)' + ) + resolved.append(match) + return resolved + + +def _execute_device_action( + auth: keeper_auth.KeeperAuth, + device_identifiers: List[str], + action_type: int, +) -> List[str]: + devices = _fetch_devices(auth) + if not devices: + raise ValueError('No devices found') + + resolved = _resolve_devices(devices, device_identifiers) + token_to_device = {token: device for token, device in resolved} + + rq = DeviceManagement_pb2.DeviceActionRequest() + device_action = rq.deviceAction.add() + device_action.deviceActionType = action_type + device_action.encryptedDeviceToken.extend(list(token_to_device.keys())) + + rs = auth.execute_auth_rest( + rest_endpoint=URL_DEVICE_USER_ACTION, + request=rq, + response_type=DeviceManagement_pb2.DeviceActionResponse, + ) + if not rs or not rs.deviceActionResult: + raise ValueError('No response returned from device action') + + succeeded: List[str] = [] + for result in rs.deviceActionResult: + for token in result.encryptedDeviceToken: + device = token_to_device.get(token) + device_name = (device.deviceName if device else None) or 'Unknown Device' + if result.deviceActionStatus == DeviceManagement_pb2.SUCCESS: + succeeded.append(device_name) + else: + status_name = DeviceManagement_pb2.DeviceActionStatus.Name( + result.deviceActionStatus + ) + if result.deviceActionStatus == DeviceManagement_pb2.NOT_ALLOWED: + msg = 'Operation not allowed' + else: + msg = f'Action failed ({status_name})' + raise ValueError(f"Device '{device_name}': {msg}") + return succeeded + + +def _timestamp_to_datetime(timestamp: Optional[int]) -> Optional[datetime]: + if not timestamp: + return None + try: + if timestamp > 10000000000: + timestamp = int(timestamp / 1000) + return datetime.fromtimestamp(timestamp) + except (ValueError, OSError, TypeError): + return None + + +def _login_state_name(login_state: int) -> str: + try: + return APIRequest_pb2.LoginState.Name(login_state) + except Exception: + return f'UNKNOWN_STATE_{login_state}' + + +def _client_type_name(client_type: int) -> str: + try: + return DeviceManagement_pb2.ClientType.Name(client_type) + except Exception: + return f'UNKNOWN_{client_type}' diff --git a/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.py b/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.py new file mode 100644 index 00000000..4de4d6d9 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: DeviceManagement.proto +# 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, + 29, + 3, + '', + 'DeviceManagement.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import APIRequest_pb2 as APIRequest__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x44\x65viceManagement.proto\x12\x10\x44\x65viceManagement\x1a\x10\x41PIRequest.proto\"\x97\x03\n\x06\x44\x65vice\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x12\n\ndeviceName\x18\x02 \x01(\t\x12\x16\n\x0e\x64\x65vicePlatform\x18\x03 \x01(\t\x12\x32\n\x0c\x64\x65viceStatus\x18\x04 \x01(\x0e\x32\x1c.Authentication.DeviceStatus\x12.\n\nloginState\x18\x05 \x01(\x0e\x32\x1a.Authentication.LoginState\x12\x15\n\rclientVersion\x18\x06 \x01(\t\x12\x30\n\nclientType\x18\x07 \x01(\x0e\x32\x1c.DeviceManagement.ClientType\x12@\n\x12\x63lientTypeCategory\x18\x08 \x01(\x0e\x32$.DeviceManagement.ClientTypeCategory\x12:\n\x10\x63lientFormFactor\x18\t \x01(\x0e\x32 .Authentication.ClientFormFactor\x12\x18\n\x10lastModifiedTime\x18\n \x01(\x03\"8\n\x0b\x44\x65viceGroup\x12)\n\x07\x64\x65vices\x18\x01 \x03(\x0b\x32\x18.DeviceManagement.Device\"I\n\x12\x44\x65viceUserResponse\x12\x33\n\x0c\x64\x65viceGroups\x18\x01 \x03(\x0b\x32\x1d.DeviceManagement.DeviceGroup\"K\n\x13\x44\x65viceActionRequest\x12\x34\n\x0c\x64\x65viceAction\x18\x01 \x03(\x0b\x32\x1e.DeviceManagement.DeviceAction\"j\n\x0c\x44\x65viceAction\x12<\n\x10\x64\x65viceActionType\x18\x01 \x01(\x0e\x32\".DeviceManagement.DeviceActionType\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x03(\x0c\"\x8d\x01\n\x14\x44\x65viceActionResponse\x12@\n\x12\x64\x65viceActionResult\x18\x01 \x03(\x0b\x32$.DeviceManagement.DeviceActionResult\x12\x33\n\x0c\x64\x65viceGroups\x18\x02 \x03(\x0b\x32\x1d.DeviceManagement.DeviceGroup\"\xb2\x01\n\x12\x44\x65viceActionResult\x12<\n\x10\x64\x65viceActionType\x18\x01 \x01(\x0e\x32\".DeviceManagement.DeviceActionType\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x02 \x03(\x0c\x12@\n\x12\x64\x65viceActionStatus\x18\x03 \x01(\x0e\x32$.DeviceManagement.DeviceActionStatus\"K\n\x13\x44\x65viceRenameRequest\x12\x34\n\x0c\x64\x65viceRename\x18\x01 \x03(\x0b\x32\x1e.DeviceManagement.DeviceRename\"C\n\x0c\x44\x65viceRename\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rdeviceNewName\x18\x02 \x01(\t\"\x8d\x01\n\x14\x44\x65viceRenameResponse\x12@\n\x12\x64\x65viceRenameResult\x18\x01 \x03(\x0b\x32$.DeviceManagement.DeviceRenameResult\x12\x33\n\x0c\x64\x65viceGroups\x18\x02 \x03(\x0b\x32\x1d.DeviceManagement.DeviceGroup\"\x8b\x01\n\x12\x44\x65viceRenameResult\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x01 \x01(\x0c\x12\x15\n\rdeviceNewName\x18\x02 \x01(\t\x12@\n\x12\x64\x65viceActionStatus\x18\x03 \x01(\x0e\x32$.DeviceManagement.DeviceActionStatus\"/\n\x12\x44\x65viceAdminRequest\x12\x19\n\x11\x65nterpriseUserIds\x18\x01 \x03(\x03\"O\n\x13\x44\x65viceAdminResponse\x12\x38\n\x0e\x64\x65viceUserList\x18\x01 \x03(\x0b\x32 .DeviceManagement.DeviceUserList\"_\n\x0e\x44\x65viceUserList\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12\x33\n\x0c\x64\x65viceGroups\x18\x02 \x03(\x0b\x32\x1d.DeviceManagement.DeviceGroup\"Z\n\x18\x44\x65viceAdminActionRequest\x12>\n\x11\x64\x65viceAdminAction\x18\x01 \x03(\x0b\x32#.DeviceManagement.DeviceAdminAction\"\x89\x01\n\x11\x44\x65viceAdminAction\x12<\n\x10\x64\x65viceActionType\x18\x01 \x01(\x0e\x32\".DeviceManagement.DeviceActionType\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x03(\x0c\"h\n\x19\x44\x65viceAdminActionResponse\x12K\n\x18\x64\x65viceAdminActionResults\x18\x01 \x03(\x0b\x32).DeviceManagement.DeviceAdminActionResult\"\xd1\x01\n\x17\x44\x65viceAdminActionResult\x12<\n\x10\x64\x65viceActionType\x18\x01 \x01(\x0e\x32\".DeviceManagement.DeviceActionType\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\x12\x1c\n\x14\x65ncryptedDeviceToken\x18\x03 \x03(\x0c\x12@\n\x12\x64\x65viceActionStatus\x18\x04 \x01(\x0e\x32$.DeviceManagement.DeviceActionStatus*\xb2\x01\n\x10\x44\x65viceActionType\x12\x0e\n\nDA_INVALID\x10\x00\x12\r\n\tDA_LOGOUT\x10\x01\x12\r\n\tDA_REMOVE\x10\x02\x12\x0b\n\x07\x44\x41_LOCK\x10\x03\x12\r\n\tDA_UNLOCK\x10\x04\x12\x1a\n\x16\x44\x41_DEVICE_ACCOUNT_LOCK\x10\x05\x12\x1c\n\x18\x44\x41_DEVICE_ACCOUNT_UNLOCK\x10\x06\x12\x0b\n\x07\x44\x41_LINK\x10\x07\x12\r\n\tDA_UNLINK\x10\x08*?\n\x12\x44\x65viceActionStatus\x12\x0b\n\x07INVALID\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\x0f\n\x0bNOT_ALLOWED\x10\x02*\xec\x05\n\nClientType\x12\x08\n\x04NONE\x10\x00\x12\x0b\n\x07\x41NDROID\x10\x01\x12\x0e\n\nBLACKBERRY\x10\x02\x12\x10\n\x0c\x42LACKBERRY10\x10\x03\x12\x0b\n\x07\x44\x45SKTOP\x10\x04\x12\x07\n\x03IOS\x10\x05\x12\x0b\n\x07MAC_APP\x10\x06\x12\x0b\n\x07WEB_APP\x10\x07\x12\x11\n\rWINDOWS_PHONE\x10\x08\x12\x0b\n\x07SURFACE\x10\t\x12\x08\n\x04WIN8\x10\n\x12\x10\n\x0cIE_EXTENSION\x10\x0b\x12\x14\n\x10\x43HROME_EXTENSION\x10\x0c\x12\x15\n\x11\x46IREFOX_EXTENSION\x10\r\x12\x14\n\x10SAFARI_EXTENSION\x10\x0e\x12\n\n\x06\x44OCOMO\x10\x0f\x12\x0b\n\x07UNKNOWN\x10\x10\x12\n\n\x06SERVER\x10\x11\x12\r\n\tCOMMANDER\x10\x12\x12\n\n\x06\x42RIDGE\x10\x13\x12!\n\x1d\x45NTERPRISE_MANAGEMENT_CONSOLE\x10\x14\x12\x12\n\x0e\x45\x44GE_EXTENSION\x10\x15\x12\x10\n\x0cSUPPORT_TOOL\x10\x16\x12\x0f\n\x0bSSO_CONNECT\x10\x17\x12\x14\n\x10\x44\x45SKTOP_ELECTRON\x10\x18\x12\x15\n\x11PASSWORD_IMPORTER\x10\x19\x12\x08\n\x04\x43HAT\x10\x1a\x12\x14\n\x10MAC_APP_ELECTRON\x10\x1b\x12\x0c\n\x08\x43HAT_IOS\x10\x1c\x12\x10\n\x0c\x43HAT_ANDROID\x10\x1d\x12\x10\n\x0c\x43HAT_WINDOWS\x10\x1e\x12\x0c\n\x08\x43HAT_MAC\x10\x1f\x12\x08\n\x04SCIM\x10 \x12\n\n\x06LAMBDA\x10!\x12\x1e\n\x1a\x43ONNECTWISE_CONTROL_HELPER\x10\"\x12\x1a\n\x16\x45NTERPRISE_CLIENT_TOOL\x10#\x12\x16\n\x12SECRETS_MANAGER_JS\x10$\x12\x1a\n\x16SECRETS_MANAGER_PYTHON\x10%\x12\x16\n\x12SECRETS_MANAGER_GO\x10&\x12\x18\n\x14SECRETS_MANAGER_JAVA\x10\'\x12\x17\n\x13SECRETS_MANAGER_NET\x10(*\xcb\x01\n\x12\x43lientTypeCategory\x12\x0c\n\x08\x43\x41T_NONE\x10\x00\x12\r\n\tCAT_ADMIN\x10\x01\x12\x0f\n\x0b\x43\x41T_DESKTOP\x10\x02\x12\x11\n\rCAT_EXTENSION\x10\x03\x12\x0e\n\nCAT_MOBILE\x10\x04\x12\r\n\tCAT_OTHER\x10\x05\x12\x11\n\rCAT_WEB_VAULT\x10\x06\x12\x14\n\x10\x43\x41T_CHAT_DESKTOP\x10\x07\x12\x13\n\x0f\x43\x41T_CHAT_MOBILE\x10\x08\x12\x17\n\x13\x43\x41T_SECRETS_MANAGER\x10\tB,\n\x18\x63om.keepersecurity.protoB\x10\x44\x65viceManagementb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'DeviceManagement_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\020DeviceManagement' + _globals['_DEVICEACTIONTYPE']._serialized_start=2325 + _globals['_DEVICEACTIONTYPE']._serialized_end=2503 + _globals['_DEVICEACTIONSTATUS']._serialized_start=2505 + _globals['_DEVICEACTIONSTATUS']._serialized_end=2568 + _globals['_CLIENTTYPE']._serialized_start=2571 + _globals['_CLIENTTYPE']._serialized_end=3319 + _globals['_CLIENTTYPECATEGORY']._serialized_start=3322 + _globals['_CLIENTTYPECATEGORY']._serialized_end=3525 + _globals['_DEVICE']._serialized_start=63 + _globals['_DEVICE']._serialized_end=470 + _globals['_DEVICEGROUP']._serialized_start=472 + _globals['_DEVICEGROUP']._serialized_end=528 + _globals['_DEVICEUSERRESPONSE']._serialized_start=530 + _globals['_DEVICEUSERRESPONSE']._serialized_end=603 + _globals['_DEVICEACTIONREQUEST']._serialized_start=605 + _globals['_DEVICEACTIONREQUEST']._serialized_end=680 + _globals['_DEVICEACTION']._serialized_start=682 + _globals['_DEVICEACTION']._serialized_end=788 + _globals['_DEVICEACTIONRESPONSE']._serialized_start=791 + _globals['_DEVICEACTIONRESPONSE']._serialized_end=932 + _globals['_DEVICEACTIONRESULT']._serialized_start=935 + _globals['_DEVICEACTIONRESULT']._serialized_end=1113 + _globals['_DEVICERENAMEREQUEST']._serialized_start=1115 + _globals['_DEVICERENAMEREQUEST']._serialized_end=1190 + _globals['_DEVICERENAME']._serialized_start=1192 + _globals['_DEVICERENAME']._serialized_end=1259 + _globals['_DEVICERENAMERESPONSE']._serialized_start=1262 + _globals['_DEVICERENAMERESPONSE']._serialized_end=1403 + _globals['_DEVICERENAMERESULT']._serialized_start=1406 + _globals['_DEVICERENAMERESULT']._serialized_end=1545 + _globals['_DEVICEADMINREQUEST']._serialized_start=1547 + _globals['_DEVICEADMINREQUEST']._serialized_end=1594 + _globals['_DEVICEADMINRESPONSE']._serialized_start=1596 + _globals['_DEVICEADMINRESPONSE']._serialized_end=1675 + _globals['_DEVICEUSERLIST']._serialized_start=1677 + _globals['_DEVICEUSERLIST']._serialized_end=1772 + _globals['_DEVICEADMINACTIONREQUEST']._serialized_start=1774 + _globals['_DEVICEADMINACTIONREQUEST']._serialized_end=1864 + _globals['_DEVICEADMINACTION']._serialized_start=1867 + _globals['_DEVICEADMINACTION']._serialized_end=2004 + _globals['_DEVICEADMINACTIONRESPONSE']._serialized_start=2006 + _globals['_DEVICEADMINACTIONRESPONSE']._serialized_end=2110 + _globals['_DEVICEADMINACTIONRESULT']._serialized_start=2113 + _globals['_DEVICEADMINACTIONRESULT']._serialized_end=2322 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.pyi b/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.pyi new file mode 100644 index 00000000..e6dab26b --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/DeviceManagement_pb2.pyi @@ -0,0 +1,300 @@ +import APIRequest_pb2 as _APIRequest_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 +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class DeviceActionType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DA_INVALID: _ClassVar[DeviceActionType] + DA_LOGOUT: _ClassVar[DeviceActionType] + DA_REMOVE: _ClassVar[DeviceActionType] + DA_LOCK: _ClassVar[DeviceActionType] + DA_UNLOCK: _ClassVar[DeviceActionType] + DA_DEVICE_ACCOUNT_LOCK: _ClassVar[DeviceActionType] + DA_DEVICE_ACCOUNT_UNLOCK: _ClassVar[DeviceActionType] + DA_LINK: _ClassVar[DeviceActionType] + DA_UNLINK: _ClassVar[DeviceActionType] + +class DeviceActionStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + INVALID: _ClassVar[DeviceActionStatus] + SUCCESS: _ClassVar[DeviceActionStatus] + NOT_ALLOWED: _ClassVar[DeviceActionStatus] + +class ClientType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + NONE: _ClassVar[ClientType] + ANDROID: _ClassVar[ClientType] + BLACKBERRY: _ClassVar[ClientType] + BLACKBERRY10: _ClassVar[ClientType] + DESKTOP: _ClassVar[ClientType] + IOS: _ClassVar[ClientType] + MAC_APP: _ClassVar[ClientType] + WEB_APP: _ClassVar[ClientType] + WINDOWS_PHONE: _ClassVar[ClientType] + SURFACE: _ClassVar[ClientType] + WIN8: _ClassVar[ClientType] + IE_EXTENSION: _ClassVar[ClientType] + CHROME_EXTENSION: _ClassVar[ClientType] + FIREFOX_EXTENSION: _ClassVar[ClientType] + SAFARI_EXTENSION: _ClassVar[ClientType] + DOCOMO: _ClassVar[ClientType] + UNKNOWN: _ClassVar[ClientType] + SERVER: _ClassVar[ClientType] + COMMANDER: _ClassVar[ClientType] + BRIDGE: _ClassVar[ClientType] + ENTERPRISE_MANAGEMENT_CONSOLE: _ClassVar[ClientType] + EDGE_EXTENSION: _ClassVar[ClientType] + SUPPORT_TOOL: _ClassVar[ClientType] + SSO_CONNECT: _ClassVar[ClientType] + DESKTOP_ELECTRON: _ClassVar[ClientType] + PASSWORD_IMPORTER: _ClassVar[ClientType] + CHAT: _ClassVar[ClientType] + MAC_APP_ELECTRON: _ClassVar[ClientType] + CHAT_IOS: _ClassVar[ClientType] + CHAT_ANDROID: _ClassVar[ClientType] + CHAT_WINDOWS: _ClassVar[ClientType] + CHAT_MAC: _ClassVar[ClientType] + SCIM: _ClassVar[ClientType] + LAMBDA: _ClassVar[ClientType] + CONNECTWISE_CONTROL_HELPER: _ClassVar[ClientType] + ENTERPRISE_CLIENT_TOOL: _ClassVar[ClientType] + SECRETS_MANAGER_JS: _ClassVar[ClientType] + SECRETS_MANAGER_PYTHON: _ClassVar[ClientType] + SECRETS_MANAGER_GO: _ClassVar[ClientType] + SECRETS_MANAGER_JAVA: _ClassVar[ClientType] + SECRETS_MANAGER_NET: _ClassVar[ClientType] + +class ClientTypeCategory(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + CAT_NONE: _ClassVar[ClientTypeCategory] + CAT_ADMIN: _ClassVar[ClientTypeCategory] + CAT_DESKTOP: _ClassVar[ClientTypeCategory] + CAT_EXTENSION: _ClassVar[ClientTypeCategory] + CAT_MOBILE: _ClassVar[ClientTypeCategory] + CAT_OTHER: _ClassVar[ClientTypeCategory] + CAT_WEB_VAULT: _ClassVar[ClientTypeCategory] + CAT_CHAT_DESKTOP: _ClassVar[ClientTypeCategory] + CAT_CHAT_MOBILE: _ClassVar[ClientTypeCategory] + CAT_SECRETS_MANAGER: _ClassVar[ClientTypeCategory] +DA_INVALID: DeviceActionType +DA_LOGOUT: DeviceActionType +DA_REMOVE: DeviceActionType +DA_LOCK: DeviceActionType +DA_UNLOCK: DeviceActionType +DA_DEVICE_ACCOUNT_LOCK: DeviceActionType +DA_DEVICE_ACCOUNT_UNLOCK: DeviceActionType +DA_LINK: DeviceActionType +DA_UNLINK: DeviceActionType +INVALID: DeviceActionStatus +SUCCESS: DeviceActionStatus +NOT_ALLOWED: DeviceActionStatus +NONE: ClientType +ANDROID: ClientType +BLACKBERRY: ClientType +BLACKBERRY10: ClientType +DESKTOP: ClientType +IOS: ClientType +MAC_APP: ClientType +WEB_APP: ClientType +WINDOWS_PHONE: ClientType +SURFACE: ClientType +WIN8: ClientType +IE_EXTENSION: ClientType +CHROME_EXTENSION: ClientType +FIREFOX_EXTENSION: ClientType +SAFARI_EXTENSION: ClientType +DOCOMO: ClientType +UNKNOWN: ClientType +SERVER: ClientType +COMMANDER: ClientType +BRIDGE: ClientType +ENTERPRISE_MANAGEMENT_CONSOLE: ClientType +EDGE_EXTENSION: ClientType +SUPPORT_TOOL: ClientType +SSO_CONNECT: ClientType +DESKTOP_ELECTRON: ClientType +PASSWORD_IMPORTER: ClientType +CHAT: ClientType +MAC_APP_ELECTRON: ClientType +CHAT_IOS: ClientType +CHAT_ANDROID: ClientType +CHAT_WINDOWS: ClientType +CHAT_MAC: ClientType +SCIM: ClientType +LAMBDA: ClientType +CONNECTWISE_CONTROL_HELPER: ClientType +ENTERPRISE_CLIENT_TOOL: ClientType +SECRETS_MANAGER_JS: ClientType +SECRETS_MANAGER_PYTHON: ClientType +SECRETS_MANAGER_GO: ClientType +SECRETS_MANAGER_JAVA: ClientType +SECRETS_MANAGER_NET: ClientType +CAT_NONE: ClientTypeCategory +CAT_ADMIN: ClientTypeCategory +CAT_DESKTOP: ClientTypeCategory +CAT_EXTENSION: ClientTypeCategory +CAT_MOBILE: ClientTypeCategory +CAT_OTHER: ClientTypeCategory +CAT_WEB_VAULT: ClientTypeCategory +CAT_CHAT_DESKTOP: ClientTypeCategory +CAT_CHAT_MOBILE: ClientTypeCategory +CAT_SECRETS_MANAGER: ClientTypeCategory + +class Device(_message.Message): + __slots__ = ("encryptedDeviceToken", "deviceName", "devicePlatform", "deviceStatus", "loginState", "clientVersion", "clientType", "clientTypeCategory", "clientFormFactor", "lastModifiedTime") + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + DEVICENAME_FIELD_NUMBER: _ClassVar[int] + DEVICEPLATFORM_FIELD_NUMBER: _ClassVar[int] + DEVICESTATUS_FIELD_NUMBER: _ClassVar[int] + LOGINSTATE_FIELD_NUMBER: _ClassVar[int] + CLIENTVERSION_FIELD_NUMBER: _ClassVar[int] + CLIENTTYPE_FIELD_NUMBER: _ClassVar[int] + CLIENTTYPECATEGORY_FIELD_NUMBER: _ClassVar[int] + CLIENTFORMFACTOR_FIELD_NUMBER: _ClassVar[int] + LASTMODIFIEDTIME_FIELD_NUMBER: _ClassVar[int] + encryptedDeviceToken: bytes + deviceName: str + devicePlatform: str + deviceStatus: _APIRequest_pb2.DeviceStatus + loginState: _APIRequest_pb2.LoginState + clientVersion: str + clientType: ClientType + clientTypeCategory: ClientTypeCategory + clientFormFactor: _APIRequest_pb2.ClientFormFactor + lastModifiedTime: int + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., deviceName: _Optional[str] = ..., devicePlatform: _Optional[str] = ..., deviceStatus: _Optional[_Union[_APIRequest_pb2.DeviceStatus, str]] = ..., loginState: _Optional[_Union[_APIRequest_pb2.LoginState, str]] = ..., clientVersion: _Optional[str] = ..., clientType: _Optional[_Union[ClientType, str]] = ..., clientTypeCategory: _Optional[_Union[ClientTypeCategory, str]] = ..., clientFormFactor: _Optional[_Union[_APIRequest_pb2.ClientFormFactor, str]] = ..., lastModifiedTime: _Optional[int] = ...) -> None: ... + +class DeviceGroup(_message.Message): + __slots__ = ("devices",) + DEVICES_FIELD_NUMBER: _ClassVar[int] + devices: _containers.RepeatedCompositeFieldContainer[Device] + def __init__(self, devices: _Optional[_Iterable[_Union[Device, _Mapping]]] = ...) -> None: ... + +class DeviceUserResponse(_message.Message): + __slots__ = ("deviceGroups",) + DEVICEGROUPS_FIELD_NUMBER: _ClassVar[int] + deviceGroups: _containers.RepeatedCompositeFieldContainer[DeviceGroup] + def __init__(self, deviceGroups: _Optional[_Iterable[_Union[DeviceGroup, _Mapping]]] = ...) -> None: ... + +class DeviceActionRequest(_message.Message): + __slots__ = ("deviceAction",) + DEVICEACTION_FIELD_NUMBER: _ClassVar[int] + deviceAction: _containers.RepeatedCompositeFieldContainer[DeviceAction] + def __init__(self, deviceAction: _Optional[_Iterable[_Union[DeviceAction, _Mapping]]] = ...) -> None: ... + +class DeviceAction(_message.Message): + __slots__ = ("deviceActionType", "encryptedDeviceToken") + DEVICEACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + deviceActionType: DeviceActionType + encryptedDeviceToken: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, deviceActionType: _Optional[_Union[DeviceActionType, str]] = ..., encryptedDeviceToken: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class DeviceActionResponse(_message.Message): + __slots__ = ("deviceActionResult", "deviceGroups") + DEVICEACTIONRESULT_FIELD_NUMBER: _ClassVar[int] + DEVICEGROUPS_FIELD_NUMBER: _ClassVar[int] + deviceActionResult: _containers.RepeatedCompositeFieldContainer[DeviceActionResult] + deviceGroups: _containers.RepeatedCompositeFieldContainer[DeviceGroup] + def __init__(self, deviceActionResult: _Optional[_Iterable[_Union[DeviceActionResult, _Mapping]]] = ..., deviceGroups: _Optional[_Iterable[_Union[DeviceGroup, _Mapping]]] = ...) -> None: ... + +class DeviceActionResult(_message.Message): + __slots__ = ("deviceActionType", "encryptedDeviceToken", "deviceActionStatus") + DEVICEACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + DEVICEACTIONSTATUS_FIELD_NUMBER: _ClassVar[int] + deviceActionType: DeviceActionType + encryptedDeviceToken: _containers.RepeatedScalarFieldContainer[bytes] + deviceActionStatus: DeviceActionStatus + def __init__(self, deviceActionType: _Optional[_Union[DeviceActionType, str]] = ..., encryptedDeviceToken: _Optional[_Iterable[bytes]] = ..., deviceActionStatus: _Optional[_Union[DeviceActionStatus, str]] = ...) -> None: ... + +class DeviceRenameRequest(_message.Message): + __slots__ = ("deviceRename",) + DEVICERENAME_FIELD_NUMBER: _ClassVar[int] + deviceRename: _containers.RepeatedCompositeFieldContainer[DeviceRename] + def __init__(self, deviceRename: _Optional[_Iterable[_Union[DeviceRename, _Mapping]]] = ...) -> None: ... + +class DeviceRename(_message.Message): + __slots__ = ("encryptedDeviceToken", "deviceNewName") + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + DEVICENEWNAME_FIELD_NUMBER: _ClassVar[int] + encryptedDeviceToken: bytes + deviceNewName: str + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., deviceNewName: _Optional[str] = ...) -> None: ... + +class DeviceRenameResponse(_message.Message): + __slots__ = ("deviceRenameResult", "deviceGroups") + DEVICERENAMERESULT_FIELD_NUMBER: _ClassVar[int] + DEVICEGROUPS_FIELD_NUMBER: _ClassVar[int] + deviceRenameResult: _containers.RepeatedCompositeFieldContainer[DeviceRenameResult] + deviceGroups: _containers.RepeatedCompositeFieldContainer[DeviceGroup] + def __init__(self, deviceRenameResult: _Optional[_Iterable[_Union[DeviceRenameResult, _Mapping]]] = ..., deviceGroups: _Optional[_Iterable[_Union[DeviceGroup, _Mapping]]] = ...) -> None: ... + +class DeviceRenameResult(_message.Message): + __slots__ = ("encryptedDeviceToken", "deviceNewName", "deviceActionStatus") + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + DEVICENEWNAME_FIELD_NUMBER: _ClassVar[int] + DEVICEACTIONSTATUS_FIELD_NUMBER: _ClassVar[int] + encryptedDeviceToken: bytes + deviceNewName: str + deviceActionStatus: DeviceActionStatus + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., deviceNewName: _Optional[str] = ..., deviceActionStatus: _Optional[_Union[DeviceActionStatus, str]] = ...) -> None: ... + +class DeviceAdminRequest(_message.Message): + __slots__ = ("enterpriseUserIds",) + ENTERPRISEUSERIDS_FIELD_NUMBER: _ClassVar[int] + enterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, enterpriseUserIds: _Optional[_Iterable[int]] = ...) -> None: ... + +class DeviceAdminResponse(_message.Message): + __slots__ = ("deviceUserList",) + DEVICEUSERLIST_FIELD_NUMBER: _ClassVar[int] + deviceUserList: _containers.RepeatedCompositeFieldContainer[DeviceUserList] + def __init__(self, deviceUserList: _Optional[_Iterable[_Union[DeviceUserList, _Mapping]]] = ...) -> None: ... + +class DeviceUserList(_message.Message): + __slots__ = ("enterpriseUserId", "deviceGroups") + ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] + DEVICEGROUPS_FIELD_NUMBER: _ClassVar[int] + enterpriseUserId: int + deviceGroups: _containers.RepeatedCompositeFieldContainer[DeviceGroup] + def __init__(self, enterpriseUserId: _Optional[int] = ..., deviceGroups: _Optional[_Iterable[_Union[DeviceGroup, _Mapping]]] = ...) -> None: ... + +class DeviceAdminActionRequest(_message.Message): + __slots__ = ("deviceAdminAction",) + DEVICEADMINACTION_FIELD_NUMBER: _ClassVar[int] + deviceAdminAction: _containers.RepeatedCompositeFieldContainer[DeviceAdminAction] + def __init__(self, deviceAdminAction: _Optional[_Iterable[_Union[DeviceAdminAction, _Mapping]]] = ...) -> None: ... + +class DeviceAdminAction(_message.Message): + __slots__ = ("deviceActionType", "enterpriseUserId", "encryptedDeviceToken") + DEVICEACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + deviceActionType: DeviceActionType + enterpriseUserId: int + encryptedDeviceToken: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, deviceActionType: _Optional[_Union[DeviceActionType, str]] = ..., enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class DeviceAdminActionResponse(_message.Message): + __slots__ = ("deviceAdminActionResults",) + DEVICEADMINACTIONRESULTS_FIELD_NUMBER: _ClassVar[int] + deviceAdminActionResults: _containers.RepeatedCompositeFieldContainer[DeviceAdminActionResult] + def __init__(self, deviceAdminActionResults: _Optional[_Iterable[_Union[DeviceAdminActionResult, _Mapping]]] = ...) -> None: ... + +class DeviceAdminActionResult(_message.Message): + __slots__ = ("deviceActionType", "enterpriseUserId", "encryptedDeviceToken", "deviceActionStatus") + DEVICEACTIONTYPE_FIELD_NUMBER: _ClassVar[int] + ENTERPRISEUSERID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDDEVICETOKEN_FIELD_NUMBER: _ClassVar[int] + DEVICEACTIONSTATUS_FIELD_NUMBER: _ClassVar[int] + deviceActionType: DeviceActionType + enterpriseUserId: int + encryptedDeviceToken: _containers.RepeatedScalarFieldContainer[bytes] + deviceActionStatus: DeviceActionStatus + def __init__(self, deviceActionType: _Optional[_Union[DeviceActionType, str]] = ..., enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[_Iterable[bytes]] = ..., deviceActionStatus: _Optional[_Union[DeviceActionStatus, str]] = ...) -> None: ... diff --git a/keepersdk-package/unit_tests/test_device_management.py b/keepersdk-package/unit_tests/test_device_management.py new file mode 100644 index 00000000..3e877240 --- /dev/null +++ b/keepersdk-package/unit_tests/test_device_management.py @@ -0,0 +1,80 @@ +import unittest +from unittest.mock import MagicMock + +from keepersdk.authentication import device_management +from keepersdk.proto import DeviceManagement_pb2 + + +def _device(name: str, last_modified: int = 0) -> DeviceManagement_pb2.Device: + d = DeviceManagement_pb2.Device() + d.deviceName = name + d.lastModifiedTime = last_modified + d.clientType = DeviceManagement_pb2.COMMANDER + d.loginState = 0 + d.encryptedDeviceToken = b'\x01\x02' + return d + + +class DeviceManagementSdkTests(unittest.TestCase): + def test_list_user_devices(self): + auth = MagicMock() + rs = DeviceManagement_pb2.DeviceUserResponse() + g = rs.deviceGroups.add() + g.devices.append(_device('A', 100)) + g.devices.append(_device('B', 200)) + auth.execute_auth_rest.return_value = rs + + devices = device_management.list_user_devices(auth) + self.assertEqual(len(devices), 2) + self.assertEqual(devices[0].name, 'B') + self.assertEqual(devices[0].list_index, 1) + self.assertEqual(devices[1].name, 'A') + + def test_logout_user_devices(self): + auth = MagicMock() + list_rs = DeviceManagement_pb2.DeviceUserResponse() + g = list_rs.deviceGroups.add() + g.devices.append(_device('Laptop', 100)) + + action_rs = DeviceManagement_pb2.DeviceActionResponse() + ar = action_rs.deviceActionResult.add() + ar.deviceActionStatus = DeviceManagement_pb2.SUCCESS + ar.encryptedDeviceToken.append(b'\x01\x02') + + auth.execute_auth_rest.side_effect = [list_rs, action_rs] + + names = device_management.logout_user_devices(auth, ['1']) + self.assertEqual(names, ['Laptop']) + self.assertEqual(auth.execute_auth_rest.call_count, 2) + action_call = auth.execute_auth_rest.call_args_list[1] + self.assertEqual(action_call.kwargs.get('rest_endpoint'), 'dm/device_user_action') + request = action_call.kwargs.get('request') + self.assertEqual( + request.deviceAction[0].deviceActionType, + DeviceManagement_pb2.DA_LOGOUT, + ) + + def test_remove_user_devices(self): + auth = MagicMock() + list_rs = DeviceManagement_pb2.DeviceUserResponse() + g = list_rs.deviceGroups.add() + g.devices.append(_device('Phone', 50)) + + action_rs = DeviceManagement_pb2.DeviceActionResponse() + ar = action_rs.deviceActionResult.add() + ar.deviceActionStatus = DeviceManagement_pb2.SUCCESS + ar.encryptedDeviceToken.append(b'\x01\x02') + + auth.execute_auth_rest.side_effect = [list_rs, action_rs] + + names = device_management.remove_user_devices(auth, ['1']) + self.assertEqual(names, ['Phone']) + request = auth.execute_auth_rest.call_args_list[1].kwargs.get('request') + self.assertEqual( + request.deviceAction[0].deviceActionType, + DeviceManagement_pb2.DA_REMOVE, + ) + + +if __name__ == '__main__': + unittest.main() From f5d6e559775da77b1a8d60000f519ea72263ebd2 Mon Sep 17 00:00:00 2001 From: mtyagi-ks Date: Wed, 3 Jun 2026 14:11:04 +0530 Subject: [PATCH 19/23] Add MSP command examples for SDK --- .../msp/login_to_managed_company.py | 567 +++++++++++++++++ examples/sdk_examples/msp/msp_add.py | 571 +++++++++++++++++ .../sdk_examples/msp/msp_billing_report.py | 578 +++++++++++++++++ examples/sdk_examples/msp/msp_convert_node.py | 577 +++++++++++++++++ examples/sdk_examples/msp/msp_copy_role.py | 546 ++++++++++++++++ examples/sdk_examples/msp/msp_down.py | 540 ++++++++++++++++ examples/sdk_examples/msp/msp_info.py | 595 ++++++++++++++++++ .../sdk_examples/msp/msp_legacy_report.py | 579 +++++++++++++++++ examples/sdk_examples/msp/msp_remove.py | 541 ++++++++++++++++ examples/sdk_examples/msp/msp_update.py | 579 +++++++++++++++++ .../secrets_manager/create_app.py | 32 +- .../sdk_examples/secrets_manager/get_app.py | 78 ++- 12 files changed, 5722 insertions(+), 61 deletions(-) create mode 100644 examples/sdk_examples/msp/login_to_managed_company.py create mode 100644 examples/sdk_examples/msp/msp_add.py create mode 100644 examples/sdk_examples/msp/msp_billing_report.py create mode 100644 examples/sdk_examples/msp/msp_convert_node.py create mode 100644 examples/sdk_examples/msp/msp_copy_role.py create mode 100644 examples/sdk_examples/msp/msp_down.py create mode 100644 examples/sdk_examples/msp/msp_info.py create mode 100644 examples/sdk_examples/msp/msp_legacy_report.py create mode 100644 examples/sdk_examples/msp/msp_remove.py create mode 100644 examples/sdk_examples/msp/msp_update.py diff --git a/examples/sdk_examples/msp/login_to_managed_company.py b/examples/sdk_examples/msp/login_to_managed_company.py new file mode 100644 index 00000000..426847a8 --- /dev/null +++ b/examples/sdk_examples/msp/login_to_managed_company.py @@ -0,0 +1,567 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def login_to_managed_company_example(loader, managed_company_id: int): + """Login to a managed company and switch back to MSP context.""" + mc_auth = None + mc_loader = None + try: + msp_auth.msp_down(loader, reset=False) + print(f'Logging into managed company {managed_company_id}...') + mc_auth, mc_tree_key = msp_auth.login_to_managed_company(loader, managed_company_id) + + conn = sqlite3.Connection('file::memory:', uri=True) + mc_storage = sqlite_enterprise_storage.SqliteEnterpriseStorage( + lambda: conn, managed_company_id + ) + mc_loader = enterprise_loader.EnterpriseLoader(mc_auth, mc_storage, tree_key=mc_tree_key) + mc_loader.load() + mc_name = mc_loader.enterprise_data.enterprise_info.enterprise_name + print(f'Connected to managed company: {mc_name}') + + print('Switching back to MSP context...') + msp_auth.switch_to_msp(loader) + print('MSP enterprise data refreshed.') + finally: + if mc_loader is not None: + mc_loader.close() + if mc_auth is not None: + mc_auth.close() + + +def main(): + """Main function to orchestrate login and managed-company context switch.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + managed_company_id = 0 + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + if not managed_company_id: + print('Set managed_company_id (from msp_info.py) before running.') + close_msp_session(loader, keeper_auth_context) + return + + try: + login_to_managed_company_example(loader, managed_company_id) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_add.py b/examples/sdk_examples/msp/msp_add.py new file mode 100644 index 00000000..8628bbdf --- /dev/null +++ b/examples/sdk_examples/msp/msp_add.py @@ -0,0 +1,571 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def add_msp_managed_company( + loader, + mc_name: str, + plan: str, + seats=None, + file_plan=None, + addons=None, + node_id=None, +): + """Register a new managed company using msp_auth.msp_add_managed_company.""" + msp_auth.msp_down(loader, reset=False) + root_node_id = loader.enterprise_data.root_node.node_id + mc_id = msp_auth.msp_add_managed_company( + loader, + enterprise_name=mc_name, + plan=plan, + node_id=node_id or root_node_id, + seats=seats, + file_plan=file_plan, + addons=addons, + ) + print(f'Created managed company "{mc_name}" (enterprise id={mc_id}).') + + +def main(): + """Main function to orchestrate login and add an MSP managed company.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + mc_name = '' + plan = 'business' # example plans: 'business', 'business-plus', 'enterprise', 'enterprise-plus' + seats = 10 # number of seats to allocate to the managed company (-1 for unlimited) + file_plan = None + addons = None + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + add_msp_managed_company( + loader, + mc_name=mc_name, + plan=plan, + seats=seats, + file_plan=file_plan, + addons=addons, + node_id=None, # None to create under root node, or specify a node_id to create under that node + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_billing_report.py b/examples/sdk_examples/msp/msp_billing_report.py new file mode 100644 index 00000000..690d862d --- /dev/null +++ b/examples/sdk_examples/msp/msp_billing_report.py @@ -0,0 +1,578 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _print_table(headers, rows): + if not headers: + return + widths = [len(str(h)) for h in headers] + str_rows = [] + for row in rows: + cells = [str(c) for c in row] + str_rows.append(cells) + for i, cell in enumerate(cells): + if i < len(widths): + widths[i] = max(widths[i], len(cell)) + fmt = ' '.join(f'{{:{w}}}' for w in widths) + print(fmt.format(*[str(h) for h in headers])) + print(fmt.format(*['-' * w for w in widths])) + for cells in str_rows: + padded = cells + [''] * (len(headers) - len(cells)) + print(fmt.format(*padded[: len(headers)])) + + +def print_msp_billing_report(report): + print(report.title) + _print_table(list(report.headers), report.rows) + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def run_msp_billing_report(loader, month=None, show_date=False, show_company=False): + """MSP billing report using msp_auth.msp_billing_report.""" + msp_auth.msp_down(loader, reset=False) + report = msp_auth.msp_billing_report( + loader, + month=month, + show_date=show_date, + show_company=show_company, + ) + print_msp_billing_report(report) + + +def main(): + """Main function to orchestrate login and run MSP billing report.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + month = None # YYYY-MM + show_date = False # Breakdown report by date + show_company = False # Breakdown report by managed company + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + run_msp_billing_report( + loader, + month=month, + show_date=show_date, + show_company=show_company, + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_convert_node.py b/examples/sdk_examples/msp/msp_convert_node.py new file mode 100644 index 00000000..6c6851f8 --- /dev/null +++ b/examples/sdk_examples/msp/msp_convert_node.py @@ -0,0 +1,577 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _resolve_node_id(loader, node_arg): + enterprise_data = loader.enterprise_data + if node_arg.isdigit(): + node = enterprise_data.nodes.get_entity(int(node_arg)) + if node is None: + raise ValueError(f'Node id {node_arg} not found') + return node.node_id + key = node_arg.lower() + for node in enterprise_data.nodes.get_all_entities(): + if node.name and node.name.lower() == key: + return node.node_id + root = enterprise_data.root_node + if key == 'root': + return root.node_id + raise ValueError(f'Node "{node_arg}" not found') + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def convert_enterprise_node_to_managed_company( + loader, + node_name_or_id: str, + seats=None, + plan=None, +): + """Convert an enterprise subtree to a managed company using msp_auth.msp_convert_node.""" + msp_auth.msp_down(loader, reset=False) + node_id = _resolve_node_id(loader, node_name_or_id) + mc_id = msp_auth.msp_convert_node( + loader, + node_id=node_id, + seats=seats, + plan=plan, + ) + print(f'Converted node {node_name_or_id} to managed company id={mc_id}.') + + +def main(): + """Main function to orchestrate login and convert a node to a managed company.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + node_name_or_id = 'root' # Node name or node ID (subtree root) + seats = None + plan = None + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + convert_enterprise_node_to_managed_company( + loader, + node_name_or_id=node_name_or_id, + seats=seats, + plan=plan, + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_copy_role.py b/examples/sdk_examples/msp/msp_copy_role.py new file mode 100644 index 00000000..a76fcf19 --- /dev/null +++ b/examples/sdk_examples/msp/msp_copy_role.py @@ -0,0 +1,546 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def copy_msp_roles_to_managed_companies(loader, roles, managed_companies): + """Copy MSP roles to managed companies using msp_auth.msp_copy_role.""" + msp_auth.msp_down(loader, reset=False) + synced = msp_auth.msp_copy_role( + loader, + roles=roles, + managed_companies=managed_companies, + ) + print(f'Roles synced to managed company id(s): {sorted(synced)}') + + +def main(): + """Main function to orchestrate login and copy MSP roles.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + roles = [''] # Role name or role ID (can be repeated) + managed_companies = [''] # Managed company identifier(s): name or id (can be repeated) + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + copy_msp_roles_to_managed_companies(loader, roles, managed_companies) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_down.py b/examples/sdk_examples/msp/msp_down.py new file mode 100644 index 00000000..321496a2 --- /dev/null +++ b/examples/sdk_examples/msp/msp_down.py @@ -0,0 +1,540 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def sync_msp_enterprise_data(loader, reset: bool = False): + """Refresh MSP enterprise data using msp_auth.msp_down.""" + touched = msp_auth.msp_down(loader, reset=reset) + print(f'MSP data synced ({len(touched)} entity type(s) updated).') + + +def main(): + """Main function to orchestrate login and sync MSP enterprise data.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + reset = False + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + sync_msp_enterprise_data(loader, reset=reset) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_info.py b/examples/sdk_examples/msp/msp_info.py new file mode 100644 index 00000000..8bba9928 --- /dev/null +++ b/examples/sdk_examples/msp/msp_info.py @@ -0,0 +1,595 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _print_table(headers, rows): + if not headers: + return + widths = [len(str(h)) for h in headers] + str_rows = [] + for row in rows: + cells = [str(c) for c in row] + str_rows.append(cells) + for i, cell in enumerate(cells): + if i < len(widths): + widths[i] = max(widths[i], len(cell)) + fmt = ' '.join(f'{{:{w}}}' for w in widths) + print(fmt.format(*[str(h) for h in headers])) + print(fmt.format(*['-' * w for w in widths])) + for cells in str_rows: + padded = cells + [''] * (len(headers) - len(cells)) + print(fmt.format(*padded[: len(headers)])) + + +def print_msp_info_report(report): + if report.message: + print(report.message) + return + headers = list(report.headers) + rows = list(report.rows) + if report.row_numbers: + if not headers or headers[0].lower() != '#': + headers = ['#'] + headers + rows = [tuple([i, *row]) for i, row in enumerate(rows, 1)] + _print_table(headers, tuple(rows)) + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def list_msp_managed_companies( + loader, + managed_company=None, + show_pricing=False, + show_restriction=False, + verbose=False, +): + """List managed companies using msp_auth.msp_info.""" + msp_auth.msp_down(loader, reset=False) + report = msp_auth.msp_info( + loader, + restriction=show_restriction, + pricing=show_pricing, + managed_company=managed_company, + verbose=verbose, + ) + print_msp_info_report(report) + + +def main(): + """Main function to orchestrate login and list MSP managed companies.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + managed_company = None # Managed company identifier: name or id + show_pricing = False + show_restriction = False + verbose = False + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + list_msp_managed_companies( + loader, + managed_company=managed_company, + show_pricing=show_pricing, + show_restriction=show_restriction, + verbose=verbose, + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_legacy_report.py b/examples/sdk_examples/msp/msp_legacy_report.py new file mode 100644 index 00000000..71eade51 --- /dev/null +++ b/examples/sdk_examples/msp/msp_legacy_report.py @@ -0,0 +1,579 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def _print_table(headers, rows): + if not headers: + return + widths = [len(str(h)) for h in headers] + str_rows = [] + for row in rows: + cells = [str(c) for c in row] + str_rows.append(cells) + for i, cell in enumerate(cells): + if i < len(widths): + widths[i] = max(widths[i], len(cell)) + fmt = ' '.join(f'{{:{w}}}' for w in widths) + print(fmt.format(*[str(h) for h in headers])) + print(fmt.format(*['-' * w for w in widths])) + for cells in str_rows: + padded = cells + [''] * (len(headers) - len(cells)) + print(fmt.format(*padded[: len(headers)])) + + +def print_msp_legacy_report(report): + if report.title: + print(report.title) + _print_table(list(report.headers), report.rows) + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def run_msp_legacy_report(loader, range_name='last_30_days', from_date=None, to_date=None): + """Legacy MSP billing report using msp_auth.msp_legacy_report.""" + msp_auth.msp_down(loader, reset=False) + report = msp_auth.msp_legacy_report( + loader, + range_name=range_name, + from_date=from_date, + to_date=to_date, + ) + print_msp_legacy_report(report) + + +def main(): + """Main function to orchestrate login and run MSP legacy billing report.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + range_name = 'last_30_days' # Range name: last_30_days, last_60_days, last_90_days, last_180_days, last_365_days + from_date = None # From date: YYYY-MM-DD + to_date = None # To date: YYYY-MM-DD + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + run_msp_legacy_report( + loader, + range_name=range_name, + from_date=from_date, + to_date=to_date, + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_remove.py b/examples/sdk_examples/msp/msp_remove.py new file mode 100644 index 00000000..da3624cd --- /dev/null +++ b/examples/sdk_examples/msp/msp_remove.py @@ -0,0 +1,541 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def remove_msp_managed_company(loader, managed_company: str): + """Remove a managed company using msp_auth.msp_remove_managed_company.""" + msp_auth.msp_down(loader, reset=False) + eid = msp_auth.msp_remove_managed_company(loader, managed_company=managed_company) + print(f'Removed managed company id={eid}.') + + +def main(): + """Main function to orchestrate login and remove an MSP managed company.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + managed_company = '' + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + remove_msp_managed_company(loader, managed_company) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/msp/msp_update.py b/examples/sdk_examples/msp/msp_update.py new file mode 100644 index 00000000..b3513ea5 --- /dev/null +++ b/examples/sdk_examples/msp/msp_update.py @@ -0,0 +1,579 @@ +import getpass +import sqlite3 +import json +import logging +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.enterprise import enterprise_loader, msp_auth, sqlite_enterprise_storage + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_msp_enterprise_loader(keeper_auth_context: keeper_auth.KeeperAuth): + """Open in-memory enterprise storage for MSP operations (enterprise admin required).""" + if not keeper_auth_context.auth_context.is_enterprise_admin: + print('ERROR: MSP examples require an enterprise administrator account.') + keeper_auth_context.close() + return None + conn = sqlite3.Connection('file::memory:', uri=True) + enterprise_id = keeper_auth_context.auth_context.enterprise_id or 0 + storage = sqlite_enterprise_storage.SqliteEnterpriseStorage(lambda: conn, enterprise_id) + return enterprise_loader.EnterpriseLoader(keeper_auth_context, storage) + + +def close_msp_session(loader, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + if loader is not None: + loader.close() + keeper_auth_context.close() + +def update_msp_managed_company( + loader, + managed_company: str, + new_name=None, + plan=None, + seats=None, + file_plan=None, + add_addons=None, + remove_addons=None, + node_id: Optional[int] = None, +): + """Update a managed company using msp_auth.msp_update_managed_company.""" + msp_auth.msp_down(loader, reset=False) + eid = msp_auth.msp_update_managed_company( + loader, + managed_company=managed_company, + node_id=node_id or None, + new_name=new_name, + plan=plan, + seats=seats, + file_plan=file_plan, + add_addons=add_addons, + remove_addons=remove_addons, + ) + print(f'Updated managed company id={eid}.') + + +def main(): + """Main function to orchestrate login and update an MSP managed company.""" + keeper_auth_context, _ = login() + if not keeper_auth_context: + return + + # Fill in your values here. + managed_company = '' + node_id = None # Node ID: name or id + new_name = None + plan = None + seats = None + file_plan = None + add_addons = None + remove_addons = None + + loader = open_msp_enterprise_loader(keeper_auth_context) + if not loader: + return + + try: + update_msp_managed_company( + loader, + managed_company=managed_company, + node_id=node_id, + new_name=new_name, + plan=plan, + seats=seats, + file_plan=file_plan, + add_addons=add_addons, + remove_addons=remove_addons, + ) + finally: + close_msp_session(loader, keeper_auth_context) + + +if __name__ == '__main__': + main() diff --git a/examples/sdk_examples/secrets_manager/create_app.py b/examples/sdk_examples/secrets_manager/create_app.py index a1cf25dc..05482a21 100644 --- a/examples/sdk_examples/secrets_manager/create_app.py +++ b/examples/sdk_examples/secrets_manager/create_app.py @@ -505,27 +505,19 @@ def create_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAu vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) try: - app_name = input('Enter name for new Secrets Manager application: ').strip() - - if not app_name: - print("Application name cannot be empty") - else: - force_add = input('Allow duplicate names? (y/n): ').strip().lower() == 'y' - - print(f"\nCreating Secrets Manager application: {app_name}") - - app_uid = ksm_management.create_secrets_manager_app(vault, app_name, force_add=force_add) - - print(f"\n✓ Secrets Manager application created successfully!") - print(f"Application Name: {app_name}") - print(f"Application UID: {app_uid}") - print("\nNext steps:") - print(" 1. Share records or folders with this application") - print(" 2. Generate client devices for access") - print(" 3. Use the application in your integrations") + app_name = "" + force_add = False + print(f"\nCreating Secrets Manager application: {app_name}") + app_uid = ksm_management.create_secrets_manager_app(vault, app_name, force_add=force_add) - vault.sync_down() - + print(f"\n✓ Secrets Manager application created successfully!") + print(f"Application Name: {app_name}") + print(f"Application UID: {app_uid}") + print("\nNext steps:") + print(" 1. Share records or folders with this application") + print(" 2. Generate client devices for access") + print(" 3. Use the application in your integrations") + except ValueError as e: print(f"Error: {e}") except Exception as e: diff --git a/examples/sdk_examples/secrets_manager/get_app.py b/examples/sdk_examples/secrets_manager/get_app.py index e75d99ed..5b77c7f1 100644 --- a/examples/sdk_examples/secrets_manager/get_app.py +++ b/examples/sdk_examples/secrets_manager/get_app.py @@ -506,53 +506,49 @@ def get_secrets_manager_application(keeper_auth_context: keeper_auth.KeeperAuth) vault.sync_down() try: - app_search = input('Enter application name or UID: ').strip() - - if not app_search: - print("Application identifier cannot be empty") - else: - app = ksm_management.get_secrets_manager_app(vault, app_search) + app_uid_or_name = "" + app = ksm_management.get_secrets_manager_app(vault, app_uid_or_name) - print(f"\nSecrets Manager Application Details") - print("=" * 100) - print(f"App Name: {app.name}") - print(f"App UID: {app.uid}") - print(f"Records Shared: {app.records}") - print(f"Folders Shared: {app.folders}") - print(f"Client Devices: {app.count}") + print(f"\nSecrets Manager Application Details") + print("=" * 100) + print(f"App Name: {app.name}") + print(f"App UID: {app.uid}") + print(f"Records Shared: {app.records}") + print(f"Folders Shared: {app.folders}") + print(f"Client Devices: {app.count}") + + if app.client_devices: + print(f"\nClient Devices ({len(app.client_devices)}):") + print("-" * 100) + print(f"{'Name':<25} {'Short ID':<15} {'Created':<20} {'Last Access':<20} {'IP Address':<20}") + print("-" * 100) - if app.client_devices: - print(f"\nClient Devices ({len(app.client_devices)}):") - print("-" * 100) - print(f"{'Name':<25} {'Short ID':<15} {'Created':<20} {'Last Access':<20} {'IP Address':<20}") - print("-" * 100) + for client in app.client_devices: + name = client.name[:24] if client.name else 'N/A' + short_id = client.short_id[:14] if client.short_id else 'N/A' + created = client.created_on.strftime('%Y-%m-%d %H:%M') if client.created_on else 'N/A' + last_access = client.last_access.strftime('%Y-%m-%d %H:%M') if client.last_access else 'Never' + ip_address = client.ip_address[:19] if client.ip_address else 'N/A' - for client in app.client_devices: - name = client.name[:24] if client.name else 'N/A' - short_id = client.short_id[:14] if client.short_id else 'N/A' - created = client.created_on.strftime('%Y-%m-%d %H:%M') if client.created_on else 'N/A' - last_access = client.last_access.strftime('%Y-%m-%d %H:%M') if client.last_access else 'Never' - ip_address = client.ip_address[:19] if client.ip_address else 'N/A' - - print(f"{name:<25} {short_id:<15} {created:<20} {last_access:<20} {ip_address:<20}") + print(f"{name:<25} {short_id:<15} {created:<20} {last_access:<20} {ip_address:<20}") + + if app.shared_secrets: + print(f"\nShared Secrets ({len(app.shared_secrets)}):") + print("-" * 100) + print(f"{'Type':<15} {'Name':<45} {'UID':<40}") + print("-" * 100) - if app.shared_secrets: - print(f"\nShared Secrets ({len(app.shared_secrets)}):") - print("-" * 100) - print(f"{'Type':<15} {'Name':<45} {'UID':<40}") - print("-" * 100) - - for secret in app.shared_secrets[:20]: - secret_type = secret.type[:14] if secret.type else 'N/A' - secret_name = secret.name[:44] if secret.name else 'N/A' - secret_uid = secret.uid[:39] if secret.uid else 'N/A' - - print(f"{secret_type:<15} {secret_name:<45} {secret_uid:<40}") + for secret in app.shared_secrets[:20]: + secret_type = secret.type[:14] if secret.type else 'N/A' + secret_name = secret.name[:44] if secret.name else 'N/A' + secret_uid = secret.uid[:39] if secret.uid else 'N/A' - if len(app.shared_secrets) > 20: - print(f" ... and {len(app.shared_secrets) - 20} more") + print(f"{secret_type:<15} {secret_name:<45} {secret_uid:<40}") - print("=" * 100) + if len(app.shared_secrets) > 20: + print(f" ... and {len(app.shared_secrets) - 20} more") + + print("=" * 100) except ValueError as e: print(f"Error: {e}") From 9fdc6e47fd8a11c279150e50731d052332b4fa98 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 16 Jun 2026 18:04:40 +0530 Subject: [PATCH 20/23] NSF Support added --- .../nested_shared_folders/nsf_get.py | 543 ++++++ .../nested_shared_folders/nsf_list.py | 555 +++++++ .../nested_shared_folders/nsf_ln.py | 543 ++++++ .../nested_shared_folders/nsf_mkdir.py | 568 +++++++ .../nested_shared_folders/nsf_record_add.py | 561 +++++++ .../nsf_record_details.py | 552 +++++++ .../nsf_record_permission.py | 581 +++++++ .../nsf_record_update.py | 549 +++++++ .../nested_shared_folders/nsf_rm.py | 567 +++++++ .../nested_shared_folders/nsf_rmdir.py | 564 +++++++ .../nested_shared_folders/nsf_rndir.py | 547 ++++++ .../nested_shared_folders/nsf_share_folder.py | 555 +++++++ .../nested_shared_folders/nsf_share_record.py | 560 +++++++ .../nsf_shortcut_keep.py | 560 +++++++ .../nsf_shortcut_list.py | 550 +++++++ .../nsf_transfer_record.py | 549 +++++++ .../src/keepercli/commands/nsf_commands.py | 1464 +++++++++++++++++ .../src/keepercli/register_commands.py | 17 +- keepersdk-package/requirements.txt | 1 + keepersdk-package/src/keepersdk/constants.py | 2 +- .../src/keepersdk/proto/APIRequest_pb2.py | 350 ++-- .../src/keepersdk/proto/APIRequest_pb2.pyi | 57 +- .../src/keepersdk/proto/AccountSummary_pb2.py | 80 +- .../keepersdk/proto/AccountSummary_pb2.pyi | 29 +- .../src/keepersdk/proto/BI_pb2.py | 376 +++-- .../src/keepersdk/proto/BI_pb2.pyi | 83 +- .../src/keepersdk/proto/GraphSync_pb2.py | 8 +- .../src/keepersdk/proto/GraphSync_pb2.pyi | 5 +- .../keepersdk/proto/NotificationCenter_pb2.py | 20 +- .../proto/NotificationCenter_pb2.pyi | 11 +- .../src/keepersdk/proto/SyncDown_pb2.py | 163 +- .../src/keepersdk/proto/SyncDown_pb2.pyi | 116 +- .../src/keepersdk/proto/automator_pb2.py | 124 +- .../src/keepersdk/proto/automator_pb2.pyi | 51 +- .../src/keepersdk/proto/breachwatch_pb2.py | 8 +- .../src/keepersdk/proto/breachwatch_pb2.pyi | 13 +- .../src/keepersdk/proto/client_pb2.py | 8 +- .../src/keepersdk/proto/client_pb2.pyi | 5 +- .../src/keepersdk/proto/dag_pb2.py | 49 + .../src/keepersdk/proto/dag_pb2.pyi | 101 ++ .../src/keepersdk/proto/enterprise_pb2.py | 630 +++---- .../src/keepersdk/proto/enterprise_pb2.pyi | 149 +- .../src/keepersdk/proto/folder_access_pb2.py | 51 + .../src/keepersdk/proto/folder_access_pb2.pyi | 52 + .../src/keepersdk/proto/folder_pb2.py | 211 ++- .../src/keepersdk/proto/folder_pb2.pyi | 465 +++++- .../src/keepersdk/proto/pagination_pb2.py | 39 + .../src/keepersdk/proto/pagination_pb2.pyi | 29 + .../src/keepersdk/proto/pam_pb2.py | 86 +- .../src/keepersdk/proto/pam_pb2.pyi | 321 +--- .../src/keepersdk/proto/pedm_pb2.py | 8 +- .../src/keepersdk/proto/pedm_pb2.pyi | 21 +- .../src/keepersdk/proto/push_pb2.py | 8 +- .../src/keepersdk/proto/push_pb2.pyi | 3 +- .../src/keepersdk/proto/record_details_pb2.py | 63 + .../keepersdk/proto/record_details_pb2.pyi | 78 + .../keepersdk/proto/record_endpoints_pb2.py | 41 + .../keepersdk/proto/record_endpoints_pb2.pyi | 46 + .../src/keepersdk/proto/record_pb2.py | 124 +- .../src/keepersdk/proto/record_pb2.pyi | 41 +- .../src/keepersdk/proto/record_sharing_pb2.py | 56 + .../keepersdk/proto/record_sharing_pb2.pyi | 95 ++ .../src/keepersdk/proto/remove_pb2.py | 69 + .../src/keepersdk/proto/remove_pb2.pyi | 208 +++ .../src/keepersdk/proto/router_pb2.py | 172 +- .../src/keepersdk/proto/router_pb2.pyi | 52 +- .../src/keepersdk/proto/ssocloud_pb2.py | 8 +- .../src/keepersdk/proto/ssocloud_pb2.pyi | 23 +- .../src/keepersdk/proto/tla_pb2.py | 39 + .../src/keepersdk/proto/tla_pb2.pyi | 25 + .../src/keepersdk/proto/version_pb2.py | 8 +- .../src/keepersdk/vault/memory_nsf_storage.py | 110 ++ .../src/keepersdk/vault/memory_storage.py | 10 +- .../src/keepersdk/vault/nsf_common.py | 340 ++++ .../src/keepersdk/vault/nsf_crypto.py | 240 +++ .../src/keepersdk/vault/nsf_data.py | 164 ++ .../src/keepersdk/vault/nsf_folder_records.py | 246 +++ .../src/keepersdk/vault/nsf_management.py | 1294 +++++++++++++++ .../src/keepersdk/vault/nsf_sharing.py | 555 +++++++ .../src/keepersdk/vault/nsf_storage_types.py | 226 +++ .../src/keepersdk/vault/nsf_sync.py | 955 +++++++++++ .../src/keepersdk/vault/nsf_vault_storage.py | 96 ++ .../keepersdk/vault/share_management_utils.py | 12 + .../src/keepersdk/vault/sqlite_nsf_storage.py | 196 +++ .../src/keepersdk/vault/sqlite_storage.py | 14 +- .../src/keepersdk/vault/sync_down.py | 23 +- .../src/keepersdk/vault/vault_online.py | 27 +- .../src/keepersdk/vault/vault_storage.py | 7 +- keepersdk-package/unit_tests/data_vault.py | 2 +- .../unit_tests/test_nsf_management.py | 72 + keepersdk-package/unit_tests/test_nsf_sync.py | 202 +++ 91 files changed, 18395 insertions(+), 1662 deletions(-) create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_get.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_list.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_ln.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_mkdir.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_record_add.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_record_details.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_record_permission.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_record_update.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_rm.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_rmdir.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_rndir.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_share_folder.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_share_record.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_shortcut_keep.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_shortcut_list.py create mode 100644 examples/sdk_examples/nested_shared_folders/nsf_transfer_record.py create mode 100644 keepercli-package/src/keepercli/commands/nsf_commands.py create mode 100644 keepersdk-package/src/keepersdk/proto/dag_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/dag_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/folder_access_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/folder_access_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/pagination_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/pagination_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/record_details_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/record_details_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/record_sharing_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/record_sharing_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/remove_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/remove_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/proto/tla_pb2.py create mode 100644 keepersdk-package/src/keepersdk/proto/tla_pb2.pyi create mode 100644 keepersdk-package/src/keepersdk/vault/memory_nsf_storage.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_common.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_crypto.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_data.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_folder_records.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_management.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_sharing.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_storage_types.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_sync.py create mode 100644 keepersdk-package/src/keepersdk/vault/nsf_vault_storage.py create mode 100644 keepersdk-package/src/keepersdk/vault/sqlite_nsf_storage.py create mode 100644 keepersdk-package/unit_tests/test_nsf_management.py create mode 100644 keepersdk-package/unit_tests/test_nsf_sync.py diff --git a/examples/sdk_examples/nested_shared_folders/nsf_get.py b/examples/sdk_examples/nested_shared_folders/nsf_get.py new file mode 100644 index 00000000..bdaeb0b5 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_get.py @@ -0,0 +1,543 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +import json + +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + + +def nsf_get(vault: vault_online.VaultOnline) -> None: + """Get NSF record or folder details by UID or title (nsf-get).""" + ITEM_UID_OR_TITLE = "" # Record/folder UID or title + + detail = nsf_management.get_nsf_item(vault, ITEM_UID_OR_TITLE) + print(json.dumps(detail, indent=2, default=str)) + + +def nsf_get_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_get(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_get_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_list.py b/examples/sdk_examples/nested_shared_folders/nsf_list.py new file mode 100644 index 00000000..5f024726 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_list.py @@ -0,0 +1,555 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_list(vault: vault_online.VaultOnline) -> None: + """List NSF folders and records (nsf-list).""" + INCLUDE_FOLDERS = True + INCLUDE_RECORDS = True + + rows = nsf_management.list_nsf_items( + vault, + include_folders=INCLUDE_FOLDERS, + include_records=INCLUDE_RECORDS, + ) + if not rows: + print("No NSF folders or records found. Run sync-down first.") + return + print(f"{'Type':<8} {'UID':<28} {'Title':<30} {'Record Type':<15} {'Location'}") + print("-" * 100) + for row in rows: + print( + f"{row.item_type:<8} {row.uid:<28} {row.title[:28]:<30} " + f"{(row.record_type or '')[:15]:<15} {row.parent_or_folder or ''}" + ) + print(f"\nTotal items: {len(rows)}") + + +def nsf_list_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_list(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_list_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_ln.py b/examples/sdk_examples/nested_shared_folders/nsf_ln.py new file mode 100644 index 00000000..e9d1a501 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_ln.py @@ -0,0 +1,543 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_folder_records, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_ln(vault: vault_online.VaultOnline) -> None: + """Link an NSF record into a folder (nsf-ln).""" + RECORD_UID_OR_TITLE = "My NSF Login" + FOLDER_UID_OR_NAME = "Archive" + + result = nsf_folder_records.link_nsf_record_to_folder( + vault, RECORD_UID_OR_TITLE, FOLDER_UID_OR_NAME + ) + print(f"Record {result.record_uid} linked to folder {result.folder_uid}") + + +def nsf_ln_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_ln(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_ln_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_mkdir.py b/examples/sdk_examples/nested_shared_folders/nsf_mkdir.py new file mode 100644 index 00000000..6df6cdf1 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_mkdir.py @@ -0,0 +1,568 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_mkdir(vault: vault_online.VaultOnline) -> None: + """Create an NSF folder (nsf-mkdir).""" + FOLDER_PATH = "Projects/Demo" # Use // for literal / in a name + COLOR = None # none, red, orange, yellow, green, blue, gray + INHERIT_PERMISSIONS = True + + segments = [s.strip() for s in FOLDER_PATH.replace("//", "\x00").split("/") if s.strip()] + if not segments: + raise ValueError("Invalid folder path") + + parent_uid = None + created_uid = None + for idx, segment in enumerate(segments): + segment = segment.replace("\x00", "/") + is_leaf = idx == len(segments) - 1 + existing = nsf_management.find_nsf_child_folder(vault, segment, parent_uid) + if existing: + if is_leaf: + print(f'Folder "{segment}" already exists: {existing}') + return + parent_uid = existing + continue + result = nsf_management.create_nsf_folder( + vault, + segment, + parent_uid=parent_uid, + color=COLOR if is_leaf else None, + inherit_permissions=INHERIT_PERMISSIONS if is_leaf else True, + ) + created_uid = result.folder_uid + parent_uid = created_uid + + if created_uid: + print(f"NSF folder created: {created_uid}") + + +def nsf_mkdir_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_mkdir(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_mkdir_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_record_add.py b/examples/sdk_examples/nested_shared_folders/nsf_record_add.py new file mode 100644 index 00000000..2eaac9e1 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_record_add.py @@ -0,0 +1,561 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_record_add(vault: vault_online.VaultOnline) -> None: + """Add a record to NSF (nsf-record-add).""" + TITLE = "My NSF Login" + RECORD_TYPE = "login" # e.g. login, password, general + FOLDER_UID_OR_NAME = "Projects" # NSF folder name or UID; None for root + NOTES = "Created via SDK example" + FIELDS = { + "login": "user@example.com", + "password": "changeme", + "url": "https://example.com", + } + + folder_uid = None + if FOLDER_UID_OR_NAME: + folder_uid = nsf_management.resolve_nsf_folder_uid(vault, FOLDER_UID_OR_NAME) + if not folder_uid: + raise ValueError(f"NSF folder not found: {FOLDER_UID_OR_NAME}") + + result = nsf_management.create_nsf_record( + vault, + title=TITLE, + record_type=RECORD_TYPE, + folder_uid=folder_uid, + fields=FIELDS, + notes=NOTES, + ) + print(f"NSF record created: {result.record_uid} (status: {result.status})") + + +def nsf_record_add_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_record_add(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_record_add_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_record_details.py b/examples/sdk_examples/nested_shared_folders/nsf_record_details.py new file mode 100644 index 00000000..232acdf5 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_record_details.py @@ -0,0 +1,552 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_record_details(vault: vault_online.VaultOnline) -> None: + """Get NSF record metadata via v3 API (nsf-record-details).""" + RECORD_IDENTIFIERS = ["My NSF Login"] # UIDs or titles + + result = nsf_management.get_nsf_record_details(vault, RECORD_IDENTIFIERS) + for record in result.get("data", []): + print(f"Record UID: {record['record_uid']}") + print(f" Title: {record.get('title')}") + print(f" Type: {record.get('type', 'Unknown')}") + print(f" Version: {record.get('version', 0)}") + print(f" Revision: {record.get('revision', 0)}") + print() + forbidden = result.get("forbidden_records") or [] + if forbidden: + print(f"Forbidden records: {len(forbidden)}") + for uid in forbidden: + print(f" {uid}") + print(f"Total records retrieved: {len(result.get('data', []))}") + + +def nsf_record_details_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_record_details(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_record_details_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_record_permission.py b/examples/sdk_examples/nested_shared_folders/nsf_record_permission.py new file mode 100644 index 00000000..c9485d0e --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_record_permission.py @@ -0,0 +1,581 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_sharing, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_record_permission(vault: vault_online.VaultOnline, current_user: str) -> None: + """Bulk-update NSF record permissions within a folder (nsf-record-permission).""" + FOLDER_UID_OR_NAME = "Projects" + ACTION = "grant" # grant or revoke + ROLE = "viewer" # Required for grant + RECURSIVE = False + FORCE = True + DRY_RUN = False + + plan = nsf_sharing.plan_nsf_record_permissions( + vault, + FOLDER_UID_OR_NAME, + action=ACTION, + role=ROLE, + recursive=RECURSIVE, + current_user=current_user, + ) + if not plan.updates and not plan.creates and not plan.revokes: + print("No permission changes are needed.") + return + for label, items in ( + ("SKIP", plan.skipped), + ("GRANT/UPDATE", plan.updates + plan.creates), + ("REVOKE", plan.revokes), + ): + if not items: + continue + print(f"\n{label}:") + for item in items: + line = f" {item.get('record_uid')} {item.get('email', '')} {item.get('cur_role', '')}" + if item.get("new_role"): + line += f" -> {item['new_role']}" + print(line) + if DRY_RUN: + return + if not FORCE: + answer = input("Proceed with permission changes? [y/N]: ").strip().lower() + if answer not in ("y", "yes"): + print("Aborted.") + return + outcomes = nsf_sharing.apply_nsf_record_permissions(vault, plan) + for bucket, rows in outcomes.items(): + for item, result in rows: + status = "ok" if result.get("success") else result.get("message") + print(f"{bucket} {item.get('record_uid')} {item.get('email')}: {status}") + + +def nsf_record_permission_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_record_permission( + vault, keeper_auth_context.auth_context.username + ) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_record_permission_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_record_update.py b/examples/sdk_examples/nested_shared_folders/nsf_record_update.py new file mode 100644 index 00000000..eb41a15a --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_record_update.py @@ -0,0 +1,549 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_record_update(vault: vault_online.VaultOnline) -> None: + """Update an NSF record (nsf-record-update).""" + RECORD_UID_OR_TITLE = "My NSF Login" + NEW_TITLE = "My NSF Login (updated)" + FIELDS = {"password": "new-secret-value"} + NOTES = "Updated via SDK example" + + result = nsf_management.update_nsf_record( + vault, + RECORD_UID_OR_TITLE, + title=NEW_TITLE, + fields=FIELDS, + notes=NOTES, + ) + print(f"NSF record updated: {result.record_uid} (status: {result.status})") + + +def nsf_record_update_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_record_update(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_record_update_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_rm.py b/examples/sdk_examples/nested_shared_folders/nsf_rm.py new file mode 100644 index 00000000..14ba53ed --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_rm.py @@ -0,0 +1,567 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_rm(vault: vault_online.VaultOnline) -> None: + """Remove NSF record(s) (nsf-rm).""" + RECORD_IDENTIFIERS = ["My NSF Login"] # UIDs or titles + OPERATION = "owner-trash" # owner-trash, folder-trash, unlink + FOLDER_UID_OR_NAME = None # Required when OPERATION is unlink + FORCE = True # Skip confirmation after preview + DRY_RUN = False # Preview only + + if OPERATION == "unlink" and not FOLDER_UID_OR_NAME: + raise ValueError('--folder is required when operation is "unlink"') + + removals = nsf_management.build_nsf_record_removals( + vault, + RECORD_IDENTIFIERS, + operation_type=OPERATION, + folder_uid=FOLDER_UID_OR_NAME, + ) + preview = nsf_management.remove_nsf_records(vault, removals, dry_run=True) + for pr in preview.preview_results: + print(f" {pr.item_uid}: {pr.error or 'ok to remove'}") + if DRY_RUN: + print("[Dry-run] No records were deleted.") + return + if not FORCE: + answer = input("Proceed with deletion? [y/N]: ").strip().lower() + if answer not in ("y", "yes"): + print("Aborted.") + return + result = nsf_management.remove_nsf_records(vault, removals, dry_run=False) + if result.confirmed: + print("Record removal completed.") + else: + print("Record removal was not confirmed by the server.") + + +def nsf_rm_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_rm(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_rm_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_rmdir.py b/examples/sdk_examples/nested_shared_folders/nsf_rmdir.py new file mode 100644 index 00000000..35d8960f --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_rmdir.py @@ -0,0 +1,564 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_rmdir(vault: vault_online.VaultOnline) -> None: + """Remove NSF folder(s) (nsf-rmdir).""" + FOLDER_IDENTIFIERS = ["Demo Renamed"] # UIDs or names + OPERATION = "folder-trash" # folder-trash, delete-permanent + FORCE = True + DRY_RUN = False + + removals = [] + for identifier in FOLDER_IDENTIFIERS: + folder_uid = nsf_management.resolve_nsf_folder_uid(vault, identifier) + if not folder_uid: + raise ValueError(f'Folder "{identifier}" not found') + removals.append({"folder_uid": folder_uid, "operation_type": OPERATION}) + + preview = nsf_management.remove_nsf_folders(vault, removals, dry_run=True) + for pr in preview.preview_results: + print(f" {pr.item_uid}: {pr.error or 'ok to remove'}") + if DRY_RUN: + print("[Dry-run] No folders were deleted.") + return + if not FORCE: + answer = input("Proceed with folder deletion? [y/N]: ").strip().lower() + if answer not in ("y", "yes"): + print("Aborted.") + return + result = nsf_management.remove_nsf_folders(vault, removals, dry_run=False) + if result.confirmed: + print("Folder removal completed.") + else: + print("Folder removal was not confirmed by the server.") + + +def nsf_rmdir_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_rmdir(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_rmdir_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_rndir.py b/examples/sdk_examples/nested_shared_folders/nsf_rndir.py new file mode 100644 index 00000000..8f298710 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_rndir.py @@ -0,0 +1,547 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_rndir(vault: vault_online.VaultOnline) -> None: + """Rename or recolor an NSF folder (nsf-rndir).""" + FOLDER_UID_OR_NAME = "Demo" + NEW_NAME = "Demo Renamed" # Set to None to only change color + COLOR = None # none, red, orange, yellow, green, blue, gray + + result = nsf_management.update_nsf_folder( + vault, + FOLDER_UID_OR_NAME, + folder_name=NEW_NAME, + color=COLOR, + ) + print(f"Folder updated: {result.folder_uid}") + + +def nsf_rndir_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_rndir(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_rndir_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_share_folder.py b/examples/sdk_examples/nested_shared_folders/nsf_share_folder.py new file mode 100644 index 00000000..a53d2647 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_share_folder.py @@ -0,0 +1,555 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_sharing, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_share_folder(vault: vault_online.VaultOnline) -> None: + """Grant or revoke NSF folder access (nsf-share-folder).""" + FOLDER_UID_OR_NAME = "Projects" + RECIPIENT_EMAIL = "colleague@example.com" + ACTION = "grant" # grant or remove + ROLE = "viewer" # viewer, share-manager, content-manager, content-share-manager, full-manager + EXPIRATION_TIMESTAMP = None # Unix ms; optional for grant + + if ACTION == "remove": + result = nsf_sharing.revoke_nsf_folder_access( + vault, FOLDER_UID_OR_NAME, RECIPIENT_EMAIL + ) + else: + result = nsf_sharing.grant_nsf_folder_access( + vault, + FOLDER_UID_OR_NAME, + RECIPIENT_EMAIL, + role=ROLE, + expiration_timestamp=EXPIRATION_TIMESTAMP, + ) + print(f"{RECIPIENT_EMAIL}: {result.get('message', result.get('status'))}") + + +def nsf_share_folder_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_share_folder(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_share_folder_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_share_record.py b/examples/sdk_examples/nested_shared_folders/nsf_share_record.py new file mode 100644 index 00000000..4f26be44 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_share_record.py @@ -0,0 +1,560 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_sharing, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_share_record(vault: vault_online.VaultOnline) -> None: + """Change NSF record sharing permissions (nsf-share-record).""" + RECORD_UID_OR_TITLE = "My NSF Login" + RECIPIENT_EMAIL = "colleague@example.com" + ACTION = "grant" # grant, revoke, owner + ROLE = "viewer" # Required for grant + RECURSIVE = False + EXPIRATION_TIMESTAMP = None + + record_uids = nsf_sharing.resolve_nsf_share_record_uids( + vault, RECORD_UID_OR_TITLE, recursive=RECURSIVE + ) + for record_uid in record_uids: + result, effective = nsf_sharing.share_nsf_record_with_action( + vault, + record_uid, + RECIPIENT_EMAIL, + action=ACTION, + role=ROLE, + expiration_timestamp=EXPIRATION_TIMESTAMP, + ) + if result.success: + print(f"Record {record_uid} permissions {effective} for {RECIPIENT_EMAIL}") + else: + msg = result.results[0]["message"] if result.results else "failed" + print(f"Share failed for {record_uid}: {msg}") + + +def nsf_share_record_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_share_record(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_share_record_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_shortcut_keep.py b/examples/sdk_examples/nested_shared_folders/nsf_shortcut_keep.py new file mode 100644 index 00000000..c8ef7333 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_shortcut_keep.py @@ -0,0 +1,560 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_folder_records, nsf_management, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_shortcut_keep(vault: vault_online.VaultOnline) -> None: + """Keep an NSF record in one folder only (nsf-shortcut keep).""" + RECORD_UID_OR_TITLE = "My NSF Login" + FOLDER_UID_OR_NAME = "Projects" # Folder to keep the record in + FORCE = True + + shortcuts = nsf_folder_records.get_nsf_shortcut_map(vault) + record_uid = nsf_management.resolve_nsf_record_uid(vault, RECORD_UID_OR_TITLE) + if not record_uid: + raise ValueError(f'Record "{RECORD_UID_OR_TITLE}" not found') + keep_folder = nsf_management.resolve_nsf_folder_uid(vault, FOLDER_UID_OR_NAME) or FOLDER_UID_OR_NAME + to_remove = [f for f in shortcuts.get(record_uid, set()) if f != keep_folder] + if not to_remove: + print("Nothing to do — record is already in only one folder.") + return + if not FORCE: + answer = input( + f"Keep record in {FOLDER_UID_OR_NAME} and remove from {len(to_remove)} folder(s)? [y/N]: " + ).strip().lower() + if answer not in ("y", "yes"): + print("Aborted.") + return + results = nsf_folder_records.keep_nsf_shortcut_in_folder( + vault, RECORD_UID_OR_TITLE, FOLDER_UID_OR_NAME + ) + print(f"Removed record from {len(results)} folder link(s).") + + +def nsf_shortcut_keep_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_shortcut_keep(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_shortcut_keep_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_shortcut_list.py b/examples/sdk_examples/nested_shared_folders/nsf_shortcut_list.py new file mode 100644 index 00000000..ecac4678 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_shortcut_list.py @@ -0,0 +1,550 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_folder_records, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_shortcut_list(vault: vault_online.VaultOnline) -> None: + """List NSF records linked to multiple folders (nsf-shortcut list).""" + TARGET_FILTER = None # Optional record or folder filter + + rows = nsf_folder_records.list_nsf_shortcuts(vault, target=TARGET_FILTER) + if not rows: + print("No NSF shortcut records found") + return + view = vault.nsf_data + for row in rows: + folder_names = [] + for fuid in row.folder_uids: + folder = view.get_folder(fuid) if view else None + fname = folder.name if folder and folder.name else fuid + folder_names.append(f"{fname} ({fuid})") + print(f"{row.record_uid} | {row.title} | {', '.join(folder_names)}") + + +def nsf_shortcut_list_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_shortcut_list(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_shortcut_list_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/examples/sdk_examples/nested_shared_folders/nsf_transfer_record.py b/examples/sdk_examples/nested_shared_folders/nsf_transfer_record.py new file mode 100644 index 00000000..36af55b5 --- /dev/null +++ b/examples/sdk_examples/nested_shared_folders/nsf_transfer_record.py @@ -0,0 +1,549 @@ +import getpass +import json +import logging +import sqlite3 +from typing import Dict, Optional + +import fido2 +import webbrowser + +from keepersdk import errors, utils +from keepersdk.authentication import ( + configuration, + endpoint, + keeper_auth, + login_auth, +) +from keepersdk.authentication.yubikey import ( + IKeeperUserInteraction, + yubikey_authenticate, +) +from keepersdk.constants import KEEPER_PUBLIC_HOSTS +from keepersdk.vault import nsf_sharing, sqlite_storage, vault_online + +try: + import pyperclip +except ImportError: + pyperclip = None + +logger = utils.get_logger() +logger.setLevel(logging.INFO) +if not logger.handlers: + _handler = logging.StreamHandler() + _handler.setLevel(logging.INFO) + _handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s") + ) + logger.addHandler(_handler) + + +class FidoCliInteraction(fido2.client.UserInteraction, IKeeperUserInteraction): + def output_text(self, text: str) -> None: + print(text) + + def prompt_up(self) -> None: + print( + "\nTouch the flashing Security key to authenticate or " + "press Ctrl-C to resume with the primary two factor authentication..." + ) + + def request_pin(self, permissions, rd_id): + return getpass.getpass("Enter Security Key PIN: ") + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +# Two-factor duration codes (used by LoginFlow) +_TWO_FACTOR_DURATION_CODES: Dict[login_auth.TwoFactorDuration, str] = { + login_auth.TwoFactorDuration.EveryLogin: "login", + login_auth.TwoFactorDuration.Every12Hours: "12_hours", + login_auth.TwoFactorDuration.EveryDay: "24_hours", + login_auth.TwoFactorDuration.Every30Days: "30_days", + login_auth.TwoFactorDuration.Forever: "forever", +} + + +class LoginFlow: + """ + Handles the full login process: server selection, username, password, + device approval, 2FA, SSO data key, and SSO token. + """ + + def __init__(self) -> None: + self._config = configuration.JsonConfigurationStorage() + self._logged_in_with_persistent = True + self._endpoint: Optional[endpoint.KeeperEndpoint] = None + + @property + def endpoint(self) -> Optional[endpoint.KeeperEndpoint]: + return self._endpoint + + @property + def logged_in_with_persistent(self) -> bool: + """True if login succeeded by resuming an existing persistent session (no step loop).""" + return self._logged_in_with_persistent + + def run(self) -> Optional[keeper_auth.KeeperAuth]: + """ + Run the login flow. + + Returns: + Authenticated Keeper context, or None if login fails. + """ + server = self._ensure_server() + keeper_endpoint = endpoint.KeeperEndpoint(self._config, server) + self._endpoint = keeper_endpoint + login_auth_context = login_auth.LoginAuth(keeper_endpoint) + + username = self._config.get().last_login or input("Enter username: ") + login_auth_context.resume_session = True + login_auth_context.login(username) + + while not login_auth_context.login_step.is_final(): + step = login_auth_context.login_step + if isinstance(step, login_auth.LoginStepDeviceApproval): + self._handle_device_approval(step) + elif isinstance(step, login_auth.LoginStepTwoFactor): + self._handle_two_factor(step) + elif isinstance(step, login_auth.LoginStepPassword): + self._handle_password(step) + elif isinstance(step, login_auth.LoginStepSsoToken): + self._handle_sso_token(step) + elif isinstance(step, login_auth.LoginStepSsoDataKey): + self._handle_sso_data_key(step) + elif isinstance(step, login_auth.LoginStepError): + print(f"Login error: ({step.code}) {step.message}") + return None + else: + raise NotImplementedError( + f"Unsupported login step type: {type(step).__name__}" + ) + self._logged_in_with_persistent = False + + if self._logged_in_with_persistent: + print("Successfully logged in with persistent login") + + if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): + return login_auth_context.login_step.take_keeper_auth() + + return None + + def _ensure_server(self) -> str: + if not self._config.get().last_server: + print("Available server options:") + for region, host in KEEPER_PUBLIC_HOSTS.items(): + print(f" {region}: {host}") + server = ( + input("Enter server (default: keepersecurity.com): ").strip() + or "keepersecurity.com" + ) + self._config.get().last_server = server + else: + server = self._config.get().last_server + return server + + def _handle_device_approval( + self, step: login_auth.LoginStepDeviceApproval + ) -> None: + """Device approval: same options as keepercli verify_device (email, keeper push, 2FA, resume).""" + menu = [ + ("email_send", "to send email"), + ("email_code=", "to validate verification code sent via email"), + ("keeper_push", "to send Keeper Push notification"), + ("2fa_send", "to send 2FA code"), + ("2fa_code=", "to validate a code provided by 2FA application"), + ("", "to resume"), + ] + lines = ["Approve by selecting a method below"] + lines.extend(f" {cmd} {desc}" for cmd, desc in menu) + print("\n".join(lines)) + + selection = input("Type your selection or to resume: ").strip() + if selection is None: + return + if selection in ("email_send", "es"): + step.send_push(channel=login_auth.DeviceApprovalChannel.Email) + print("An email with instructions has been sent. Press when approved.") + elif selection.startswith("email_code="): + code = selection[len("email_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.Email, code=code) + print("Successfully verified email code.") + elif selection in ("keeper_push", "kp"): + step.send_push(channel=login_auth.DeviceApprovalChannel.KeeperPush) + print( + "Successfully made a push notification to the approved device. " + "Press when approved." + ) + elif selection in ("2fa_send", "2fs"): + step.send_push(channel=login_auth.DeviceApprovalChannel.TwoFactor) + print("2FA code was sent.") + elif selection.startswith("2fa_code="): + code = selection[len("2fa_code=") :] + step.send_code(channel=login_auth.DeviceApprovalChannel.TwoFactor, code=code) + print("Successfully verified 2FA code.") + else: + step.resume() + + def _handle_password(self, step: login_auth.LoginStepPassword) -> None: + """Password step: prompt for password and retry on auth_failed (aligned with keepercli handle_verify_password).""" + print(f"\nEnter password for {step.username}") + while True: + password = getpass.getpass("Password: ") + if not password: + raise KeyboardInterrupt() + try: + step.verify_password(password) + break + except errors.KeeperApiError as kae: + print( + "Invalid email or password combination, please re-enter." + if kae.result_code == "auth_failed" + else kae.message + ) + + def _handle_two_factor(self, step: login_auth.LoginStepTwoFactor) -> None: + channels = [ + x + for x in step.get_channels() + if x.channel_type != login_auth.TwoFactorChannel.Other + ] + menu = [] + for i, channel in enumerate(channels): + desc = self._two_factor_channel_desc(channel.channel_type) + menu.append( + ( + str(i + 1), + f"{desc} {channel.channel_name} {channel.phone}", + ) + ) + menu.append(("q", "Quit authentication attempt and return to Commander prompt.")) + + lines = ["", "This account requires 2FA Authentication"] + lines.extend(f" {a}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + selection = input("Selection: ") + if selection is None: + return + if selection in ("q", "Q"): + raise KeyboardInterrupt() + try: + assert selection.isnumeric() + idx = 1 if not selection else int(selection) + assert 1 <= idx <= len(channels) + channel = channels[idx - 1] + desc = self._two_factor_channel_desc(channel.channel_type) + print(f"Selected {idx}. {desc}") + except AssertionError: + print( + "Invalid entry, additional factors of authentication shown " + "may be configured if not currently enabled." + ) + continue + + if channel.channel_type in ( + login_auth.TwoFactorChannel.TextMessage, + login_auth.TwoFactorChannel.KeeperDNA, + login_auth.TwoFactorChannel.DuoSecurity, + ): + action = next( + ( + x + for x in step.get_channel_push_actions(channel.channel_uid) + if x + in ( + login_auth.TwoFactorPushAction.TextMessage, + login_auth.TwoFactorPushAction.KeeperDna, + ) + ), + None, + ) + if action: + step.send_push(channel.channel_uid, action) + + if channel.channel_type == login_auth.TwoFactorChannel.SecurityKey: + try: + challenge = json.loads(channel.challenge) + signature = yubikey_authenticate(challenge, FidoCliInteraction()) + if signature: + print("Verified Security Key.") + step.send_code(channel.channel_uid, signature) + return + except Exception as e: + logger.error(e) + continue + + # 2FA code path + step.duration = min(step.duration, channel.max_expiration) + available_dura = sorted( + x for x in _TWO_FACTOR_DURATION_CODES if x <= channel.max_expiration + ) + available_codes = [ + _TWO_FACTOR_DURATION_CODES.get(x) or "login" for x in available_dura + ] + + while True: + mfa_desc = self._two_factor_duration_desc(step.duration) + prompt_exp = ( + f"\n2FA Code Duration: {mfa_desc}.\n" + f"To change duration: 2fa_duration={'|'.join(available_codes)}" + ) + print(prompt_exp) + + selection = input("\nEnter 2FA Code or Duration: ") + if not selection: + return + if selection in available_codes: + step.duration = self._two_factor_code_to_duration(selection) + elif selection.startswith("2fa_duration="): + code = selection[len("2fa_duration=") :] + if code in available_codes: + step.duration = self._two_factor_code_to_duration(code) + else: + print(f"Invalid 2FA duration: {code}") + else: + try: + step.send_code(channel.channel_uid, selection) + print("Successfully verified 2FA Code.") + return + except errors.KeeperApiError as kae: + print(f"Invalid 2FA code: ({kae.result_code}) {kae.message}") + + def _handle_sso_data_key( + self, step: login_auth.LoginStepSsoDataKey + ) -> None: + menu = [ + ("1", "Keeper Push. Send a push notification to your device."), + ("2", "Admin Approval. Request your admin to approve this device."), + ("r", "Resume SSO authentication after device is approved."), + ("q", "Quit SSO authentication attempt and return to Commander prompt."), + ] + lines = ["Approve this device by selecting a method below:"] + lines.extend(f" {cmd:>3}. {text}" for cmd, text in menu) + print("\n".join(lines)) + + while True: + answer = input("Selection: ") + if answer is None: + return + if answer == "q": + raise KeyboardInterrupt() + if answer == "r": + step.resume() + break + if answer in ("1", "2"): + step.request_data_key( + login_auth.DataKeyShareChannel.KeeperPush + if answer == "1" + else login_auth.DataKeyShareChannel.AdminApproval + ) + else: + print(f'Action "{answer}" is not supported.') + + def _handle_sso_token(self, step: login_auth.LoginStepSsoToken) -> None: + menu = [ + ("a", "SSO User with a Master Password."), + ] + if pyperclip: + menu.append(("c", "Copy SSO Login URL to clipboard.")) + else: + menu.append(("u", "Show SSO Login URL.")) + try: + wb = webbrowser.get() + menu.append(("o", "Navigate to SSO Login URL with the default web browser.")) + except Exception: + wb = None + if pyperclip: + menu.append(("p", "Paste SSO Token from clipboard.")) + menu.append(("t", "Enter SSO Token manually.")) + menu.append(("q", "Quit SSO authentication attempt and return to Commander prompt.")) + + lines = [ + "", + "SSO Login URL:", + step.sso_login_url, + "Navigate to SSO Login URL with your browser and complete authentication.", + "Copy a returned SSO Token into clipboard." + + (" Paste that token into Commander." if pyperclip else " Then use option 't' to enter the token manually."), + 'NOTE: To copy SSO Token please click "Copy authentication token" ' + 'button on "SSO Connect" page.', + "", + ] + lines.extend(f" {a:>3}. {t}" for a, t in menu) + print("\n".join(lines)) + + while True: + token = input("Selection: ") + if token == "q": + raise KeyboardInterrupt() + if token == "a": + step.login_with_password() + return + if token == "c": + token = None + if pyperclip: + try: + pyperclip.copy(step.sso_login_url) + print("SSO Login URL is copied to clipboard.") + except Exception: + print("Failed to copy SSO Login URL to clipboard.") + else: + print("Clipboard not available (install pyperclip).") + elif token == "u": + token = None + if not pyperclip: + print("\nSSO Login URL:", step.sso_login_url, "\n") + else: + print("Unsupported menu option (use 'c' to copy URL).") + elif token == "o": + token = None + if wb: + try: + wb.open_new_tab(step.sso_login_url) + except Exception: + print("Failed to open web browser.") + elif token == "p": + if pyperclip: + try: + token = pyperclip.paste() + except Exception: + token = "" + print("Failed to paste from clipboard") + else: + token = None + print("Clipboard not available (use 't' to enter token manually).") + elif token == "t": + token = getpass.getpass("Enter SSO Token: ").strip() + else: + if len(token) < 10: + print(f"Unsupported menu option: {token}") + continue + + if token: + try: + step.set_sso_token(token) + break + except errors.KeeperApiError as kae: + print(f"SSO Login error: ({kae.result_code}) {kae.message}") + + @staticmethod + def _two_factor_channel_desc( + channel_type: login_auth.TwoFactorChannel, + ) -> str: + return { + login_auth.TwoFactorChannel.Authenticator: "TOTP (Google and Microsoft Authenticator)", + login_auth.TwoFactorChannel.TextMessage: "Send SMS Code", + login_auth.TwoFactorChannel.DuoSecurity: "DUO", + login_auth.TwoFactorChannel.RSASecurID: "RSA SecurID", + login_auth.TwoFactorChannel.SecurityKey: "WebAuthN (FIDO2 Security Key)", + login_auth.TwoFactorChannel.KeeperDNA: "Keeper DNA (Watch)", + login_auth.TwoFactorChannel.Backup: "Backup Code", + }.get(channel_type, "Not Supported") + + @staticmethod + def _two_factor_duration_desc( + duration: login_auth.TwoFactorDuration, + ) -> str: + return { + login_auth.TwoFactorDuration.EveryLogin: "Require Every Login", + login_auth.TwoFactorDuration.Forever: "Save on this Device Forever", + login_auth.TwoFactorDuration.Every12Hours: "Ask Every 12 hours", + login_auth.TwoFactorDuration.EveryDay: "Ask Every 24 hours", + login_auth.TwoFactorDuration.Every30Days: "Ask Every 30 days", + }.get(duration, "Require Every Login") + + @staticmethod + def _two_factor_code_to_duration( + text: str, + ) -> login_auth.TwoFactorDuration: + for dura, code in _TWO_FACTOR_DURATION_CODES.items(): + if code == text: + return dura + return login_auth.TwoFactorDuration.EveryLogin + + +def enable_persistent_login(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + """ + Enable persistent login and register data key for device. + Sets persistent_login to on and logout_timer to 30 days. + """ + keeper_auth.set_user_setting(keeper_auth_context, 'persistent_login', '1') + keeper_auth.register_data_key_for_device(keeper_auth_context) + mins_per_day = 60 * 24 + timeout_in_minutes = mins_per_day * 30 # 30 days + keeper_auth.set_user_setting(keeper_auth_context, 'logout_timer', str(timeout_in_minutes)) + print("Persistent login turned on successfully and device registered") + + +def login(): + """ + Handle the login process including server selection, authentication, + and multi-factor authentication steps (device approval, password, 2FA + with channel selection and Security Key, SSO data key, SSO token). + + Returns: + tuple: (keeper_auth_context, keeper_endpoint) on success, or (None, None) if login fails. + """ + flow = LoginFlow() + keeper_auth_context = flow.run() + if keeper_auth_context and not flow.logged_in_with_persistent: + enable_persistent_login(keeper_auth_context) + keeper_endpoint = flow.endpoint if keeper_auth_context else None + return keeper_auth_context, keeper_endpoint + + +def open_vault(keeper_auth_context: keeper_auth.KeeperAuth) -> vault_online.VaultOnline: + conn = sqlite3.Connection("file::memory:", uri=True) + vault_storage = sqlite_storage.SqliteVaultStorage( + lambda: conn, + vault_owner=bytes(keeper_auth_context.auth_context.username, "utf-8"), + ) + vault = vault_online.VaultOnline(keeper_auth_context, vault_storage) + vault.sync_down() + return vault + + +def close_vault(vault: vault_online.VaultOnline, keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault.close() + keeper_auth_context.close() + +def nsf_transfer_record(vault: vault_online.VaultOnline) -> None: + """Transfer NSF record ownership (nsf-transfer-record).""" + RECORD_IDENTIFIERS = ["My NSF Login"] + NEW_OWNER_EMAIL = "newowner@example.com" + + for identifier in RECORD_IDENTIFIERS: + result = nsf_sharing.transfer_nsf_record_ownership( + vault, identifier, NEW_OWNER_EMAIL + ) + for row in result.results: + if row.get("success"): + print(f"Record '{row['record_uid']}' transferred to {NEW_OWNER_EMAIL}") + print("You will no longer have access to this record!") + else: + print(f"Transfer failed: {row.get('message')}") + + +def nsf_transfer_record_run(keeper_auth_context: keeper_auth.KeeperAuth) -> None: + vault = open_vault(keeper_auth_context) + try: + nsf_transfer_record(vault) + except Exception as e: + print(f"Error: {e}") + finally: + close_vault(vault, keeper_auth_context) + + +def main() -> None: + keeper_auth_context, _ = login() + if keeper_auth_context: + nsf_transfer_record_run(keeper_auth_context) + else: + print("Login failed.") + + +if __name__ == "__main__": + main() diff --git a/keepercli-package/src/keepercli/commands/nsf_commands.py b/keepercli-package/src/keepercli/commands/nsf_commands.py new file mode 100644 index 00000000..ca0694ac --- /dev/null +++ b/keepercli-package/src/keepercli/commands/nsf_commands.py @@ -0,0 +1,1464 @@ +import argparse +import json +from typing import Any, Dict, List, Optional + +from keepersdk.vault import nsf_folder_records, nsf_management, nsf_sharing, vault_record, nsf_common +from keepersdk.vault.share_management_utils import parse_nsf_share_expiration +from keepersdk.vault.nsf_management import ( + NsfError, + NsfListRow, + NsfRemovePreviewItem, + NsfRemoveResult, +) +from keepersdk.vault import share_management_utils + +from . import base +from .record_edit import RecordEditMixin, record_fields_description, ParsedFieldValue +from .. import api, prompt_utils +from ..helpers import report_utils +from ..params import KeeperParams + +logger = api.get_logger() + +_MASKED_TYPES = frozenset({'password', 'secret', 'pinCode', 'pin_code'}) + + +def _mask_sensitive_fields(fields: List[Any], *, unmask: bool) -> List[Any]: + """Return a copy of record fields with sensitive values replaced unless ``unmask``.""" + if unmask or not fields: + return list(fields) + masked_fields: List[Any] = [] + for f in fields: + if not isinstance(f, dict): + masked_fields.append(f) + continue + ftype = str(f.get('type', '')) + if ftype not in _MASKED_TYPES: + masked_fields.append(f) + continue + entry = dict(f) + values = entry.get('value', []) + if not isinstance(values, list): + values = [values] + entry['value'] = [ + '********' if (val or val == 0) else val + for val in values + ] + masked_fields.append(entry) + return masked_fields + + +def _require_vault(context: KeeperParams): + base.require_login(context) + if context.vault is None: + raise base.CommandError('Vault is not initialized. Login to initialize the vault.') + return context.vault + + +def _wrap_nsf(command: str, fn): + try: + return fn() + except NsfError as e: + raise base.CommandError(str(e)) from e + + +def _typed_record_to_data( + record: vault_record.TypedRecord, + title: str, + notes: Optional[str]) -> Dict[str, Any]: + data: Dict[str, Any] = { + 'type': record.record_type, + 'title': title, + 'fields': [ + {'type': f.type, 'label': f.label or '', 'value': list(f.value)} + for f in record.fields + ], + } + if record.custom: + data['custom'] = [ + {'type': f.type, 'label': f.label or '', 'value': list(f.value)} + for f in record.custom + ] + if notes: + data['notes'] = notes + return data + + +def _legacy_record_to_data( + record: vault_record.PasswordRecord, + title: str, + notes: Optional[str]) -> Dict[str, Any]: + data: Dict[str, Any] = {'type': record.get_record_type(), 'title': title, 'fields': []} + for ftype, val in ( + ('login', record.login), + ('password', record.password), + ('url', record.link), + ('oneTimeCode', record.totp), + ): + if val: + data['fields'].append({'type': ftype, 'value': [val]}) + for cf in record.custom or []: + data['fields'].append({ + 'type': 'text', + 'label': cf.name if hasattr(cf, 'name') else '', + 'value': [cf.value if hasattr(cf, 'value') else str(cf)], + }) + if notes: + data['notes'] = notes + return data + + +def _access_role_label(access: Dict[str, Any]) -> str: + if access.get('owner'): + return 'owner' + if access.get('can_edit'): + return 'editor' + if access.get('can_view') or access.get('can_view_title'): + return 'viewer' + return str(access.get('access_type') or '') + + +class _NsfRecordDataMixin(RecordEditMixin): + """Build encrypted record JSON payloads for nsf-record-add / nsf-record-update.""" + + def build_nsf_record_data( + self, + context: KeeperParams, + record_type: str, + title: str, + notes: Optional[str], + record_fields: List[ParsedFieldValue]) -> Dict[str, Any]: + notes = self.validate_notes(notes or '') + if record_type in ('legacy', 'general'): + record = vault_record.PasswordRecord() + self.assign_legacy_fields(record, record_fields) + record.title = title + record.notes = notes + return _legacy_record_to_data(record, title, notes) + + rt = context.vault.vault_data.get_record_type_by_name(record_type) + if not rt: + raise base.CommandError(f'Record type "{record_type}" cannot be found.') + + record = vault_record.TypedRecord() + record.record_type = record_type + for rf in rt.fields: + ref = rf.type + if not ref: + continue + field = vault_record.TypedField.create_field(ref, rf.label) + if rf.required is True: + field.required = True + record.fields.append(field) + self.assign_typed_fields(record, record_fields) + record.title = title + record.notes = notes + return _typed_record_to_data(record, title, notes) + + +class NsfListCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-list', + description='List NSF folders and records', + ) + NsfListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--folders', action='store_true', help='Show only folders') + parser.add_argument('--records', action='store_true', help='Show only records') + parser.add_argument( + '--format', dest='format', choices=['table', 'csv', 'json'], default='table', + ) + parser.add_argument( + '--output', dest='output', type=str, + help='Path to output file (ignored for table format)', + ) + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + show_folders = kwargs.get('folders', False) + show_records = kwargs.get('records', False) + fmt = kwargs.get('format', 'table') + if not show_folders and not show_records: + show_folders = show_records = True + + def _run(): + return nsf_management.list_nsf_items( + vault, + include_folders=show_folders, + include_records=show_records, + ) + + rows_data: List[NsfListRow] = _wrap_nsf('nsf-list', _run) + if not rows_data: + if show_folders and show_records: + logger.info('No NSF folders or records found. Run sync-down first.') + elif show_folders: + logger.info('No NSF folders found.') + else: + logger.info('No NSF records found.') + return + + table = [] + if fmt in ('json', 'csv'): + headers = ['Item Type', 'UID', 'Title', 'Type', 'Description', 'Parent/Folder'] + for r in rows_data: + table.append([r.item_type, r.uid, r.title, r.record_type, r.description, r.parent_or_folder]) + else: + headers = ['Item Type', 'UID', 'Title', 'Type', 'Description'] + for r in rows_data: + table.append([r.item_type, r.uid, r.title, r.record_type, r.description]) + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + return report_utils.dump_report_data( + table, headers, fmt=fmt, filename=kwargs.get('output'), + row_number=True, column_width=40, + ) + + +class NsfGetCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-get', + description='Get details of an NSF record or folder by UID or title', + ) + NsfGetCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('uid', type=str, help='Record UID, folder UID, or title') + parser.add_argument( + '--format', dest='format', choices=['detail', 'json'], default='detail', + help='Output format: detail (default) or json', + ) + parser.add_argument( + '--verbose', '-v', dest='verbose', action='store_true', + help='Show full permission breakdown for each accessor', + ) + parser.add_argument( + '--unmask', dest='unmask', action='store_true', + help='Reveal masked field values (passwords, secrets)', + ) + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + uid = (kwargs.get('uid') or '').strip() + if not uid: + raise base.CommandError('UID parameter is required') + + fmt = kwargs.get('format') or 'detail' + verbose = kwargs.get('verbose', False) + unmask = kwargs.get('unmask', False) + + def _run(): + return nsf_management.get_nsf_item(vault, uid) + + detail = _wrap_nsf('nsf-get', _run) + if detail.get('item_type') == 'folder': + if fmt == 'json': + self._print_folder_json(detail, verbose) + else: + self._print_folder_detail(detail, verbose) + return + + if unmask: + logger.warning( + 'nsf-get: --unmask was requested for record %s; ' + 'sensitive values are printed to stdout only.', + detail.get('record_uid', uid), + ) + if fmt == 'json': + self._print_record_json(vault, detail, verbose, unmask) + else: + self._print_record_detail(detail, verbose, unmask) + + @staticmethod + def _print_folder_detail(detail: Dict[str, Any], verbose: bool) -> None: + logger.info('') + logger.info('{0:>25s}: {1}'.format('NSF Folder UID', detail.get('nsf_folder_uid', ''))) + logger.info('{0:>25s}: {1}'.format('Name', detail.get('name', ''))) + logger.info('{0:>25s}: {1}'.format('Parent', detail.get('parent_uid', ''))) + NsfGetCommand._print_folder_access( + detail.get('access') or {}, + verbose, + owner_username=detail.get('owner_username'), + owner_account_uid=detail.get('owner_account_uid'), + ) + + @staticmethod + def _print_folder_json(detail: Dict[str, Any], verbose: bool) -> None: + fo = { + 'nsf_folder_uid': detail.get('nsf_folder_uid'), + 'name': detail.get('name'), + 'parent_uid': detail.get('parent_uid'), + } + if detail.get('owner_username'): + fo['owner'] = detail['owner_username'] + access = detail.get('access') or {} + for fr in access.get('results') or []: + if not fr.get('success'): + continue + accessors = fr.get('accessors') or [] + if accessors: + owner_username = detail.get('owner_username') + owner_account_uid = detail.get('owner_account_uid') + fo['accessors'] = accessors if verbose else [ + { + 'username': a.get('username'), + 'role': nsf_common.folder_access_role_label( + a, owner_username, owner_account_uid), + } + for a in accessors + ] + logger.info(json.dumps(fo, indent=2)) + + @staticmethod + def _print_folder_access( + access: Dict[str, Any], + verbose: bool, + owner_username: Optional[str] = None, + owner_account_uid: Optional[str] = None) -> None: + for fr in access.get('results') or []: + if not fr.get('success'): + err = fr.get('error') or {} + logger.warning(' Access error: %s — %s', err.get('status'), err.get('message')) + continue + accessors = fr.get('accessors') or [] + if not accessors: + continue + logger.info('') + logger.info('{0:>25s}:'.format('Folder Access')) + for a in accessors: + label = a.get('username', '') or a.get('accessor_uid', '') + role = nsf_common.folder_access_role_label( + a, owner_username, owner_account_uid) + logger.info('{0:>25s}: {1}'.format(label, role)) + if verbose and a.get('permissions'): + logger.info('{0:>25s}: {1}'.format('', json.dumps(a.get('permissions', {})))) + + def _print_record_detail(self, detail: Dict[str, Any], verbose: bool, unmask: bool) -> None: + record_uid = detail.get('record_uid', '') + logger.info('') + logger.info('{0:>20s}: {1}'.format('UID', record_uid)) + logger.info('{0:>20s}: {1}'.format('Type', detail.get('type') or '')) + if detail.get('title'): + logger.info('{0:>20s}: {1}'.format('Title', detail['title'])) + if detail.get('folder'): + logger.info('{0:>20s}: {1}'.format('Folder', detail['folder'])) + + fields = _mask_sensitive_fields(detail.get('fields') or [], unmask=unmask) + for label, key in (('Login', 'login'), ('Password', 'password'), ('URL', 'url')): + val = self._extract_field_value(fields, key) + if val: + logger.info('{0:>20s}: {1}'.format(label, val)) + + shown = {'login', 'password', 'url'} + for f in fields: + if not isinstance(f, dict): + continue + ftype = f.get('type', '') + if ftype in shown: + continue + label = f.get('label') or ftype.replace('_', ' ').title() + values = f.get('value', []) + if not isinstance(values, list): + values = [values] + for val in values: + if not val and val != 0: + continue + if isinstance(val, dict): + dval = ', '.join(f'{k}: {v}' for k, v in val.items() if v) + else: + dval = str(val) + logger.info('{0:>20s}: {1}'.format(label, dval)) + + notes = detail.get('notes') or '' + if notes: + for i, line in enumerate(notes.split('\n')): + logger.info('{0:>21s} {1}'.format('Notes:' if i == 0 else '', line.strip())) + + self._print_record_permissions(detail.get('record_accesses') or [], verbose) + + def _print_record_json( + self, + vault, + detail: Dict[str, Any], + verbose: bool, + unmask: bool) -> None: + ro: Dict[str, Any] = { + 'record_uid': detail.get('record_uid'), + 'title': detail.get('title'), + 'type': detail.get('type'), + 'version': detail.get('version'), + 'revision': detail.get('revision'), + } + if detail.get('folder'): + ro['folder'] = detail['folder'] + if detail.get('fields'): + ro['fields'] = _mask_sensitive_fields(detail['fields'], unmask=unmask) + if detail.get('notes'): + ro['notes'] = detail['notes'] + + accesses = detail.get('record_accesses') or [] + if accesses: + ro['user_permissions'] = [ + { + 'username': a.get('accessor_name') or a.get('access_type_uid', ''), + 'owner': a.get('owner', False), + 'editable': a.get('can_edit', False), + 'role': nsf_common.access_role_label(a), + **({flag: a.get(flag) for flag in ( + 'can_view_title', 'can_edit', 'can_view', 'can_list_access', + 'can_update_access', 'can_delete', + )} if verbose else {}), + } + for a in accesses + ] + logger.info(json.dumps(ro, indent=2)) + + @staticmethod + def _extract_field_value(fields: List[Any], field_type: str) -> str: + for f in fields: + if not isinstance(f, dict) or f.get('type', '') != field_type: + continue + values = f.get('value', []) + if not isinstance(values, list): + values = [values] + for val in values: + if val: + if isinstance(val, dict): + return ', '.join(f'{k}: {v}' for k, v in val.items() if v) + return str(val) + return '' + + @staticmethod + def _print_record_permissions(accesses: List[Dict[str, Any]], verbose: bool) -> None: + if not accesses: + return + logger.info('') + logger.info('User Permissions:') + for a in accesses: + accessor = a.get('accessor_name') or a.get('access_type_uid', '') + logger.info('') + logger.info(' User: ' + accessor) + if a.get('owner'): + logger.info(' Owner: Yes') + else: + logger.info(' Role: ' + nsf_common.access_role_label(a)) + can_edit = a.get('can_edit', False) + can_share = a.get('can_approve_access', False) or a.get('can_update_access', False) + logger.info(' Shareable: ' + ('Yes' if can_share else 'No')) + logger.info(' Read-Only: ' + ('Yes' if not can_edit else 'No')) + if verbose: + logger.info(f' {"Permission":<20} Value') + logger.info(f' {"-"*20} -----') + for flag in ( + 'can_view_title', 'can_edit', 'can_view', 'can_list_access', + 'can_update_access', 'can_delete', 'can_change_ownership', + ): + logger.info(f' {flag:<20} {"Y" if a.get(flag) else "N"}') + + +class NsfRecordAddCommand(base.ArgparseCommand, _NsfRecordDataMixin): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-record-add', + description='Add a record to NSF', + ) + NsfRecordAddCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--syntax-help', dest='syntax_help', action='store_true', + help='Display help on field parameters.') + parser.add_argument('-f', '--force', dest='force', action='store_true', help='ignore warnings') + parser.add_argument('-t', '--title', dest='title', type=str, help='record title') + parser.add_argument('-rt', '--record-type', dest='record_type', type=str, help='record type') + parser.add_argument('-n', '--notes', dest='notes', type=str, help='record notes') + parser.add_argument('--folder', dest='folder_uid', metavar='FOLDER', type=str, + 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') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + if kwargs.get('syntax_help'): + prompt_utils.output_text(record_fields_description) + return + + title = kwargs.get('title') + if not title: + raise base.CommandError('Title parameter is required.') + record_type = kwargs.get('record_type') + if not record_type: + raise base.CommandError('Record type parameter is required.') + + record_fields: List[ParsedFieldValue] = [] + add_attachments: List[ParsedFieldValue] = [] + for field in kwargs.get('fields', []): + parsed = RecordEditMixin.parse_field(field) + if parsed.type == 'file': + add_attachments.append(parsed) + else: + record_fields.append(parsed) + + self.warnings.clear() + data = self.build_nsf_record_data( + context, record_type, title, kwargs.get('notes'), record_fields) + + if self.warnings: + for w in self.warnings: + logger.warning(w) + if not kwargs.get('force'): + return + + if add_attachments: + logger.warning( + 'File attachments are not yet supported in nsf-record-add. ' + 'Use record-add for attachment support.') + if not kwargs.get('force'): + return + + folder_uid = kwargs.get('folder_uid') + if folder_uid: + resolved = nsf_management.resolve_nsf_folder_uid(vault, folder_uid) + if resolved is None or resolved == '': + raise base.CommandError(f'No such NSF folder: {folder_uid}') + folder_uid = resolved + + def _run(): + return nsf_management.create_nsf_record( + vault, + title=title, + record_type=record_type, + folder_uid=folder_uid, + record_data=data, + ) + + result = _wrap_nsf('nsf-record-add', _run) + logger.info('NSF record created: %s', result.record_uid) + return result.record_uid + + +class NsfRecordUpdateCommand(base.ArgparseCommand, _NsfRecordDataMixin): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-record-update', + description='Update an NSF record', + ) + NsfRecordUpdateCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--syntax-help', dest='syntax_help', action='store_true', + help='Display help on field parameters.') + parser.add_argument('-f', '--force', dest='force', action='store_true', help='ignore warnings') + parser.add_argument('-t', '--title', dest='title', type=str, help='modify record title') + parser.add_argument('-rt', '--record-type', dest='record_type', type=str, help='record type') + parser.add_argument('-n', '--notes', dest='notes', type=str, help='modify record notes') + parser.add_argument('-r', '--record', dest='record_uids', metavar='RECORD', type=str, + action='append', help='record UID or title') + parser.add_argument('fields', nargs='*', type=str, + help='load record type data from strings with dot notation') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + if kwargs.get('syntax_help'): + prompt_utils.output_text(record_fields_description) + return + + record_uids = kwargs.get('record_uids') or [] + if not record_uids: + raise base.CommandError('Record UID is required (use -r or --record)') + + record_type = kwargs.get('record_type') + if record_type and record_type not in ('legacy', 'general'): + rt = vault.vault_data.get_record_type_by_name(record_type) + if not rt: + raise base.CommandError(f'Record type "{record_type}" cannot be found.') + + fields: Dict[str, Any] = {} + for spec in [f.strip() for f in kwargs.get('fields', []) if f.strip()]: + try: + parsed = RecordEditMixin.parse_field(spec) + if parsed.type in fields: + existing = fields[parsed.type] + fields[parsed.type] = ( + ([existing] if not isinstance(existing, list) else existing) + [parsed.value] + ) + else: + fields[parsed.type] = parsed.value + except ValueError as e: + raise base.CommandError(f'Invalid field specification: {e}') from e + + title = kwargs.get('title') + notes = kwargs.get('notes') + + for identifier in record_uids: + def _run(uid=identifier): + return nsf_management.update_nsf_record( + vault, + uid, + title=title, + record_type=record_type, + fields=fields or None, + notes=notes, + ) + + result = _wrap_nsf('nsf-record-update', _run) + logger.info('NSF record updated: %s (%s)', result.record_uid, result.status) + + +def _record_title_from_vault(vault, record_uid: str) -> str: + entry = vault.nsf_data.get_record(record_uid) if vault.nsf_data else None + if entry and entry.decrypted_data: + try: + payload = json.loads(entry.decrypted_data) + if isinstance(payload, dict) and payload.get('title'): + return str(payload['title']) + except json.JSONDecodeError: + pass + return record_uid + + +def _folder_name_from_vault(vault, folder_uid: str) -> str: + if vault.nsf_data: + folder = vault.nsf_data.get_folder(folder_uid) + if folder and folder.name: + return folder.name + return folder_uid + + +def _print_remove_preview_items( + vault, + items: List[NsfRemovePreviewItem], + *, + item_label: str, + operation: str, + name_fn, + quiet: bool = False) -> bool: + """Print preview lines. Returns True if any error.""" + any_error = False + for pr in items: + name = name_fn(vault, pr.item_uid) + if pr.error: + any_error = True + logger.error( + f" {name} [{pr.item_uid}]: " + f"{pr.error.get('code', '')} — {pr.error.get('message', '')}" + ) + else: + action = 'permanently deleted' if operation == 'delete-permanent' else operation + logger.info(f"\nThe following {item_label} will be {action}:") + logger.info(f" {name} [{pr.item_uid}]") + if pr.impact and not quiet: + parts = [] + for key, label in ( + ('folders_count', 'sub-folder(s)'), + ('records_count', 'record(s)'), + ('affected_users_count', 'user(s)'), + ('affected_teams_count', 'team(s)'), + ): + count = pr.impact.get(key, 0) + if count: + parts.append(f"{count} {label}") + if parts: + logger.info(f" This will affect: {', '.join(parts)}") + for w in pr.impact.get('warnings') or []: + logger.info(f" Warning: {w}") + return any_error + + +def _confirm_removal(prompt: str, force: bool) -> bool: + if force: + return True + return prompt_utils.user_choice(prompt, 'yn', default='n') in ('y', 'yes') + + +class NsfRecordDetailsCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-record-details', + description='Get NSF record metadata (title, type, revision) using v3 API', + ) + NsfRecordDetailsCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('record_uids', nargs='+', type=str, + help='Record UIDs or titles') + parser.add_argument( + '--format', dest='format', choices=['table', 'json'], default='table', + help='Output format (default: table)', + ) + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + identifiers = kwargs.get('record_uids') or [] + fmt = kwargs.get('format', 'table') + + def _run(): + return nsf_management.get_nsf_record_details(vault, identifiers) + + result = _wrap_nsf('nsf-record-details', _run) + if fmt == 'json': + logger.info(json.dumps(result, indent=2)) + return + + for record in result.get('data', []): + logger.info('Record UID: %s', record['record_uid']) + logger.info(' Title: %s', record['title']) + logger.info(' Type: %s', record.get('type', 'Unknown')) + logger.info(' Version: %s', record.get('version', 0)) + logger.info(' Revision: %s', record.get('revision', 0)) + logger.info('') + forbidden = result.get('forbidden_records') or [] + if forbidden: + logger.warning('Forbidden records: %d', len(forbidden)) + for uid in forbidden: + logger.warning(' %s', uid) + logger.info('Total records retrieved: %d', len(result.get('data', []))) + + +class NsfMkdirCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-mkdir', + description='Create a new NSF folder using v3 API', + ) + NsfMkdirCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('folder', type=str, + help='Folder name (use "//" to embed a literal "/" in the name)') + parser.add_argument('--color', type=str, + choices=['none', 'red', 'orange', 'yellow', 'green', 'blue', 'gray'], + help='Folder color') + parser.add_argument('--no-inherit', dest='no_inherit_permissions', action='store_true', + help='Do not inherit parent folder permissions') + + @staticmethod + def _parse_path(folder_path: str) -> List[str]: + """Split *folder_path* into segment names (``//`` → literal ``/`` in a name).""" + sentinel = '\x00' + collapsed = folder_path.replace('//', sentinel) + raw_segments = collapsed.split('/') + segments = [] + for raw in raw_segments: + name = raw.replace(sentinel, '/').strip() + if name: + segments.append(name) + if not segments: + raise base.CommandError('Invalid folder name') + return segments + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + folder_path = (kwargs.get('folder') or '').strip() + if not folder_path: + raise base.CommandError('Folder name is required') + + color = kwargs.get('color') + inherit_permissions = not kwargs.get('no_inherit_permissions', False) + + parent_uid = None + current_folder = context.current_folder + if current_folder and nsf_management.is_nsf_folder(vault, current_folder): + parent_uid = current_folder + + segments = self._parse_path(folder_path) + last_idx = len(segments) - 1 + created_uid: Optional[str] = None + + for idx, segment in enumerate(segments): + is_leaf = idx == last_idx + existing = nsf_management.find_nsf_child_folder(vault, segment, parent_uid) + if existing: + if is_leaf: + logger.warning('nsf-mkdir: Folder "%s" already exists', segment) + return existing + parent_uid = existing + continue + + seg_color = color if is_leaf else None + seg_inherit = inherit_permissions if is_leaf else True + + def _run(name=segment, parent=parent_uid, seg_color=seg_color, seg_inherit=seg_inherit): + return nsf_management.create_nsf_folder( + vault, + name, + parent_uid=parent, + color=seg_color, + inherit_permissions=seg_inherit, + ) + + result = _wrap_nsf('nsf-mkdir', _run) + created_uid = result.folder_uid + parent_uid = created_uid + + if created_uid: + logger.info('NSF folder created: %s', created_uid) + return created_uid + + +class NsfRndirCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-rndir', + description='Rename or recolor an NSF folder', + ) + NsfRndirCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('-n', '--name', dest='folder_name', action='store', metavar='NAME', + help='folder new name') + parser.add_argument('--color', type=str, + choices=['none', 'red', 'orange', 'yellow', 'green', 'blue', 'gray'], + help='folder color') + parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', + help='suppress success message') + parser.add_argument('folder', nargs='?', type=str, help='folder path or UID') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + folder_arg = kwargs.get('folder') + if not folder_arg: + raise base.CommandError('Enter the path or UID of existing folder.') + + new_name = kwargs.get('folder_name') + color = kwargs.get('color') + if new_name is not None: + new_name = new_name.strip() + if not new_name: + raise base.CommandError('Folder name cannot be empty') + if new_name is None and color is None: + raise base.CommandError('New folder name and/or color parameters are required.') + + def _run(): + return nsf_management.update_nsf_folder( + vault, folder_arg, folder_name=new_name, color=color) + + result = _wrap_nsf('nsf-rndir', _run) + if not kwargs.get('quiet'): + display = _folder_name_from_vault(vault, result.folder_uid) + if new_name: + logger.info('Folder "%s" has been renamed to "%s"', display, new_name) + elif color: + logger.info('Folder "%s" color has been updated', display) + else: + logger.info('Folder "%s" has been updated', display) + + +class NsfRmCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-rm', + description='Remove NSF record(s). Supports owner-trash, folder-trash, or unlink.', + ) + NsfRmCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('records', nargs='+', metavar='RECORD', + help='Record UID(s) or title(s) to remove (max 500)') + parser.add_argument('--folder', dest='folder_uid', metavar='FOLDER', + help='Folder UID or name for operation context') + parser.add_argument('--operation', '-o', dest='operation', + choices=['owner-trash', 'folder-trash', 'unlink'], default='owner-trash', + help='Removal operation (default: owner-trash)') + _confirm = parser.add_mutually_exclusive_group() + _confirm.add_argument('--force', '-f', action='store_true', + help='Skip confirmation after preview') + _confirm.add_argument('--dry-run', dest='dry_run', action='store_true', + help='Preview only; do not delete') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + record_args = kwargs.get('records') or [] + operation = kwargs.get('operation', 'owner-trash') + folder_arg = kwargs.get('folder_uid') + force = kwargs.get('force', False) + dry_run = kwargs.get('dry_run', False) + + if not record_args: + raise base.CommandError('At least one record UID or title is required') + if operation == 'unlink' and not folder_arg: + raise base.CommandError('--folder is required when --operation is "unlink"') + record_limit = 500 + if len(record_args) > record_limit: + raise base.CommandError('Maximum 500 records per invocation') + + def _build(): + return nsf_management.build_nsf_record_removals( + vault, record_args, + operation_type=operation, + folder_uid=folder_arg, + ) + + removals = _wrap_nsf('nsf-rm', _build) + self._preview_and_confirm(vault, removals, operation, force, dry_run) + + def _preview_and_confirm( + self, + vault, + removals: List[Dict[str, str]], + operation: str, + force: bool, + dry_run: bool) -> None: + def _preview(): + return nsf_management.remove_nsf_records(vault, removals, dry_run=True) + + preview: NsfRemoveResult = _wrap_nsf('nsf-rm', _preview) + any_error = _print_remove_preview_items( + vault, preview.preview_results, + item_label='record', operation=operation, + name_fn=_record_title_from_vault, + ) + if any_error: + logger.info('\nOne or more records could not be previewed. Aborting.') + return + if dry_run: + logger.info('\n[Dry-run] No records were deleted.') + return + if not _confirm_removal('Do you want to proceed with deletion?', force): + return + + def _confirm(): + return nsf_management.remove_nsf_records(vault, removals, dry_run=False) + + result = _wrap_nsf('nsf-rm', _confirm) + if result.confirmed: + logger.info('Record removal completed.') + else: + logger.warning('Record removal was not confirmed by the server.') + + +class NsfRmdirCommand(base.ArgparseCommand): + + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-rmdir', + description='Remove NSF folder(s) and their contents', + ) + NsfRmdirCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('folders', nargs='+', metavar='FOLDER', + help='Folder UID(s) or name(s) to remove (max 100)') + parser.add_argument('--operation', '-o', dest='operation', + choices=['folder-trash', 'delete-permanent'], default='folder-trash', + help='Removal operation (default: folder-trash)') + parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', + help='Suppress per-folder impact detail') + _confirm = parser.add_mutually_exclusive_group() + _confirm.add_argument('--force', '-f', action='store_true', + help='Skip confirmation after preview') + _confirm.add_argument('--dry-run', dest='dry_run', action='store_true', + help='Preview only; do not delete') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + folder_args = kwargs.get('folders') or [] + operation = kwargs.get('operation', 'folder-trash') + force = kwargs.get('force', False) + dry_run = kwargs.get('dry_run', False) + quiet = kwargs.get('quiet', False) + + if not folder_args: + raise base.CommandError('Enter the name or UID of at least one folder.') + folder_limit = 100 + if len(folder_args) > folder_limit: + raise base.CommandError('Maximum 100 folders per invocation') + + removals: List[Dict[str, str]] = [] + for identifier in folder_args: + folder_uid = nsf_management.resolve_nsf_folder_uid(vault, identifier) + if not folder_uid: + raise base.CommandError(f'Folder "{identifier}" not found') + removals.append({'folder_uid': folder_uid, 'operation_type': operation}) + + if operation == 'delete-permanent' and not force and not dry_run: + logger.info( + '\n *** WARNING ***\n' + ' --operation delete-permanent is IRREVERSIBLE.\n' + ' All sub-folders and records inside will be permanently destroyed.\n') + + self._preview_and_confirm(vault, removals, operation, force, dry_run, quiet) + + def _preview_and_confirm( + self, + vault, + removals: List[Dict[str, str]], + operation: str, + force: bool, + dry_run: bool, + quiet: bool) -> None: + def _preview(): + return nsf_management.remove_nsf_folders(vault, removals, dry_run=True) + + preview: NsfRemoveResult = _wrap_nsf('nsf-rmdir', _preview) + any_error = _print_remove_preview_items( + vault, preview.preview_results, + item_label='folder', operation=operation, + name_fn=_folder_name_from_vault, + quiet=quiet, + ) + if any_error: + prefix = '[Dry-run] ' if dry_run else '' + logger.info(f"\n{prefix}The following folder(s) cannot be removed:") + logger.info('\nAborting — fix the errors above before retrying.') + return + if dry_run: + logger.info('\n[Dry-run] No folders were deleted.') + return + prompt = ( + 'Do you want to permanently delete the folder(s) and all their contents?' + if operation == 'delete-permanent' + else 'Do you want to proceed with the folder deletion?' + ) + if not _confirm_removal(prompt, force): + return + + def _confirm(): + return nsf_management.remove_nsf_folders(vault, removals, dry_run=False) + + result = _wrap_nsf('nsf-rmdir', _confirm) + if result.confirmed: + logger.info('Folder removal completed.') + else: + logger.warning('Folder removal was not confirmed by the server.') + + +class NsfLnCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-ln', + description='Link an NSF record into a folder (RECORD FOLDER)', + ) + NsfLnCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('src', type=str, help='record UID or title') + parser.add_argument('dst', type=str, help='destination folder UID or name') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + src, dst = kwargs.get('src'), kwargs.get('dst') + if not src or not dst: + raise base.CommandError('Both record and folder arguments are required') + + def _run(): + return nsf_folder_records.link_nsf_record_to_folder(vault, src, dst) + + result = _wrap_nsf('nsf-ln', _run) + logger.info('Record %s linked to folder %s', result.record_uid, result.folder_uid) + + +class NsfShareFolderCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-share-folder', + description='Change sharing permissions of an NSF folder', + ) + NsfShareFolderCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('-a', '--action', dest='action', choices=['grant', 'remove'], + default='grant', help='grant (default) or remove access') + parser.add_argument('-e', '--email', dest='user', action='append', metavar='USER', + help='email, team name/UID, or @existing for all folder accessors') + parser.add_argument('-r', '--role', dest='role', + choices=['viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager'], + default='viewer', + ) + parser.add_argument('folder', nargs='+', type=str, help='folder UID or name') + _expire = parser.add_mutually_exclusive_group() + _expire.add_argument('--expire-at', dest='expire_at', metavar='TIMESTAMP') + _expire.add_argument('--expire-in', dest='expire_in', metavar='PERIOD') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + folders = kwargs.get('folder') or [] + recipients = kwargs.get('user') or [] + action = kwargs.get('action') or 'grant' + if not folders: + raise base.CommandError('Folder path or UID is required') + if not recipients: + raise base.CommandError('Recipient is required (use -e/--email)') + + expiration = None + if action == 'grant': + try: + expiration = parse_nsf_share_expiration(kwargs.get('expire_at'), kwargs.get('expire_in')) + except Exception as e: + raise base.CommandError(str(e)) from e + + for folder_arg in folders: + targets = self._collect_targets(vault, recipients, folder_arg, context) + for recipient, is_team in targets: + def _run(rec=recipient, team=is_team, f=folder_arg): + if action == 'remove': + return nsf_sharing.revoke_nsf_folder_access( + vault, f, rec, as_team=team) + return nsf_sharing.grant_nsf_folder_access( + vault, f, rec, role=kwargs.get('role') or 'viewer', + expiration_timestamp=expiration, as_team=team) + + result = _wrap_nsf('nsf-share-folder', _run) + logger.info('%s: %s', recipient, result.get('message', result.get('status'))) + + @classmethod + def _collect_targets(cls, vault, recipients, folder_arg, context): + targets: List[tuple] = [] + seen: set = set() + for raw in recipients: + if raw in ('@existing', '@current'): + access = nsf_management.get_nsf_folder_access(vault, [ + nsf_management.resolve_nsf_folder_uid(vault, folder_arg) or folder_arg]) + for fr in access.get('results') or []: + if not fr.get('success'): + continue + for a in fr.get('accessors') or []: + username = a.get('username') + if username and username != context.auth.login: + key = ('user', username.casefold()) + if key not in seen: + seen.add(key) + targets.append((username, False)) + continue + if '@' in raw: + is_team = False + else: + teams = share_management_utils.get_share_objects(vault).get('teams', {}) + is_team = ( + raw in teams + or any((info.get('name') or '').casefold() == raw.casefold() + for info in teams.values()) + ) + key = ('team' if is_team else 'user', raw.casefold()) + if key not in seen: + seen.add(key) + targets.append((raw, is_team)) + return targets + + +class NsfShareRecordCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-share-record', + description='Change sharing permissions of an NSF record', + ) + NsfShareRecordCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('record', nargs='?', type=str, help='record UID, title, or folder') + parser.add_argument( + '-e', '--email', dest='email', action='append', required=True, + help='recipient email (repeatable)', + ) + parser.add_argument( + '-a', '--action', dest='action', choices=['grant', 'revoke', 'owner'], default='grant', + ) + parser.add_argument( + '-r', '--role', dest='role', + choices=['viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager'], + ) + parser.add_argument('-R', '--recursive', dest='recursive', action='store_true') + parser.add_argument('--dry-run', dest='dry_run', action='store_true') + _expire = parser.add_mutually_exclusive_group() + _expire.add_argument('--expire-at', dest='expire_at', metavar='EXPIRE_AT') + _expire.add_argument('--expire-in', dest='expire_in', metavar='PERIOD') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + record_arg = kwargs.get('record') + emails = kwargs.get('email') or [] + action = kwargs.get('action') or 'grant' + if not record_arg: + raise base.CommandError('Record path or UID is required') + if action == 'owner' and len(emails) > 1: + raise base.CommandError('Ownership can only be transferred to a single account') + if action == 'grant' and not kwargs.get('role'): + raise base.CommandError('Role is required for grant action') + + expiration = None + if action == 'grant': + try: + expiration = parse_nsf_share_expiration(kwargs.get('expire_at'), kwargs.get('expire_in')) + except Exception as e: + raise base.CommandError(str(e)) from e + + record_uids = _wrap_nsf( + 'nsf-share-record', + lambda: nsf_sharing.resolve_nsf_share_record_uids( + vault, record_arg, recursive=kwargs.get('recursive', False))) + + if kwargs.get('dry_run'): + logger.info(f'[dry-run] Action: {action.upper()}') + logger.info(f'[dry-run] Records: {", ".join(record_uids)}') + logger.info(f'[dry-run] Recipients: {", ".join(emails)}') + return + + for email in emails: + for record_uid in record_uids: + def _run(uid=record_uid, em=email): + return nsf_sharing.share_nsf_record_with_action( + vault, uid, em, action=action, role=kwargs.get('role'), + expiration_timestamp=expiration) + + result, effective = _wrap_nsf('nsf-share-record', _run) + if effective == 'owner' and result.success: + logger.info("Record '%s' ownership transferred to '%s'", record_uid, email) + logger.warning('You will no longer have access to this record!') + elif result.success: + logger.info('Record %s permissions %s for %s', record_uid, effective, email) + else: + msg = result.results[0]['message'] if result.results else 'failed' + logger.error('Share failed for %s: %s', record_uid, msg) + + +class NsfRecordPermissionCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-record-permission', + description='Bulk-update NSF record permissions within a folder', + ) + NsfRecordPermissionCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--dry-run', dest='dry_run', action='store_true') + parser.add_argument('-f', '--force', dest='force', action='store_true') + parser.add_argument('-R', '--recursive', dest='recursive', action='store_true') + parser.add_argument('-a', '--action', dest='action', choices=['grant', 'revoke'], required=True) + parser.add_argument( + '-r', '--role', dest='role', + choices=['viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager'], + ) + parser.add_argument('folder', nargs='?', type=str, help='folder UID or name') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + action = kwargs.get('action') + role = kwargs.get('role') + if action == 'grant' and not role: + raise base.CommandError('Role is required for grant action') + + login = context.auth.auth_context.username if context.auth and context.auth.auth_context else '' + plan = _wrap_nsf( + 'nsf-record-permission', + lambda: nsf_sharing.plan_nsf_record_permissions( + vault, kwargs.get('folder'), action=action, role=role, + recursive=kwargs.get('recursive', False), current_user=login)) + + if not plan.updates and not plan.creates and not plan.revokes: + if plan.skipped: + logger.warning('No permission changes can be made (see skipped entries).') + else: + logger.info('No permission changes are needed.') + return + + if kwargs.get('dry_run') or not kwargs.get('force'): + self._print_plan(plan) + if kwargs.get('dry_run'): + return + if not kwargs.get('force'): + if not _confirm_removal('Do you want to proceed with these permission changes?', False): + return + + outcomes = _wrap_nsf( + 'nsf-record-permission', + lambda: nsf_sharing.apply_nsf_record_permissions(vault, plan)) + for bucket, rows in outcomes.items(): + for item, result in rows: + if result.get('success'): + logger.info('%s %s %s: ok', bucket, item.get('record_uid'), item.get('email')) + elif not result.get('skipped'): + logger.warning('%s %s %s: %s', bucket, item.get('record_uid'), + item.get('email'), result.get('message')) + + @staticmethod + def _print_plan(plan) -> None: + for label, items in ( + ('SKIP', plan.skipped), ('GRANT/UPDATE', plan.updates + plan.creates), + ('REVOKE', plan.revokes)): + if not items: + continue + logger.info(f'\n{label}:') + for item in items: + line = f" {item.get('record_uid')} {item.get('email', '')} {item.get('cur_role', '')}" + if item.get('new_role'): + line += f" -> {item['new_role']}" + if item.get('reason'): + line += f" ({item['reason']})" + logger.info(line) + + +class NsfTransferRecordCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-transfer-record', + description='Transfer NSF record ownership to another user', + ) + NsfTransferRecordCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('record_uids', nargs='+', type=str, help='record UID(s) or title(s)') + parser.add_argument('new_owner_email', type=str, help='new owner email') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + identifiers = kwargs.get('record_uids') or [] + new_owner = kwargs.get('new_owner_email') + if not identifiers or not new_owner: + raise base.CommandError('Record UID(s) and new owner email are required') + + for identifier in identifiers: + def _run(uid=identifier): + return nsf_sharing.transfer_nsf_record_ownership(vault, uid, new_owner) + + result = _wrap_nsf('nsf-transfer-record', _run) + for row in result.results: + if row.get('success'): + logger.info("Record '%s' transferred to %s", row['record_uid'], new_owner) + logger.warning('You will no longer have access to this record!') + else: + logger.error('Transfer failed: %s', row.get('message')) + + +class NsfShortcutListCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-shortcut list', + description='List NSF records linked to multiple folders', + ) + NsfShortcutListCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('target', nargs='?', type=str, help='optional record or folder filter') + parser.add_argument('--format', dest='format', choices=['table', 'csv', 'json'], default='table') + parser.add_argument('--output', dest='output', type=str) + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + fmt = kwargs.get('format', 'table') + + def _run(): + return nsf_folder_records.list_nsf_shortcuts(vault, target=kwargs.get('target')) + + rows = _wrap_nsf('nsf-shortcut list', _run) + if not rows: + logger.info('No NSF shortcut records found') + return + + view = vault.nsf_data + table = [] + for row in rows: + if fmt == 'json': + folders = [ + {'folder_uid': fuid, + 'name': (view.get_folder(fuid).name if view and view.get_folder(fuid) else fuid)} + for fuid in row.folder_uids + ] + table.append([row.record_uid, row.title, folders]) + else: + names = [] + for fuid in row.folder_uids: + fname = view.get_folder(fuid).name if view and view.get_folder(fuid) else fuid + names.append(f'{fname} ({fuid})') + table.append([row.record_uid, row.title, names]) + + headers = ['Record UID', 'Record Title', 'Folders'] + if fmt != 'json': + headers = [report_utils.field_to_title(x) for x in headers] + return report_utils.dump_report_data( + table, headers, fmt=fmt, filename=kwargs.get('output'), row_number=True, column_width=40) + + +class NsfShortcutKeepCommand(base.ArgparseCommand): + def __init__(self): + parser = argparse.ArgumentParser( + prog='nsf-shortcut keep', + description='Keep an NSF record in one folder only', + ) + NsfShortcutKeepCommand.add_arguments_to_parser(parser) + super().__init__(parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument('target', type=str, help='record UID or title') + parser.add_argument('folder', nargs='?', type=str, help='folder to keep (default: current)') + parser.add_argument('-f', '--force', dest='force', action='store_true') + + def execute(self, context: KeeperParams, **kwargs): + vault = _require_vault(context) + target = kwargs.get('target') + folder_arg = kwargs.get('folder') + if not target: + raise base.CommandError('Record UID or title is required') + if not folder_arg: + if context.current_folder and nsf_management.is_nsf_folder(vault, context.current_folder): + folder_arg = context.current_folder + else: + raise base.CommandError('No folder specified and current folder is not an NSF folder') + + shortcuts = nsf_folder_records.get_nsf_shortcut_map(vault) + record_uid = nsf_management.resolve_nsf_record_uid(vault, target) + if not record_uid: + raise base.CommandError(f'Record "{target}" not found') + keep_folder = nsf_management.resolve_nsf_folder_uid(vault, folder_arg) or folder_arg + to_remove = [f for f in shortcuts.get(record_uid, set()) if f != keep_folder] + if not to_remove: + logger.info('Nothing to do — record is already in only one folder.') + return + if not kwargs.get('force'): + logger.info(f'Will keep record in {folder_arg} and remove from {len(to_remove)} other folder(s).') + if not _confirm_removal('Do you want to proceed?', False): + return + + def _run(): + return nsf_folder_records.keep_nsf_shortcut_in_folder(vault, target, folder_arg) + + results = _wrap_nsf('nsf-shortcut keep', _run) + logger.info('Removed record from %d folder link(s).', len(results)) + + +class NsfShortcutCommand(base.GroupCommand): + def __init__(self): + super().__init__('Manage NSF record shortcuts (multi-folder links)') + self.register_command(NsfShortcutListCommand(), 'list') + self.register_command(NsfShortcutKeepCommand(), 'keep') + self.default_verb = 'list' + diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index 20c4323f..10bae416 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -36,7 +36,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, record_type, secrets_manager, shares, password_report, trash, record_file_report, record_handling_commands, register, password_generate, verify_records, - shared_records_report, share_report) + shared_records_report, share_report, nsf_commands) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -90,6 +90,21 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('verify-records', verify_records.VerifyRecordsCommand(), base.CommandScope.Vault) commands.register_command('shared-records-report', shared_records_report.SharedRecordsReportCommand(), base.CommandScope.Vault) commands.register_command('share-report', share_report.ShareReportCommand(), base.CommandScope.Vault) + commands.register_command('nsf-list', nsf_commands.NsfListCommand(), base.CommandScope.Vault) + commands.register_command('nsf-get', nsf_commands.NsfGetCommand(), base.CommandScope.Vault) + commands.register_command('nsf-record-add', nsf_commands.NsfRecordAddCommand(), base.CommandScope.Vault) + commands.register_command('nsf-record-update', nsf_commands.NsfRecordUpdateCommand(), base.CommandScope.Vault) + commands.register_command('nsf-record-details', nsf_commands.NsfRecordDetailsCommand(), base.CommandScope.Vault) + commands.register_command('nsf-rm', nsf_commands.NsfRmCommand(), base.CommandScope.Vault) + commands.register_command('nsf-mkdir', nsf_commands.NsfMkdirCommand(), base.CommandScope.Vault) + commands.register_command('nsf-rndir', nsf_commands.NsfRndirCommand(), base.CommandScope.Vault) + commands.register_command('nsf-rmdir', nsf_commands.NsfRmdirCommand(), base.CommandScope.Vault) + commands.register_command('nsf-ln', nsf_commands.NsfLnCommand(), base.CommandScope.Vault) + commands.register_command('nsf-share-folder', nsf_commands.NsfShareFolderCommand(), base.CommandScope.Vault) + commands.register_command('nsf-share-record', nsf_commands.NsfShareRecordCommand(), base.CommandScope.Vault) + commands.register_command('nsf-record-permission', nsf_commands.NsfRecordPermissionCommand(), base.CommandScope.Vault) + commands.register_command('nsf-transfer-record', nsf_commands.NsfTransferRecordCommand(), base.CommandScope.Vault) + commands.register_command('nsf-shortcut', nsf_commands.NsfShortcutCommand(), base.CommandScope.Vault) if not scopes or bool(scopes & base.CommandScope.Enterprise): diff --git a/keepersdk-package/requirements.txt b/keepersdk-package/requirements.txt index cba5d495..54ae42e8 100644 --- a/keepersdk-package/requirements.txt +++ b/keepersdk-package/requirements.txt @@ -6,3 +6,4 @@ websockets>=13.1 fido2>=2.0.0; python_version>='3.10' email-validator>=2.0.0 pydantic>=2.6.4; python_version>='3.8' +google-api-core>=2.16.0 diff --git a/keepersdk-package/src/keepersdk/constants.py b/keepersdk-package/src/keepersdk/constants.py index 14a9a095..0aebc2f4 100644 --- a/keepersdk-package/src/keepersdk/constants.py +++ b/keepersdk-package/src/keepersdk/constants.py @@ -10,4 +10,4 @@ DEFAULT_KEEPER_SERVER = KEEPER_PUBLIC_HOSTS['US'] DEFAULT_DEVICE_NAME = 'Python Keeper API' -CLIENT_VERSION = 'c17.0.0' +CLIENT_VERSION = 'c18.0.0' diff --git a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py index 39de1cd0..1b85c2f4 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: APIRequest.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'APIRequest.proto' ) @@ -25,7 +25,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\"{\n\rQrcMessageKey\x12\x19\n\x11\x63lientEcPublicKey\x18\x01 \x01(\x0c\x12\x1c\n\x14mlKemEncapsulatedKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x12\n\nmsgVersion\x18\x04 \x01(\x05\x12\x0f\n\x07\x65\x63KeyId\x18\x05 \x01(\x05\"\xe6\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\x12\x34\n\rqrcMessageKey\x18\x08 \x01(\x0b\x32\x1d.Authentication.QrcMessageKey\"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\"v\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\x12\x15\n\rprofilePicUrl\x18\x05 \x01(\t\"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\"\xb7\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\x12\x16\n\x0eparentThreadId\x18\x07 \x01(\t\"\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\"\xc5\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\x12\x10\n\x08username\x18\x06 \x01(\t\"\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*r\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\x12\x19\n\x15LICENSE_SEAT_EXCEEDED\x10\x04*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') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x41PIRequest.proto\x12\x0e\x41uthentication\x1a\x10\x65nterprise.proto\"{\n\rQrcMessageKey\x12\x19\n\x11\x63lientEcPublicKey\x18\x01 \x01(\x0c\x12\x1c\n\x14mlKemEncapsulatedKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x12\n\nmsgVersion\x18\x04 \x01(\x05\x12\x0f\n\x07\x65\x63KeyId\x18\x05 \x01(\x05\"\xe6\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\x12\x34\n\rqrcMessageKey\x18\x08 \x01(\x0b\x32\x1d.Authentication.QrcMessageKey\"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\"v\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\x12\x15\n\rprofilePicUrl\x18\x05 \x01(\t\"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\"\xb7\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\x12\x16\n\x0eparentThreadId\x18\x07 \x01(\t\"\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\"\xc5\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\x12\x10\n\x08username\x18\x06 \x01(\t\"\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\"\x86\x01\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\x12\x12\n\naccountUid\x18\x06 \x01(\x0c\"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*r\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\x12\x19\n\x15LICENSE_SEAT_EXCEEDED\x10\x04*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,66 +33,66 @@ 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=21539 - _globals['_SUPPORTEDLANGUAGE']._serialized_end=21878 - _globals['_LOGINTYPE']._serialized_start=21880 - _globals['_LOGINTYPE']._serialized_end=21987 - _globals['_DEVICESTATUS']._serialized_start=21989 - _globals['_DEVICESTATUS']._serialized_end=22102 - _globals['_LICENSESTATUS']._serialized_start=22104 - _globals['_LICENSESTATUS']._serialized_end=22169 - _globals['_ACCOUNTTYPE']._serialized_start=22171 - _globals['_ACCOUNTTYPE']._serialized_end=22226 - _globals['_SESSIONTOKENTYPE']._serialized_start=22229 - _globals['_SESSIONTOKENTYPE']._serialized_end=22516 - _globals['_VERSION']._serialized_start=22518 - _globals['_VERSION']._serialized_end=22589 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=22591 - _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=22646 - _globals['_LOGINMETHOD']._serialized_start=22648 - _globals['_LOGINMETHOD']._serialized_end=22756 - _globals['_LOGINSTATE']._serialized_start=22759 - _globals['_LOGINSTATE']._serialized_end=23333 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=23335 - _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=23442 - _globals['_PASSWORDMETHOD']._serialized_start=23444 - _globals['_PASSWORDMETHOD']._serialized_end=23489 - _globals['_TWOFACTORPUSHTYPE']._serialized_start=23492 - _globals['_TWOFACTORPUSHTYPE']._serialized_end=23677 - _globals['_TWOFACTORVALUETYPE']._serialized_start=23680 - _globals['_TWOFACTORVALUETYPE']._serialized_end=23875 - _globals['_TWOFACTORCHANNELTYPE']._serialized_start=23878 - _globals['_TWOFACTORCHANNELTYPE']._serialized_end=24103 - _globals['_TWOFACTOREXPIRATION']._serialized_start=24106 - _globals['_TWOFACTOREXPIRATION']._serialized_end=24277 - _globals['_LICENSETYPE']._serialized_start=24279 - _globals['_LICENSETYPE']._serialized_end=24343 - _globals['_OBJECTTYPES']._serialized_start=24345 - _globals['_OBJECTTYPES']._serialized_end=24450 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=24453 - _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=24742 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=24744 - _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=24821 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=24823 - _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=24919 - _globals['_THROTTLETYPE']._serialized_start=24922 - _globals['_THROTTLETYPE']._serialized_end=25204 - _globals['_REGION']._serialized_start=25206 - _globals['_REGION']._serialized_end=25278 - _globals['_APPLICATIONSHARETYPE']._serialized_start=25280 - _globals['_APPLICATIONSHARETYPE']._serialized_end=25348 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=25351 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=25515 - _globals['_BACKUPKEYTYPE']._serialized_start=25517 - _globals['_BACKUPKEYTYPE']._serialized_end=25577 - _globals['_GENERICSTATUS']._serialized_start=25579 - _globals['_GENERICSTATUS']._serialized_end=25693 - _globals['_AUTHENTICATORATTACHMENT']._serialized_start=25695 - _globals['_AUTHENTICATORATTACHMENT']._serialized_end=25773 - _globals['_PASSKEYPURPOSE']._serialized_start=25775 - _globals['_PASSKEYPURPOSE']._serialized_end=25820 - _globals['_CLIENTFORMFACTOR']._serialized_start=25822 - _globals['_CLIENTFORMFACTOR']._serialized_end=25897 + _globals['_SUPPORTEDLANGUAGE']._serialized_start=21560 + _globals['_SUPPORTEDLANGUAGE']._serialized_end=21899 + _globals['_LOGINTYPE']._serialized_start=21901 + _globals['_LOGINTYPE']._serialized_end=22008 + _globals['_DEVICESTATUS']._serialized_start=22010 + _globals['_DEVICESTATUS']._serialized_end=22123 + _globals['_LICENSESTATUS']._serialized_start=22125 + _globals['_LICENSESTATUS']._serialized_end=22190 + _globals['_ACCOUNTTYPE']._serialized_start=22192 + _globals['_ACCOUNTTYPE']._serialized_end=22247 + _globals['_SESSIONTOKENTYPE']._serialized_start=22250 + _globals['_SESSIONTOKENTYPE']._serialized_end=22537 + _globals['_VERSION']._serialized_start=22539 + _globals['_VERSION']._serialized_end=22610 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_start=22612 + _globals['_MASTERPASSWORDREENTRYACTIONTYPE']._serialized_end=22667 + _globals['_LOGINMETHOD']._serialized_start=22669 + _globals['_LOGINMETHOD']._serialized_end=22777 + _globals['_LOGINSTATE']._serialized_start=22780 + _globals['_LOGINSTATE']._serialized_end=23354 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_start=23356 + _globals['_ENCRYPTEDDATAKEYTYPE']._serialized_end=23463 + _globals['_PASSWORDMETHOD']._serialized_start=23465 + _globals['_PASSWORDMETHOD']._serialized_end=23510 + _globals['_TWOFACTORPUSHTYPE']._serialized_start=23513 + _globals['_TWOFACTORPUSHTYPE']._serialized_end=23698 + _globals['_TWOFACTORVALUETYPE']._serialized_start=23701 + _globals['_TWOFACTORVALUETYPE']._serialized_end=23896 + _globals['_TWOFACTORCHANNELTYPE']._serialized_start=23899 + _globals['_TWOFACTORCHANNELTYPE']._serialized_end=24124 + _globals['_TWOFACTOREXPIRATION']._serialized_start=24127 + _globals['_TWOFACTOREXPIRATION']._serialized_end=24298 + _globals['_LICENSETYPE']._serialized_start=24300 + _globals['_LICENSETYPE']._serialized_end=24364 + _globals['_OBJECTTYPES']._serialized_start=24366 + _globals['_OBJECTTYPES']._serialized_end=24471 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_start=24474 + _globals['_ENCRYPTEDOBJECTTYPE']._serialized_end=24763 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_start=24765 + _globals['_MASTERPASSWORDREENTRYSTATUS']._serialized_end=24842 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_start=24844 + _globals['_ALTERNATEAUTHENTICATIONTYPE']._serialized_end=24940 + _globals['_THROTTLETYPE']._serialized_start=24943 + _globals['_THROTTLETYPE']._serialized_end=25225 + _globals['_REGION']._serialized_start=25227 + _globals['_REGION']._serialized_end=25299 + _globals['_APPLICATIONSHARETYPE']._serialized_start=25301 + _globals['_APPLICATIONSHARETYPE']._serialized_end=25369 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=25372 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=25536 + _globals['_BACKUPKEYTYPE']._serialized_start=25538 + _globals['_BACKUPKEYTYPE']._serialized_end=25598 + _globals['_GENERICSTATUS']._serialized_start=25600 + _globals['_GENERICSTATUS']._serialized_end=25714 + _globals['_AUTHENTICATORATTACHMENT']._serialized_start=25716 + _globals['_AUTHENTICATORATTACHMENT']._serialized_end=25794 + _globals['_PASSKEYPURPOSE']._serialized_start=25796 + _globals['_PASSKEYPURPOSE']._serialized_end=25841 + _globals['_CLIENTFORMFACTOR']._serialized_start=25843 + _globals['_CLIENTFORMFACTOR']._serialized_end=25918 _globals['_QRCMESSAGEKEY']._serialized_start=54 _globals['_QRCMESSAGEKEY']._serialized_end=177 _globals['_APIREQUEST']._serialized_start=180 @@ -323,114 +323,114 @@ _globals['_GETDATAKEYBACKUPV3RESPONSE']._serialized_end=15436 _globals['_GETPUBLICKEYSREQUEST']._serialized_start=15438 _globals['_GETPUBLICKEYSREQUEST']._serialized_end=15479 - _globals['_PUBLICKEYRESPONSE']._serialized_start=15481 - _globals['_PUBLICKEYRESPONSE']._serialized_end=15595 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=15597 - _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=15677 - _globals['_SETECCKEYPAIRREQUEST']._serialized_start=15679 - _globals['_SETECCKEYPAIRREQUEST']._serialized_end=15749 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=15751 - _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=15824 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=15826 - _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=15908 - _globals['_TEAMECCKEYPAIR']._serialized_start=15910 - _globals['_TEAMECCKEYPAIR']._serialized_end=15991 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=15993 - _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=16081 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=16083 - _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=16151 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=16153 - _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=16238 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=16240 - _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=16329 - _globals['_ADDAPPSHARESREQUEST']._serialized_start=16331 - _globals['_ADDAPPSHARESREQUEST']._serialized_end=16419 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=16421 - _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=16483 - _globals['_APPSHAREADD']._serialized_start=16486 - _globals['_APPSHAREADD']._serialized_end=16621 - _globals['_APPSHARE']._serialized_start=16624 - _globals['_APPSHARE']._serialized_end=16761 - _globals['_ADDAPPCLIENTREQUEST']._serialized_start=16764 - _globals['_ADDAPPCLIENTREQUEST']._serialized_end=16981 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=16983 - _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=17047 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=17050 - _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=17220 - _globals['_APPCLIENT']._serialized_start=17223 - _globals['_APPCLIENT']._serialized_end=17498 - _globals['_GETAPPINFOREQUEST']._serialized_start=17500 - _globals['_GETAPPINFOREQUEST']._serialized_end=17541 - _globals['_APPINFO']._serialized_start=17544 - _globals['_APPINFO']._serialized_end=17686 - _globals['_GETAPPINFORESPONSE']._serialized_start=17688 - _globals['_GETAPPINFORESPONSE']._serialized_end=17750 - _globals['_APPLICATIONSUMMARY']._serialized_start=17753 - _globals['_APPLICATIONSUMMARY']._serialized_end=17966 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=17968 - _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=18064 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=18066 - _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=18113 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=18115 - _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=18181 - _globals['_SENDSHAREINVITEREQUEST']._serialized_start=18183 - _globals['_SENDSHAREINVITEREQUEST']._serialized_end=18222 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=18225 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=18422 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=18424 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=18479 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=18482 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=18730 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=18732 - _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=18775 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=18777 - _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=18880 - _globals['_DOWNLOAD']._serialized_start=18882 - _globals['_DOWNLOAD']._serialized_end=18950 - _globals['_DELETEUSERREQUEST']._serialized_start=18952 - _globals['_DELETEUSERREQUEST']._serialized_end=18987 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=18990 - _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=19122 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=19124 - _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=19185 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=19187 - _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=19276 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=19279 - _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=19451 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=19453 - _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=19497 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=19500 - _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=19681 - _globals['_USERTEAMKEY']._serialized_start=19684 - _globals['_USERTEAMKEY']._serialized_end=19862 - _globals['_GENERICREQUESTRESPONSE']._serialized_start=19864 - _globals['_GENERICREQUESTRESPONSE']._serialized_end=19905 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=19907 - _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=20009 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=20011 - _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=20091 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=20094 - _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=20226 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=20229 - _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=20536 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=20539 - _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=20678 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=20681 - _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=20872 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=20874 - _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=20947 - _globals['_UPDATEPASSKEYREQUEST']._serialized_start=20949 - _globals['_UPDATEPASSKEYREQUEST']._serialized_end=21053 - _globals['_PASSKEYLISTREQUEST']._serialized_start=21055 - _globals['_PASSKEYLISTREQUEST']._serialized_end=21100 - _globals['_PASSKEYINFO']._serialized_start=21103 - _globals['_PASSKEYINFO']._serialized_end=21267 - _globals['_PASSKEYLISTRESPONSE']._serialized_start=21269 - _globals['_PASSKEYLISTRESPONSE']._serialized_end=21340 - _globals['_TRANSLATIONINFO']._serialized_start=21342 - _globals['_TRANSLATIONINFO']._serialized_end=21409 - _globals['_TRANSLATIONREQUEST']._serialized_start=21411 - _globals['_TRANSLATIONREQUEST']._serialized_end=21455 - _globals['_TRANSLATIONRESPONSE']._serialized_start=21457 - _globals['_TRANSLATIONRESPONSE']._serialized_end=21536 + _globals['_PUBLICKEYRESPONSE']._serialized_start=15482 + _globals['_PUBLICKEYRESPONSE']._serialized_end=15616 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_start=15618 + _globals['_GETPUBLICKEYSRESPONSE']._serialized_end=15698 + _globals['_SETECCKEYPAIRREQUEST']._serialized_start=15700 + _globals['_SETECCKEYPAIRREQUEST']._serialized_end=15770 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_start=15772 + _globals['_SETECCKEYPAIRSREQUEST']._serialized_end=15845 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_start=15847 + _globals['_SETECCKEYPAIRSRESPONSE']._serialized_end=15929 + _globals['_TEAMECCKEYPAIR']._serialized_start=15931 + _globals['_TEAMECCKEYPAIR']._serialized_end=16012 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_start=16014 + _globals['_TEAMECCKEYPAIRRESPONSE']._serialized_end=16102 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_start=16104 + _globals['_GETKSMPUBLICKEYSREQUEST']._serialized_end=16172 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_start=16174 + _globals['_DEVICEPUBLICKEYRESPONSE']._serialized_end=16259 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_start=16261 + _globals['_GETKSMPUBLICKEYSRESPONSE']._serialized_end=16350 + _globals['_ADDAPPSHARESREQUEST']._serialized_start=16352 + _globals['_ADDAPPSHARESREQUEST']._serialized_end=16440 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_start=16442 + _globals['_REMOVEAPPSHARESREQUEST']._serialized_end=16504 + _globals['_APPSHAREADD']._serialized_start=16507 + _globals['_APPSHAREADD']._serialized_end=16642 + _globals['_APPSHARE']._serialized_start=16645 + _globals['_APPSHARE']._serialized_end=16782 + _globals['_ADDAPPCLIENTREQUEST']._serialized_start=16785 + _globals['_ADDAPPCLIENTREQUEST']._serialized_end=17002 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_start=17004 + _globals['_REMOVEAPPCLIENTSREQUEST']._serialized_end=17068 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_start=17071 + _globals['_ADDEXTERNALSHAREREQUEST']._serialized_end=17241 + _globals['_APPCLIENT']._serialized_start=17244 + _globals['_APPCLIENT']._serialized_end=17519 + _globals['_GETAPPINFOREQUEST']._serialized_start=17521 + _globals['_GETAPPINFOREQUEST']._serialized_end=17562 + _globals['_APPINFO']._serialized_start=17565 + _globals['_APPINFO']._serialized_end=17707 + _globals['_GETAPPINFORESPONSE']._serialized_start=17709 + _globals['_GETAPPINFORESPONSE']._serialized_end=17771 + _globals['_APPLICATIONSUMMARY']._serialized_start=17774 + _globals['_APPLICATIONSUMMARY']._serialized_end=17987 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_start=17989 + _globals['_GETAPPLICATIONSSUMMARYRESPONSE']._serialized_end=18085 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_start=18087 + _globals['_GETVERIFICATIONTOKENREQUEST']._serialized_end=18134 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_start=18136 + _globals['_GETVERIFICATIONTOKENRESPONSE']._serialized_end=18202 + _globals['_SENDSHAREINVITEREQUEST']._serialized_start=18204 + _globals['_SENDSHAREINVITEREQUEST']._serialized_end=18243 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=18246 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=18443 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=18445 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=18500 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=18503 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=18751 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_start=18753 + _globals['_REQUESTDOWNLOADREQUEST']._serialized_end=18796 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_start=18798 + _globals['_REQUESTDOWNLOADRESPONSE']._serialized_end=18901 + _globals['_DOWNLOAD']._serialized_start=18903 + _globals['_DOWNLOAD']._serialized_end=18971 + _globals['_DELETEUSERREQUEST']._serialized_start=18973 + _globals['_DELETEUSERREQUEST']._serialized_end=19008 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_start=19011 + _globals['_CHANGEMASTERPASSWORDREQUEST']._serialized_end=19143 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_start=19145 + _globals['_CHANGEMASTERPASSWORDRESPONSE']._serialized_end=19206 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_start=19208 + _globals['_ACCOUNTRECOVERYSETUPREQUEST']._serialized_end=19297 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_start=19300 + _globals['_ACCOUNTRECOVERYVERIFYCODERESPONSE']._serialized_end=19472 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_start=19474 + _globals['_EMERGENCYACCESSLOGINREQUEST']._serialized_end=19518 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_start=19521 + _globals['_EMERGENCYACCESSLOGINRESPONSE']._serialized_end=19702 + _globals['_USERTEAMKEY']._serialized_start=19705 + _globals['_USERTEAMKEY']._serialized_end=19883 + _globals['_GENERICREQUESTRESPONSE']._serialized_start=19885 + _globals['_GENERICREQUESTRESPONSE']._serialized_end=19926 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_start=19928 + _globals['_PASSKEYREGISTRATIONREQUEST']._serialized_end=20030 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_start=20032 + _globals['_PASSKEYREGISTRATIONRESPONSE']._serialized_end=20112 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_start=20115 + _globals['_PASSKEYREGISTRATIONFINALIZATION']._serialized_end=20247 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_start=20250 + _globals['_PASSKEYAUTHENTICATIONREQUEST']._serialized_end=20557 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_start=20560 + _globals['_PASSKEYAUTHENTICATIONRESPONSE']._serialized_end=20699 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_start=20702 + _globals['_PASSKEYVALIDATIONREQUEST']._serialized_end=20893 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_start=20895 + _globals['_PASSKEYVALIDATIONRESPONSE']._serialized_end=20968 + _globals['_UPDATEPASSKEYREQUEST']._serialized_start=20970 + _globals['_UPDATEPASSKEYREQUEST']._serialized_end=21074 + _globals['_PASSKEYLISTREQUEST']._serialized_start=21076 + _globals['_PASSKEYLISTREQUEST']._serialized_end=21121 + _globals['_PASSKEYINFO']._serialized_start=21124 + _globals['_PASSKEYINFO']._serialized_end=21288 + _globals['_PASSKEYLISTRESPONSE']._serialized_start=21290 + _globals['_PASSKEYLISTRESPONSE']._serialized_end=21361 + _globals['_TRANSLATIONINFO']._serialized_start=21363 + _globals['_TRANSLATIONINFO']._serialized_end=21430 + _globals['_TRANSLATIONREQUEST']._serialized_start=21432 + _globals['_TRANSLATIONREQUEST']._serialized_end=21476 + _globals['_TRANSLATIONRESPONSE']._serialized_start=21478 + _globals['_TRANSLATIONRESPONSE']._serialized_end=21557 # @@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 f12b21e6..571e1409 100644 --- a/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/APIRequest_pb2.pyi @@ -3,8 +3,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -562,7 +561,7 @@ class NewUserMinimumParams(_message.Message): isEnterpriseDomain: bool enterpriseEccPublicKey: bytes forbidKeyType2: bool - def __init__(self, minimumIterations: _Optional[int] = ..., passwordMatchRegex: _Optional[_Iterable[str]] = ..., passwordMatchDescription: _Optional[_Iterable[str]] = ..., isEnterpriseDomain: _Optional[bool] = ..., enterpriseEccPublicKey: _Optional[bytes] = ..., forbidKeyType2: _Optional[bool] = ...) -> None: ... + def __init__(self, minimumIterations: _Optional[int] = ..., passwordMatchRegex: _Optional[_Iterable[str]] = ..., passwordMatchDescription: _Optional[_Iterable[str]] = ..., isEnterpriseDomain: bool = ..., enterpriseEccPublicKey: _Optional[bytes] = ..., forbidKeyType2: bool = ...) -> None: ... class PreLoginRequest(_message.Message): __slots__ = ("authRequest", "loginType", "twoFactorToken") @@ -650,7 +649,7 @@ class StartLoginRequest(_message.Message): v2TwoFactorToken: str accountUid: bytes 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: _Optional[bool] = ..., cloneCode: _Optional[bytes] = ..., v2TwoFactorToken: _Optional[str] = ..., accountUid: _Optional[bytes] = ..., fromSessionToken: _Optional[bytes] = ...) -> None: ... + 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") @@ -700,7 +699,7 @@ class SwitchListElement(_message.Message): authRequired: bool isLinked: bool profilePicUrl: str - def __init__(self, username: _Optional[str] = ..., fullName: _Optional[str] = ..., authRequired: _Optional[bool] = ..., isLinked: _Optional[bool] = ..., profilePicUrl: _Optional[str] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., fullName: _Optional[str] = ..., authRequired: bool = ..., isLinked: bool = ..., profilePicUrl: _Optional[str] = ...) -> None: ... class SwitchListResponse(_message.Message): __slots__ = ("elements",) @@ -888,7 +887,7 @@ class License(_message.Message): licenseStatus: LicenseStatus paid: bool message: str - def __init__(self, created: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseStatus: _Optional[_Union[LicenseStatus, str]] = ..., paid: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, created: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseStatus: _Optional[_Union[LicenseStatus, str]] = ..., paid: bool = ..., message: _Optional[str] = ...) -> None: ... class OwnerlessRecord(_message.Message): __slots__ = ("recordUid", "recordKey", "status") @@ -1046,7 +1045,7 @@ class EmailVerificationLinkResponse(_message.Message): __slots__ = ("emailVerified",) EMAILVERIFIED_FIELD_NUMBER: _ClassVar[int] emailVerified: bool - def __init__(self, emailVerified: _Optional[bool] = ...) -> None: ... + def __init__(self, emailVerified: bool = ...) -> None: ... class SecurityData(_message.Message): __slots__ = ("uid", "data") @@ -1118,7 +1117,7 @@ class SecurityReport(_message.Message): securityReportIncrementalData: _containers.RepeatedCompositeFieldContainer[SecurityReportIncrementalData] userId: int hasOldEncryption: bool - 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: _Optional[bool] = ...) -> None: ... + 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", "continuationToken") @@ -1152,7 +1151,7 @@ class SecurityReportResponse(_message.Message): complete: bool enterpriseEccPrivateKey: bytes hasIncrementalData: bool - def __init__(self, enterprisePrivateKey: _Optional[bytes] = ..., securityReport: _Optional[_Iterable[_Union[SecurityReport, _Mapping]]] = ..., asOfRevision: _Optional[int] = ..., fromPage: _Optional[int] = ..., toPage: _Optional[int] = ..., complete: _Optional[bool] = ..., enterpriseEccPrivateKey: _Optional[bytes] = ..., hasIncrementalData: _Optional[bool] = ...) -> None: ... + 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",) @@ -1230,7 +1229,7 @@ class GetChangeKeyTypesRequest(_message.Message): includeRecommended: bool includeKeys: bool includeAllowedKeyTypes: bool - def __init__(self, onlyTheseObjects: _Optional[_Iterable[_Union[EncryptedObjectType, str]]] = ..., limit: _Optional[int] = ..., includeRecommended: _Optional[bool] = ..., includeKeys: _Optional[bool] = ..., includeAllowedKeyTypes: _Optional[bool] = ...) -> None: ... + def __init__(self, onlyTheseObjects: _Optional[_Iterable[_Union[EncryptedObjectType, str]]] = ..., limit: _Optional[int] = ..., includeRecommended: bool = ..., includeKeys: bool = ..., includeAllowedKeyTypes: bool = ...) -> None: ... class GetChangeKeyTypesResponse(_message.Message): __slots__ = ("keys", "allowedKeyTypes") @@ -1496,7 +1495,7 @@ class ApproveDeviceRequest(_message.Message): encryptedDeviceDataKey: bytes denyApproval: bool linkDevice: bool - def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: _Optional[bool] = ..., linkDevice: _Optional[bool] = ...) -> None: ... + def __init__(self, encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: bool = ..., linkDevice: bool = ...) -> None: ... class EnterpriseUserAliasRequest(_message.Message): __slots__ = ("enterpriseUserId", "alias") @@ -1514,7 +1513,7 @@ class EnterpriseUserAddAliasRequest(_message.Message): enterpriseUserId: int alias: str primary: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., alias: _Optional[str] = ..., primary: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., alias: _Optional[str] = ..., primary: bool = ...) -> None: ... class EnterpriseUserAddAliasRequestV2(_message.Message): __slots__ = ("enterpriseUserAddAliasRequest",) @@ -1622,7 +1621,7 @@ class SsoServiceProviderResponse(_message.Message): spUrl: str isCloud: bool clientVersion: str - def __init__(self, name: _Optional[str] = ..., spUrl: _Optional[str] = ..., isCloud: _Optional[bool] = ..., clientVersion: _Optional[str] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., spUrl: _Optional[str] = ..., isCloud: bool = ..., clientVersion: _Optional[str] = ...) -> None: ... class UserSettingRequest(_message.Message): __slots__ = ("setting", "value") @@ -1642,7 +1641,7 @@ class ThrottleState(_message.Message): key: str value: str state: bool - def __init__(self, type: _Optional[_Union[ThrottleType, str]] = ..., key: _Optional[str] = ..., value: _Optional[str] = ..., state: _Optional[bool] = ...) -> None: ... + def __init__(self, type: _Optional[_Union[ThrottleType, str]] = ..., key: _Optional[str] = ..., value: _Optional[str] = ..., state: bool = ...) -> None: ... class ThrottleState2(_message.Message): __slots__ = ("key", "keyDescription", "value", "valueDescription", "identifier", "locked", "includedInAllClear", "expireSeconds") @@ -1662,7 +1661,7 @@ class ThrottleState2(_message.Message): locked: bool includedInAllClear: bool expireSeconds: int - def __init__(self, key: _Optional[str] = ..., keyDescription: _Optional[str] = ..., value: _Optional[str] = ..., valueDescription: _Optional[str] = ..., identifier: _Optional[str] = ..., locked: _Optional[bool] = ..., includedInAllClear: _Optional[bool] = ..., expireSeconds: _Optional[int] = ...) -> None: ... + def __init__(self, key: _Optional[str] = ..., keyDescription: _Optional[str] = ..., value: _Optional[str] = ..., valueDescription: _Optional[str] = ..., identifier: _Optional[str] = ..., locked: bool = ..., includedInAllClear: bool = ..., expireSeconds: _Optional[int] = ...) -> None: ... class DeviceInformation(_message.Message): __slots__ = ("deviceId", "deviceName", "clientVersion", "lastLogin", "deviceStatus") @@ -1684,7 +1683,7 @@ class UserSetting(_message.Message): VALUE_FIELD_NUMBER: _ClassVar[int] name: str value: bool - def __init__(self, name: _Optional[str] = ..., value: _Optional[bool] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., value: bool = ...) -> None: ... class UserDataKeyRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -1780,7 +1779,7 @@ class PasswordRules(_message.Message): description: str minimum: int value: str - def __init__(self, ruleType: _Optional[str] = ..., match: _Optional[bool] = ..., pattern: _Optional[str] = ..., description: _Optional[str] = ..., minimum: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... + def __init__(self, ruleType: _Optional[str] = ..., match: bool = ..., pattern: _Optional[str] = ..., description: _Optional[str] = ..., minimum: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... class GetDataKeyBackupV3Response(_message.Message): __slots__ = ("dataKeyBackup", "dataKeyBackupDate", "publicKey", "encryptedPrivateKey", "clientKey", "encryptedSessionToken", "passwordRules", "passwordRulesIntro", "minimumPbkdf2Iterations", "keyType") @@ -1813,18 +1812,20 @@ class GetPublicKeysRequest(_message.Message): def __init__(self, usernames: _Optional[_Iterable[str]] = ...) -> None: ... class PublicKeyResponse(_message.Message): - __slots__ = ("username", "publicKey", "publicEccKey", "message", "errorCode") + __slots__ = ("username", "publicKey", "publicEccKey", "message", "errorCode", "accountUid") USERNAME_FIELD_NUMBER: _ClassVar[int] PUBLICKEY_FIELD_NUMBER: _ClassVar[int] PUBLICECCKEY_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int] ERRORCODE_FIELD_NUMBER: _ClassVar[int] + ACCOUNTUID_FIELD_NUMBER: _ClassVar[int] username: str publicKey: bytes publicEccKey: bytes message: str errorCode: str - def __init__(self, username: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., publicEccKey: _Optional[bytes] = ..., message: _Optional[str] = ..., errorCode: _Optional[str] = ...) -> None: ... + accountUid: bytes + def __init__(self, username: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., publicEccKey: _Optional[bytes] = ..., message: _Optional[str] = ..., errorCode: _Optional[str] = ..., accountUid: _Optional[bytes] = ...) -> None: ... class GetPublicKeysResponse(_message.Message): __slots__ = ("keyResponses",) @@ -1920,7 +1921,7 @@ class AppShareAdd(_message.Message): shareType: ApplicationShareType encryptedSecretKey: bytes editable: bool - def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., encryptedSecretKey: _Optional[bytes] = ..., editable: _Optional[bool] = ...) -> None: ... + def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., encryptedSecretKey: _Optional[bytes] = ..., editable: bool = ...) -> None: ... class AppShare(_message.Message): __slots__ = ("secretUid", "shareType", "editable", "createdOn", "data") @@ -1934,7 +1935,7 @@ class AppShare(_message.Message): editable: bool createdOn: int data: bytes - def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., editable: _Optional[bool] = ..., createdOn: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... + def __init__(self, secretUid: _Optional[bytes] = ..., shareType: _Optional[_Union[ApplicationShareType, str]] = ..., editable: bool = ..., createdOn: _Optional[int] = ..., data: _Optional[bytes] = ...) -> None: ... class AddAppClientRequest(_message.Message): __slots__ = ("appRecordUid", "encryptedAppKey", "clientId", "lockIp", "firstAccessExpireOn", "accessExpireOn", "id", "appClientType") @@ -1954,7 +1955,7 @@ class AddAppClientRequest(_message.Message): accessExpireOn: int id: str appClientType: _enterprise_pb2.AppClientType - def __init__(self, appRecordUid: _Optional[bytes] = ..., encryptedAppKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., lockIp: _Optional[bool] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., encryptedAppKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., lockIp: bool = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ...) -> None: ... class RemoveAppClientsRequest(_message.Message): __slots__ = ("appRecordUid", "clients") @@ -1980,7 +1981,7 @@ class AddExternalShareRequest(_message.Message): id: str isSelfDestruct: bool isEditable: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedRecordKey: _Optional[bytes] = ..., clientId: _Optional[bytes] = ..., accessExpireOn: _Optional[int] = ..., id: _Optional[str] = ..., isSelfDestruct: _Optional[bool] = ..., isEditable: _Optional[bool] = ...) -> None: ... + 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", "canEdit") @@ -2008,7 +2009,7 @@ class AppClient(_message.Message): accessExpireOn: int appClientType: _enterprise_pb2.AppClientType canEdit: bool - def __init__(self, id: _Optional[str] = ..., clientId: _Optional[bytes] = ..., createdOn: _Optional[int] = ..., firstAccess: _Optional[int] = ..., lastAccess: _Optional[int] = ..., publicKey: _Optional[bytes] = ..., lockIp: _Optional[bool] = ..., ipAddress: _Optional[str] = ..., firstAccessExpireOn: _Optional[int] = ..., accessExpireOn: _Optional[int] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., canEdit: _Optional[bool] = ...) -> None: ... + 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",) @@ -2026,7 +2027,7 @@ class AppInfo(_message.Message): shares: _containers.RepeatedCompositeFieldContainer[AppShare] clients: _containers.RepeatedCompositeFieldContainer[AppClient] isExternalShare: bool - def __init__(self, appRecordUid: _Optional[bytes] = ..., shares: _Optional[_Iterable[_Union[AppShare, _Mapping]]] = ..., clients: _Optional[_Iterable[_Union[AppClient, _Mapping]]] = ..., isExternalShare: _Optional[bool] = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., shares: _Optional[_Iterable[_Union[AppShare, _Mapping]]] = ..., clients: _Optional[_Iterable[_Union[AppClient, _Mapping]]] = ..., isExternalShare: bool = ...) -> None: ... class GetAppInfoResponse(_message.Message): __slots__ = ("appInfo",) @@ -2158,7 +2159,7 @@ class ChangeMasterPasswordRequest(_message.Message): encryptionParams: bytes fromServiceProvider: bool iterationsChange: bool - def __init__(self, authVerifier: _Optional[bytes] = ..., encryptionParams: _Optional[bytes] = ..., fromServiceProvider: _Optional[bool] = ..., iterationsChange: _Optional[bool] = ...) -> None: ... + def __init__(self, authVerifier: _Optional[bytes] = ..., encryptionParams: _Optional[bytes] = ..., fromServiceProvider: bool = ..., iterationsChange: bool = ...) -> None: ... class ChangeMasterPasswordResponse(_message.Message): __slots__ = ("encryptedSessionToken",) @@ -2296,7 +2297,7 @@ class PasskeyValidationResponse(_message.Message): ENCRYPTEDLOGINTOKEN_FIELD_NUMBER: _ClassVar[int] isValid: bool encryptedLoginToken: bytes - def __init__(self, isValid: _Optional[bool] = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... + def __init__(self, isValid: bool = ..., encryptedLoginToken: _Optional[bytes] = ...) -> None: ... class UpdatePasskeyRequest(_message.Message): __slots__ = ("userId", "credentialId", "friendlyName") @@ -2312,7 +2313,7 @@ class PasskeyListRequest(_message.Message): __slots__ = ("includeDisabled",) INCLUDEDISABLED_FIELD_NUMBER: _ClassVar[int] includeDisabled: bool - def __init__(self, includeDisabled: _Optional[bool] = ...) -> None: ... + def __init__(self, includeDisabled: bool = ...) -> None: ... class PasskeyInfo(_message.Message): __slots__ = ("userId", "credentialId", "friendlyName", "AAGUID", "createdAtMillis", "lastUsedMillis", "disabledAtMillis") diff --git a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py index aa9d944b..5897e523 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: AccountSummary.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'AccountSummary.proto' ) @@ -25,7 +25,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\"\xb0\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\x12\x16\n\x0e\x66orbidKeyType1\x18\x13 \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\"\xf4\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\x12\x15\n\rmaxIpDistance\x18( \x01(\x05\x12\x1e\n\x16maxIpDistanceEffective\x18) \x01(\x05\"&\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\"\xcc\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\x12\x16\n\x0e\x66orbidKeyType1\x18\x13 \x01(\x08\x12\x1a\n\x12\x64isallowedFeatures\x18\x14 \x03(\t\"\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\"\xf4\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\x12\x15\n\rmaxIpDistance\x18( \x01(\x05\x12\x1e\n\x16maxIpDistanceEffective\x18) \x01(\x05\"&\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) @@ -36,39 +36,39 @@ _globals['_ACCOUNTSUMMARYREQUEST']._serialized_start=58 _globals['_ACCOUNTSUMMARYREQUEST']._serialized_end=136 _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_start=139 - _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_end=827 - _globals['_DEVICEINFO']._serialized_start=830 - _globals['_DEVICEINFO']._serialized_end=1225 - _globals['_KEYSINFO']._serialized_start=1228 - _globals['_KEYSINFO']._serialized_end=1421 - _globals['_SYNCLOG']._serialized_start=1424 - _globals['_SYNCLOG']._serialized_end=1553 - _globals['_LICENSE']._serialized_start=1556 - _globals['_LICENSE']._serialized_end=2449 - _globals['_ADDON']._serialized_start=2452 - _globals['_ADDON']._serialized_end=2615 - _globals['_SETTINGS']._serialized_start=2618 - _globals['_SETTINGS']._serialized_end=3886 - _globals['_KEYVALUE']._serialized_start=3888 - _globals['_KEYVALUE']._serialized_end=3926 - _globals['_KEYVALUEBOOLEAN']._serialized_start=3928 - _globals['_KEYVALUEBOOLEAN']._serialized_end=3973 - _globals['_KEYVALUELONG']._serialized_start=3975 - _globals['_KEYVALUELONG']._serialized_end=4017 - _globals['_RESULT']._serialized_start=4019 - _globals['_RESULT']._serialized_end=4080 - _globals['_ENFORCEMENTS']._serialized_start=4083 - _globals['_ENFORCEMENTS']._serialized_end=4277 - _globals['_MISSINGACCOUNTSHAREKEY']._serialized_start=4279 - _globals['_MISSINGACCOUNTSHAREKEY']._serialized_end=4339 - _globals['_PASSWORDRULE']._serialized_start=4341 - _globals['_PASSWORDRULE']._serialized_end=4458 - _globals['_SECURITYKEY']._serialized_start=4461 - _globals['_SECURITYKEY']._serialized_end=4612 - _globals['_DEVICEREGISTRATION']._serialized_start=4614 - _globals['_DEVICEREGISTRATION']._serialized_end=4735 - _globals['_GROUP']._serialized_start=4737 - _globals['_GROUP']._serialized_end=4844 - _globals['_ADMINISTRATOR']._serialized_start=4847 - _globals['_ADMINISTRATOR']._serialized_end=5039 + _globals['_ACCOUNTSUMMARYELEMENTS']._serialized_end=855 + _globals['_DEVICEINFO']._serialized_start=858 + _globals['_DEVICEINFO']._serialized_end=1253 + _globals['_KEYSINFO']._serialized_start=1256 + _globals['_KEYSINFO']._serialized_end=1449 + _globals['_SYNCLOG']._serialized_start=1452 + _globals['_SYNCLOG']._serialized_end=1581 + _globals['_LICENSE']._serialized_start=1584 + _globals['_LICENSE']._serialized_end=2477 + _globals['_ADDON']._serialized_start=2480 + _globals['_ADDON']._serialized_end=2643 + _globals['_SETTINGS']._serialized_start=2646 + _globals['_SETTINGS']._serialized_end=3914 + _globals['_KEYVALUE']._serialized_start=3916 + _globals['_KEYVALUE']._serialized_end=3954 + _globals['_KEYVALUEBOOLEAN']._serialized_start=3956 + _globals['_KEYVALUEBOOLEAN']._serialized_end=4001 + _globals['_KEYVALUELONG']._serialized_start=4003 + _globals['_KEYVALUELONG']._serialized_end=4045 + _globals['_RESULT']._serialized_start=4047 + _globals['_RESULT']._serialized_end=4108 + _globals['_ENFORCEMENTS']._serialized_start=4111 + _globals['_ENFORCEMENTS']._serialized_end=4305 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_start=4307 + _globals['_MISSINGACCOUNTSHAREKEY']._serialized_end=4367 + _globals['_PASSWORDRULE']._serialized_start=4369 + _globals['_PASSWORDRULE']._serialized_end=4486 + _globals['_SECURITYKEY']._serialized_start=4489 + _globals['_SECURITYKEY']._serialized_end=4640 + _globals['_DEVICEREGISTRATION']._serialized_start=4642 + _globals['_DEVICEREGISTRATION']._serialized_end=4763 + _globals['_GROUP']._serialized_start=4765 + _globals['_GROUP']._serialized_end=4872 + _globals['_ADMINISTRATOR']._serialized_start=4875 + _globals['_ADMINISTRATOR']._serialized_end=5067 # @@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 1e1b67d4..38059775 100644 --- a/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/AccountSummary_pb2.pyi @@ -2,8 +2,7 @@ import APIRequest_pb2 as _APIRequest_pb2 from google.protobuf.internal import containers as _containers from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -13,10 +12,10 @@ class AccountSummaryRequest(_message.Message): INCLUDERECENTACTIVITY_FIELD_NUMBER: _ClassVar[int] summaryVersion: int includeRecentActivity: bool - def __init__(self, summaryVersion: _Optional[int] = ..., includeRecentActivity: _Optional[bool] = ...) -> None: ... + def __init__(self, summaryVersion: _Optional[int] = ..., includeRecentActivity: bool = ...) -> None: ... class AccountSummaryElements(_message.Message): - __slots__ = ("clientKey", "settings", "keysInfo", "syncLogs", "isEnterpriseAdmin", "license", "group", "Enforcements", "Images", "personalLicense", "fixSharedFolderRecords", "usernames", "devices", "isShareAdmin", "accountRecovery", "accountRecoveryPrompt", "minMasterPasswordLengthNoPrompt", "forbidKeyType2", "forbidKeyType1") + __slots__ = ("clientKey", "settings", "keysInfo", "syncLogs", "isEnterpriseAdmin", "license", "group", "Enforcements", "Images", "personalLicense", "fixSharedFolderRecords", "usernames", "devices", "isShareAdmin", "accountRecovery", "accountRecoveryPrompt", "minMasterPasswordLengthNoPrompt", "forbidKeyType2", "forbidKeyType1", "disallowedFeatures") CLIENTKEY_FIELD_NUMBER: _ClassVar[int] SETTINGS_FIELD_NUMBER: _ClassVar[int] KEYSINFO_FIELD_NUMBER: _ClassVar[int] @@ -36,6 +35,7 @@ class AccountSummaryElements(_message.Message): MINMASTERPASSWORDLENGTHNOPROMPT_FIELD_NUMBER: _ClassVar[int] FORBIDKEYTYPE2_FIELD_NUMBER: _ClassVar[int] FORBIDKEYTYPE1_FIELD_NUMBER: _ClassVar[int] + DISALLOWEDFEATURES_FIELD_NUMBER: _ClassVar[int] clientKey: bytes settings: Settings keysInfo: KeysInfo @@ -55,7 +55,8 @@ class AccountSummaryElements(_message.Message): minMasterPasswordLengthNoPrompt: int forbidKeyType2: bool forbidKeyType1: bool - def __init__(self, clientKey: _Optional[bytes] = ..., settings: _Optional[_Union[Settings, _Mapping]] = ..., keysInfo: _Optional[_Union[KeysInfo, _Mapping]] = ..., syncLogs: _Optional[_Iterable[_Union[SyncLog, _Mapping]]] = ..., isEnterpriseAdmin: _Optional[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: _Optional[bool] = ..., usernames: _Optional[_Iterable[str]] = ..., devices: _Optional[_Iterable[_Union[DeviceInfo, _Mapping]]] = ..., isShareAdmin: _Optional[bool] = ..., accountRecovery: _Optional[bool] = ..., accountRecoveryPrompt: _Optional[bool] = ..., minMasterPasswordLengthNoPrompt: _Optional[int] = ..., forbidKeyType2: _Optional[bool] = ..., forbidKeyType1: _Optional[bool] = ...) -> None: ... + disallowedFeatures: _containers.RepeatedScalarFieldContainer[str] + 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 = ..., forbidKeyType1: bool = ..., disallowedFeatures: _Optional[_Iterable[str]] = ...) -> None: ... class DeviceInfo(_message.Message): __slots__ = ("encryptedDeviceToken", "deviceName", "deviceStatus", "devicePublicKey", "encryptedDataKeyDoNotUse", "clientVersion", "username", "ipAddress", "approveRequestTime", "encryptedDataKeyPresent", "groupId", "devicePlatform", "clientFormFactor") @@ -85,7 +86,7 @@ class DeviceInfo(_message.Message): groupId: int 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: _Optional[bool] = ..., groupId: _Optional[int] = ..., devicePlatform: _Optional[str] = ..., clientFormFactor: _Optional[_Union[_APIRequest_pb2.ClientFormFactor, str]] = ...) -> None: ... + 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") @@ -191,7 +192,7 @@ class License(_message.Message): pendingEnterprise: bool 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: _Optional[bool] = ..., accountType: _Optional[int] = ..., uploadsRemaining: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., chatEnabled: _Optional[bool] = ..., auditAndReportingEnabled: _Optional[bool] = ..., breachWatchFeatureDisable: _Optional[bool] = ..., accountUid: _Optional[bytes] = ..., allowPersonalLicense: _Optional[bool] = ..., licensedBy: _Optional[str] = ..., email: _Optional[str] = ..., breachWatchEnabled: _Optional[bool] = ..., breachWatchScanned: _Optional[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: _Optional[bool] = ..., isPamEnabled: _Optional[bool] = ..., isKsmEnabled: _Optional[bool] = ...) -> None: ... + 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") @@ -211,7 +212,7 @@ class AddOn(_message.Message): enabled: bool scanned: bool featureDisable: bool - def __init__(self, licenseKeyId: _Optional[int] = ..., name: _Optional[str] = ..., expirationDate: _Optional[int] = ..., createdDate: _Optional[int] = ..., isTrial: _Optional[bool] = ..., enabled: _Optional[bool] = ..., scanned: _Optional[bool] = ..., featureDisable: _Optional[bool] = ...) -> None: ... + 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", "personalUsernames", "maxIpDistance", "maxIpDistanceEffective") @@ -297,7 +298,7 @@ class Settings(_message.Message): personalUsernames: _containers.RepeatedScalarFieldContainer[str] maxIpDistance: int maxIpDistanceEffective: int - def __init__(self, audit: _Optional[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: _Optional[bool] = ..., emailVerified: _Optional[bool] = ..., masterPasswordLastModified: _Optional[float] = ..., accountFolderKey: _Optional[bytes] = ..., securityKeys: _Optional[_Iterable[_Union[SecurityKey, _Mapping]]] = ..., keyValues: _Optional[_Iterable[_Union[KeyValue, _Mapping]]] = ..., ssoUser: _Optional[bool] = ..., onlineAccessOnly: _Optional[bool] = ..., masterPasswordExpiry: _Optional[int] = ..., twoFactorRequired: _Optional[bool] = ..., disallowExport: _Optional[bool] = ..., restrictFiles: _Optional[bool] = ..., restrictAllSharing: _Optional[bool] = ..., restrictSharing: _Optional[bool] = ..., restrictSharingIncomingAll: _Optional[bool] = ..., restrictSharingIncomingEnterprise: _Optional[bool] = ..., logoutTimer: _Optional[int] = ..., persistentLogin: _Optional[bool] = ..., ipDisableAutoApprove: _Optional[bool] = ..., shareDataKeyWithEccPublicKey: _Optional[bool] = ..., shareDataKeyWithDevicePublicKey: _Optional[bool] = ..., RecordTypesCounter: _Optional[int] = ..., RecordTypesEnterpriseCounter: _Optional[int] = ..., recordTypesEnabled: _Optional[bool] = ..., canManageRecordTypes: _Optional[bool] = ..., recordTypesPAMCounter: _Optional[int] = ..., logoutTimerMinutes: _Optional[int] = ..., securityKeysNoUserVerify: _Optional[bool] = ..., channels: _Optional[_Iterable[_Union[_APIRequest_pb2.TwoFactorChannelType, str]]] = ..., personalUsernames: _Optional[_Iterable[str]] = ..., maxIpDistance: _Optional[int] = ..., maxIpDistanceEffective: _Optional[int] = ...) -> None: ... + 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]] = ..., maxIpDistance: _Optional[int] = ..., maxIpDistanceEffective: _Optional[int] = ...) -> None: ... class KeyValue(_message.Message): __slots__ = ("key", "value") @@ -313,7 +314,7 @@ class KeyValueBoolean(_message.Message): VALUE_FIELD_NUMBER: _ClassVar[int] key: str value: bool - def __init__(self, key: _Optional[str] = ..., value: _Optional[bool] = ...) -> None: ... + def __init__(self, key: _Optional[str] = ..., value: bool = ...) -> None: ... class KeyValueLong(_message.Message): __slots__ = ("key", "value") @@ -367,7 +368,7 @@ class PasswordRule(_message.Message): minimum: int description: str value: str - def __init__(self, ruleType: _Optional[str] = ..., pattern: _Optional[str] = ..., match: _Optional[bool] = ..., minimum: _Optional[int] = ..., description: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... + def __init__(self, ruleType: _Optional[str] = ..., pattern: _Optional[str] = ..., match: bool = ..., minimum: _Optional[int] = ..., description: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ... class SecurityKey(_message.Message): __slots__ = ("deviceId", "deviceName", "dateAdded", "isValid", "deviceRegistration") @@ -381,7 +382,7 @@ class SecurityKey(_message.Message): dateAdded: int isValid: bool deviceRegistration: DeviceRegistration - def __init__(self, deviceId: _Optional[int] = ..., deviceName: _Optional[str] = ..., dateAdded: _Optional[int] = ..., isValid: _Optional[bool] = ..., deviceRegistration: _Optional[_Union[DeviceRegistration, _Mapping]] = ...) -> None: ... + def __init__(self, deviceId: _Optional[int] = ..., deviceName: _Optional[str] = ..., dateAdded: _Optional[int] = ..., isValid: bool = ..., deviceRegistration: _Optional[_Union[DeviceRegistration, _Mapping]] = ...) -> None: ... class DeviceRegistration(_message.Message): __slots__ = ("keyHandle", "publicKey", "attestationCert", "counter", "compromised") @@ -395,7 +396,7 @@ class DeviceRegistration(_message.Message): attestationCert: str counter: int compromised: bool - def __init__(self, keyHandle: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., attestationCert: _Optional[str] = ..., counter: _Optional[int] = ..., compromised: _Optional[bool] = ...) -> None: ... + def __init__(self, keyHandle: _Optional[str] = ..., publicKey: _Optional[bytes] = ..., attestationCert: _Optional[str] = ..., counter: _Optional[int] = ..., compromised: bool = ...) -> None: ... class Group(_message.Message): __slots__ = ("admin", "groupVerificationCode", "administrator") @@ -405,7 +406,7 @@ class Group(_message.Message): admin: bool groupVerificationCode: str administrator: Administrator - def __init__(self, admin: _Optional[bool] = ..., groupVerificationCode: _Optional[str] = ..., administrator: _Optional[_Union[Administrator, _Mapping]] = ...) -> None: ... + def __init__(self, admin: bool = ..., groupVerificationCode: _Optional[str] = ..., administrator: _Optional[_Union[Administrator, _Mapping]] = ...) -> None: ... class Administrator(_message.Message): __slots__ = ("firstName", "lastName", "email", "currentNumberOfUsers", "numberOfUsers", "subscriptionCode", "expirationDate", "purchaseDate") diff --git a/keepersdk-package/src/keepersdk/proto/BI_pb2.py b/keepersdk-package/src/keepersdk/proto/BI_pb2.py index c1960027..8f2304c7 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: BI.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'BI.proto' ) @@ -25,7 +25,7 @@ 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\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\".\n\rEventResponse\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x0e\n\x06status\x18\x02 \x01(\x08\"5\n\x0e\x45ventsResponse\x12#\n\x08response\x18\x01 \x03(\x0b\x32\x11.BI.EventResponse\"\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\"\x96\x01\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\x12\x1a\n\x12purchaseIdentifier\x18\x06 \x01(\t\"k\n\x0fPurchaseOptions\x12\x16\n\tinConsole\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10\x65xternalCheckout\x18\x02 \x01(\x08H\x01\x88\x01\x01\x42\x0c\n\n_inConsoleB\x13\n\x11_externalCheckout\"\xae\x06\n\x14\x41\x64\x64onPurchaseOptions\x12)\n\x07storage\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x00\x88\x01\x01\x12\'\n\x05\x61udit\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x01\x88\x01\x01\x12-\n\x0b\x62reachwatch\x18\x03 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x02\x88\x01\x01\x12&\n\x04\x63hat\x18\x04 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x03\x88\x01\x01\x12,\n\ncompliance\x18\x05 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x04\x88\x01\x01\x12<\n\x1aprofessionalServicesSilver\x18\x06 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x05\x88\x01\x01\x12>\n\x1cprofessionalServicesPlatinum\x18\x07 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x06\x88\x01\x01\x12%\n\x03pam\x18\x08 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x07\x88\x01\x01\x12%\n\x03\x65pm\x18\t \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x08\x88\x01\x01\x12\x30\n\x0esecretsManager\x18\n \x01(\x0b\x32\x13.BI.PurchaseOptionsH\t\x88\x01\x01\x12\x33\n\x11\x63onnectionManager\x18\x0b \x01(\x0b\x32\x13.BI.PurchaseOptionsH\n\x88\x01\x01\x12\x38\n\x16remoteBrowserIsolation\x18\x0c \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x0b\x88\x01\x01\x42\n\n\x08_storageB\x08\n\x06_auditB\x0e\n\x0c_breachwatchB\x07\n\x05_chatB\r\n\x0b_complianceB\x1d\n\x1b_professionalServicesSilverB\x1f\n\x1d_professionalServicesPlatinumB\x06\n\x04_pamB\x06\n\x04_epmB\x11\n\x0f_secretsManagerB\x14\n\x12_connectionManagerB\x19\n\x17_remoteBrowserIsolation\"\x8f\x01\n\x18\x41vailablePurchaseOptions\x12%\n\x08\x62\x61sePlan\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12\"\n\x05users\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12(\n\x06\x61\x64\x64ons\x18\x03 \x01(\x0b\x32\x18.BI.AddonPurchaseOptions\"\x1d\n\x1bUpgradeLicenseStatusRequest\"\x91\x01\n\x1cUpgradeLicenseStatusResponse\x12 \n\x18\x61llowPurchaseFromConsole\x18\x01 \x01(\x08\x12\x35\n\x0fpurchaseOptions\x18\x02 \x01(\x0b\x32\x1c.BI.AvailablePurchaseOptions\x12\x18\n\x05\x65rror\x18\x03 \x01(\x0b\x32\t.BI.Error\"r\n\"UpgradeLicenseQuotePurchaseRequest\x12,\n\x0bproductType\x18\x01 \x01(\x0e\x32\x17.BI.PurchaseProductType\x12\x10\n\x08quantity\x18\x02 \x01(\x05\x12\x0c\n\x04tier\x18\x03 \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\"\x9f\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\x12\x0c\n\x04tier\x18\x04 \x01(\x05\"\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\"J\n\x18SingularDeviceIdentifier\x12\n\n\x02id\x18\x01 \x01(\t\x12\"\n\x06idType\x18\x02 \x01(\x0e\x32\x12.BI.IdentifierType\"\xac\x01\n\x12SingularSharedData\x12\x10\n\x08platform\x18\x01 \x01(\t\x12\x11\n\tosVersion\x18\x02 \x01(\t\x12\x0c\n\x04make\x18\x03 \x01(\t\x12\r\n\x05model\x18\x04 \x01(\t\x12\x0e\n\x06locale\x18\x05 \x01(\t\x12\r\n\x05\x62uild\x18\x06 \x01(\t\x12\x15\n\rappIdentifier\x18\x07 \x01(\t\x12\x1e\n\x16\x61ttAuthorizationStatus\x18\x08 \x01(\x05\"\x8b\x03\n\x16SingularSessionRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x1a\n\x12\x61pplicationVersion\x18\x03 \x01(\t\x12\x0f\n\x07install\x18\x04 \x01(\x08\x12\x13\n\x0binstallTime\x18\x05 \x01(\x03\x12\x12\n\nupdateTime\x18\x06 \x01(\x03\x12\x15\n\rinstallSource\x18\x07 \x01(\t\x12\x16\n\x0einstallReceipt\x18\x08 \x01(\t\x12\x0f\n\x07openuri\x18\t \x01(\t\x12\x12\n\nddlEnabled\x18\n \x01(\x08\x12#\n\x1bsingularLinkResolveRequired\x18\x0b \x01(\x08\x12\x12\n\ninstallRef\x18\x0c \x01(\t\x12\x0f\n\x07metaRef\x18\r \x01(\t\x12\x18\n\x10\x61ttributionToken\x18\x0e \x01(\t\"\x8e\x01\n\x14SingularEventRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x11\n\teventName\x18\x03 \x01(\t\"-\n\x15\x41\x63tivePamCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\"*\n\x16\x41\x63tivePamCountResponse\x12\x10\n\x08pamCount\x18\x01 \x01(\x05*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*\xd5\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\t\x12\x0b\n\x07\x61\x64\x64KEPM\x10\n*\xe0\x01\n\x0eIdentifierType\x12\x1b\n\x17UNKNOWN_IDENTIFIER_TYPE\x10\x00\x12\n\n\x06IOS_ID\x10\x01\x12\x1a\n\x16\x41NDROID_GOOGLE_PLAY_ID\x10\x02\x12\x16\n\x12\x41NDROID_APP_SET_ID\x10\x03\x12\x0e\n\nANDROID_ID\x10\x04\x12\x19\n\x15\x41MAZON_ADVERTISING_ID\x10\x05\x12\x17\n\x13OPEN_ADVERTISING_ID\x10\x06\x12\x16\n\x12SINGULAR_DEVICE_ID\x10\x07\x12\x15\n\x11\x43LIENT_DEFINED_ID\x10\x08\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\"\xe1\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\x12\x1b\n\x03nhi\x18\x0e \x01(\x0b\x32\x0e.BI.NhiBilling\x12\x1c\n\x14\x66reeKsmApiCallsCount\x18\x0f \x01(\x05\"\x95\x01\n\nNhiBilling\x12\x1d\n\x15\x62illingStartTimestamp\x18\x01 \x01(\x03\x12\x1b\n\x13\x62illingEndTimestamp\x18\x02 \x01(\x03\x12\x15\n\rcurrentTierId\x18\x03 \x01(\x05\x12\x18\n\x10\x65nterpriseBlocks\x18\x04 \x01(\x05\x12\x1a\n\x12\x63urrentTierCeiling\x18\x05 \x01(\x05\"\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\".\n\rEventResponse\x12\r\n\x05index\x18\x01 \x01(\x05\x12\x0e\n\x06status\x18\x02 \x01(\x08\"5\n\x0e\x45ventsResponse\x12#\n\x08response\x18\x01 \x03(\x0b\x32\x11.BI.EventResponse\"\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\"\x96\x01\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\x12\x1a\n\x12purchaseIdentifier\x18\x06 \x01(\t\"k\n\x0fPurchaseOptions\x12\x16\n\tinConsole\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10\x65xternalCheckout\x18\x02 \x01(\x08H\x01\x88\x01\x01\x42\x0c\n\n_inConsoleB\x13\n\x11_externalCheckout\"\xae\x06\n\x14\x41\x64\x64onPurchaseOptions\x12)\n\x07storage\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x00\x88\x01\x01\x12\'\n\x05\x61udit\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x01\x88\x01\x01\x12-\n\x0b\x62reachwatch\x18\x03 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x02\x88\x01\x01\x12&\n\x04\x63hat\x18\x04 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x03\x88\x01\x01\x12,\n\ncompliance\x18\x05 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x04\x88\x01\x01\x12<\n\x1aprofessionalServicesSilver\x18\x06 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x05\x88\x01\x01\x12>\n\x1cprofessionalServicesPlatinum\x18\x07 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x06\x88\x01\x01\x12%\n\x03pam\x18\x08 \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x07\x88\x01\x01\x12%\n\x03\x65pm\x18\t \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x08\x88\x01\x01\x12\x30\n\x0esecretsManager\x18\n \x01(\x0b\x32\x13.BI.PurchaseOptionsH\t\x88\x01\x01\x12\x33\n\x11\x63onnectionManager\x18\x0b \x01(\x0b\x32\x13.BI.PurchaseOptionsH\n\x88\x01\x01\x12\x38\n\x16remoteBrowserIsolation\x18\x0c \x01(\x0b\x32\x13.BI.PurchaseOptionsH\x0b\x88\x01\x01\x42\n\n\x08_storageB\x08\n\x06_auditB\x0e\n\x0c_breachwatchB\x07\n\x05_chatB\r\n\x0b_complianceB\x1d\n\x1b_professionalServicesSilverB\x1f\n\x1d_professionalServicesPlatinumB\x06\n\x04_pamB\x06\n\x04_epmB\x11\n\x0f_secretsManagerB\x14\n\x12_connectionManagerB\x19\n\x17_remoteBrowserIsolation\"\x8f\x01\n\x18\x41vailablePurchaseOptions\x12%\n\x08\x62\x61sePlan\x18\x01 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12\"\n\x05users\x18\x02 \x01(\x0b\x32\x13.BI.PurchaseOptions\x12(\n\x06\x61\x64\x64ons\x18\x03 \x01(\x0b\x32\x18.BI.AddonPurchaseOptions\"\x1d\n\x1bUpgradeLicenseStatusRequest\"\x91\x01\n\x1cUpgradeLicenseStatusResponse\x12 \n\x18\x61llowPurchaseFromConsole\x18\x01 \x01(\x08\x12\x35\n\x0fpurchaseOptions\x18\x02 \x01(\x0b\x32\x1c.BI.AvailablePurchaseOptions\x12\x18\n\x05\x65rror\x18\x03 \x01(\x0b\x32\t.BI.Error\"r\n\"UpgradeLicenseQuotePurchaseRequest\x12,\n\x0bproductType\x18\x01 \x01(\x0e\x32\x17.BI.PurchaseProductType\x12\x10\n\x08quantity\x18\x02 \x01(\x05\x12\x0c\n\x04tier\x18\x03 \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\"\x9f\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\x12\x0c\n\x04tier\x18\x04 \x01(\x05\"\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\"J\n\x18SingularDeviceIdentifier\x12\n\n\x02id\x18\x01 \x01(\t\x12\"\n\x06idType\x18\x02 \x01(\x0e\x32\x12.BI.IdentifierType\"\xac\x01\n\x12SingularSharedData\x12\x10\n\x08platform\x18\x01 \x01(\t\x12\x11\n\tosVersion\x18\x02 \x01(\t\x12\x0c\n\x04make\x18\x03 \x01(\t\x12\r\n\x05model\x18\x04 \x01(\t\x12\x0e\n\x06locale\x18\x05 \x01(\t\x12\r\n\x05\x62uild\x18\x06 \x01(\t\x12\x15\n\rappIdentifier\x18\x07 \x01(\t\x12\x1e\n\x16\x61ttAuthorizationStatus\x18\x08 \x01(\x05\"\x8b\x03\n\x16SingularSessionRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x1a\n\x12\x61pplicationVersion\x18\x03 \x01(\t\x12\x0f\n\x07install\x18\x04 \x01(\x08\x12\x13\n\x0binstallTime\x18\x05 \x01(\x03\x12\x12\n\nupdateTime\x18\x06 \x01(\x03\x12\x15\n\rinstallSource\x18\x07 \x01(\t\x12\x16\n\x0einstallReceipt\x18\x08 \x01(\t\x12\x0f\n\x07openuri\x18\t \x01(\t\x12\x12\n\nddlEnabled\x18\n \x01(\x08\x12#\n\x1bsingularLinkResolveRequired\x18\x0b \x01(\x08\x12\x12\n\ninstallRef\x18\x0c \x01(\t\x12\x0f\n\x07metaRef\x18\r \x01(\t\x12\x18\n\x10\x61ttributionToken\x18\x0e \x01(\t\"\x8e\x01\n\x14SingularEventRequest\x12\x37\n\x11\x64\x65viceIdentifiers\x18\x01 \x03(\x0b\x32\x1c.BI.SingularDeviceIdentifier\x12*\n\nsharedData\x18\x02 \x01(\x0b\x32\x16.BI.SingularSharedData\x12\x11\n\teventName\x18\x03 \x01(\t\"-\n\x15\x41\x63tivePamCountRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\"*\n\x16\x41\x63tivePamCountResponse\x12\x10\n\x08pamCount\x18\x01 \x01(\x05\"P\n\x14NhiEnterpriseRequest\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x05\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\"\x89\x01\n\x11NhiMetricsRequest\x12\x19\n\renterpriseIds\x18\x01 \x03(\x05\x42\x02\x18\x01\x12\x15\n\tstartTime\x18\x02 \x01(\x03\x42\x02\x18\x01\x12\x13\n\x07\x65ndTime\x18\x03 \x01(\x03\x42\x02\x18\x01\x12-\n\x0b\x65nterprises\x18\x04 \x03(\x0b\x32\x18.BI.NhiEnterpriseRequest*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*\xd5\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\t\x12\x0b\n\x07\x61\x64\x64KEPM\x10\n*\xe0\x01\n\x0eIdentifierType\x12\x1b\n\x17UNKNOWN_IDENTIFIER_TYPE\x10\x00\x12\n\n\x06IOS_ID\x10\x01\x12\x1a\n\x16\x41NDROID_GOOGLE_PLAY_ID\x10\x02\x12\x16\n\x12\x41NDROID_APP_SET_ID\x10\x03\x12\x0e\n\nANDROID_ID\x10\x04\x12\x19\n\x15\x41MAZON_ADVERTISING_ID\x10\x05\x12\x17\n\x13OPEN_ADVERTISING_ID\x10\x06\x12\x16\n\x12SINGULAR_DEVICE_ID\x10\x07\x12\x15\n\x11\x43LIENT_DEFINED_ID\x10\x08\x42\x1e\n\x18\x63om.keepersecurity.protoB\x02\x42Ib\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -35,16 +35,22 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\002BI' _globals['_ERROR_EXTRASENTRY']._loaded_options = None _globals['_ERROR_EXTRASENTRY']._serialized_options = b'8\001' - _globals['_CURRENCY']._serialized_start=8918 - _globals['_CURRENCY']._serialized_end=8995 - _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_start=8997 - _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_end=9080 - _globals['_EVENTTYPE']._serialized_start=9083 - _globals['_EVENTTYPE']._serialized_end=9306 - _globals['_PURCHASEPRODUCTTYPE']._serialized_start=9309 - _globals['_PURCHASEPRODUCTTYPE']._serialized_end=9522 - _globals['_IDENTIFIERTYPE']._serialized_start=9525 - _globals['_IDENTIFIERTYPE']._serialized_end=9749 + _globals['_NHIMETRICSREQUEST'].fields_by_name['enterpriseIds']._loaded_options = None + _globals['_NHIMETRICSREQUEST'].fields_by_name['enterpriseIds']._serialized_options = b'\030\001' + _globals['_NHIMETRICSREQUEST'].fields_by_name['startTime']._loaded_options = None + _globals['_NHIMETRICSREQUEST'].fields_by_name['startTime']._serialized_options = b'\030\001' + _globals['_NHIMETRICSREQUEST'].fields_by_name['endTime']._loaded_options = None + _globals['_NHIMETRICSREQUEST'].fields_by_name['endTime']._serialized_options = b'\030\001' + _globals['_CURRENCY']._serialized_start=9351 + _globals['_CURRENCY']._serialized_end=9428 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_start=9430 + _globals['_GRADIENTINTEGRATIONSTATUS']._serialized_end=9513 + _globals['_EVENTTYPE']._serialized_start=9516 + _globals['_EVENTTYPE']._serialized_end=9739 + _globals['_PURCHASEPRODUCTTYPE']._serialized_start=9742 + _globals['_PURCHASEPRODUCTTYPE']._serialized_end=9955 + _globals['_IDENTIFIERTYPE']._serialized_start=9958 + _globals['_IDENTIFIERTYPE']._serialized_end=10182 _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_start=46 _globals['_VALIDATESESSIONTOKENREQUEST']._serialized_end=148 _globals['_VALIDATESESSIONTOKENRESPONSE']._serialized_start=151 @@ -54,171 +60,177 @@ _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['_EVENTRESPONSE']._serialized_start=5221 - _globals['_EVENTRESPONSE']._serialized_end=5267 - _globals['_EVENTSRESPONSE']._serialized_start=5269 - _globals['_EVENTSRESPONSE']._serialized_end=5322 - _globals['_CUSTOMERCAPTUREREQUEST']._serialized_start=5325 - _globals['_CUSTOMERCAPTUREREQUEST']._serialized_end=5494 - _globals['_CUSTOMERCAPTURERESPONSE']._serialized_start=5496 - _globals['_CUSTOMERCAPTURERESPONSE']._serialized_end=5521 - _globals['_ERROR']._serialized_start=5523 - _globals['_ERROR']._serialized_end=5647 - _globals['_ERROR_EXTRASENTRY']._serialized_start=5602 - _globals['_ERROR_EXTRASENTRY']._serialized_end=5647 - _globals['_QUOTEPURCHASE']._serialized_start=5650 - _globals['_QUOTEPURCHASE']._serialized_end=5800 - _globals['_PURCHASEOPTIONS']._serialized_start=5802 - _globals['_PURCHASEOPTIONS']._serialized_end=5909 - _globals['_ADDONPURCHASEOPTIONS']._serialized_start=5912 - _globals['_ADDONPURCHASEOPTIONS']._serialized_end=6726 - _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_start=6729 - _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_end=6872 - _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_start=6874 - _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_end=6903 - _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_start=6906 - _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_end=7051 - _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_start=7053 - _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_end=7167 - _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_start=7170 - _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_end=7317 - _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_start=7320 - _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_end=7479 - _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_start=7482 - _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_end=7630 - _globals['_ENTERPRISEBASEPLAN']._serialized_start=7633 - _globals['_ENTERPRISEBASEPLAN']._serialized_end=7846 - _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_start=7754 - _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_end=7846 - _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_start=7848 - _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_end=7886 - _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_start=7889 - _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_end=8031 - _globals['_SINGULARDEVICEIDENTIFIER']._serialized_start=8033 - _globals['_SINGULARDEVICEIDENTIFIER']._serialized_end=8107 - _globals['_SINGULARSHAREDDATA']._serialized_start=8110 - _globals['_SINGULARSHAREDDATA']._serialized_end=8282 - _globals['_SINGULARSESSIONREQUEST']._serialized_start=8285 - _globals['_SINGULARSESSIONREQUEST']._serialized_end=8680 - _globals['_SINGULAREVENTREQUEST']._serialized_start=8683 - _globals['_SINGULAREVENTREQUEST']._serialized_end=8825 - _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_start=8827 - _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_end=8872 - _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_start=8874 - _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_end=8916 + _globals['_SUBSCRIPTIONSTATUSRESPONSE']._serialized_end=1010 + _globals['_NHIBILLING']._serialized_start=1013 + _globals['_NHIBILLING']._serialized_end=1162 + _globals['_LICENSESTATS']._serialized_start=1165 + _globals['_LICENSESTATS']._serialized_end=1444 + _globals['_LICENSESTATS_TYPE']._serialized_start=1252 + _globals['_LICENSESTATS_TYPE']._serialized_end=1444 + _globals['_AUTORENEWAL']._serialized_start=1446 + _globals['_AUTORENEWAL']._serialized_end=1510 + _globals['_PAYMENTMETHOD']._serialized_start=1513 + _globals['_PAYMENTMETHOD']._serialized_end=2029 + _globals['_PAYMENTMETHOD_CARD']._serialized_start=1807 + _globals['_PAYMENTMETHOD_CARD']._serialized_end=1843 + _globals['_PAYMENTMETHOD_SEPA']._serialized_start=1845 + _globals['_PAYMENTMETHOD_SEPA']._serialized_end=1883 + _globals['_PAYMENTMETHOD_PAYPAL']._serialized_start=1885 + _globals['_PAYMENTMETHOD_PAYPAL']._serialized_end=1893 + _globals['_PAYMENTMETHOD_VENDOR']._serialized_start=1895 + _globals['_PAYMENTMETHOD_VENDOR']._serialized_end=1917 + _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_start=1919 + _globals['_PAYMENTMETHOD_PURCHASEORDER']._serialized_end=1948 + _globals['_PAYMENTMETHOD_TYPE']._serialized_start=1950 + _globals['_PAYMENTMETHOD_TYPE']._serialized_end=2029 + _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_start=2031 + _globals['_SUBSCRIPTIONMSPPRICINGREQUEST']._serialized_end=2062 + _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_start=2064 + _globals['_SUBSCRIPTIONMSPPRICINGRESPONSE']._serialized_end=2156 + _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_start=2158 + _globals['_SUBSCRIPTIONMCPRICINGREQUEST']._serialized_end=2188 + _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_start=2190 + _globals['_SUBSCRIPTIONMCPRICINGRESPONSE']._serialized_end=2314 + _globals['_BASEPLAN']._serialized_start=2316 + _globals['_BASEPLAN']._serialized_end=2362 + _globals['_ADDON']._serialized_start=2364 + _globals['_ADDON']._serialized_end=2431 + _globals['_FILEPLAN']._serialized_start=2433 + _globals['_FILEPLAN']._serialized_end=2479 + _globals['_COST']._serialized_start=2482 + _globals['_COST']._serialized_end=2742 + _globals['_COST_AMOUNTPER']._serialized_start=2578 + _globals['_COST_AMOUNTPER']._serialized_end=2742 + _globals['_INVOICESEARCHREQUEST']._serialized_start=2744 + _globals['_INVOICESEARCHREQUEST']._serialized_end=2836 + _globals['_INVOICESEARCHRESPONSE']._serialized_start=2838 + _globals['_INVOICESEARCHRESPONSE']._serialized_end=2892 + _globals['_INVOICE']._serialized_start=2895 + _globals['_INVOICE']._serialized_end=3213 + _globals['_INVOICE_COST']._serialized_start=3060 + _globals['_INVOICE_COST']._serialized_end=3114 + _globals['_INVOICE_TYPE']._serialized_start=3116 + _globals['_INVOICE_TYPE']._serialized_end=3213 + _globals['_VAULTINVOICESLISTREQUEST']._serialized_start=3215 + _globals['_VAULTINVOICESLISTREQUEST']._serialized_end=3241 + _globals['_VAULTINVOICESLISTRESPONSE']._serialized_start=3243 + _globals['_VAULTINVOICESLISTRESPONSE']._serialized_end=3306 + _globals['_VAULTINVOICE']._serialized_start=3309 + _globals['_VAULTINVOICE']._serialized_end=3452 + _globals['_INVOICEDOWNLOADREQUEST']._serialized_start=3454 + _globals['_INVOICEDOWNLOADREQUEST']._serialized_end=3501 + _globals['_INVOICEDOWNLOADRESPONSE']._serialized_start=3503 + _globals['_INVOICEDOWNLOADRESPONSE']._serialized_end=3560 + _globals['_VAULTINVOICEDOWNLOADLINKREQUEST']._serialized_start=3562 + _globals['_VAULTINVOICEDOWNLOADLINKREQUEST']._serialized_end=3618 + _globals['_VAULTINVOICEDOWNLOADLINKRESPONSE']._serialized_start=3620 + _globals['_VAULTINVOICEDOWNLOADLINKRESPONSE']._serialized_end=3686 + _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_start=3688 + _globals['_REPORTINGDAILYSNAPSHOTREQUEST']._serialized_end=3748 + _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_start=3750 + _globals['_REPORTINGDAILYSNAPSHOTRESPONSE']._serialized_end=3868 + _globals['_SNAPSHOTRECORD']._serialized_start=3871 + _globals['_SNAPSHOTRECORD']._serialized_end=4086 + _globals['_SNAPSHOTRECORD_ADDON']._serialized_start=4044 + _globals['_SNAPSHOTRECORD_ADDON']._serialized_end=4086 + _globals['_SNAPSHOTMCENTERPRISE']._serialized_start=4088 + _globals['_SNAPSHOTMCENTERPRISE']._serialized_end=4136 + _globals['_MAPPINGADDONSREQUEST']._serialized_start=4138 + _globals['_MAPPINGADDONSREQUEST']._serialized_end=4160 + _globals['_MAPPINGADDONSRESPONSE']._serialized_start=4162 + _globals['_MAPPINGADDONSRESPONSE']._serialized_end=4254 + _globals['_MAPPINGITEM']._serialized_start=4256 + _globals['_MAPPINGITEM']._serialized_end=4295 + _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_start=4297 + _globals['_GRADIENTVALIDATEKEYREQUEST']._serialized_end=4346 + _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_start=4348 + _globals['_GRADIENTVALIDATEKEYRESPONSE']._serialized_end=4411 + _globals['_GRADIENTSAVEREQUEST']._serialized_start=4413 + _globals['_GRADIENTSAVEREQUEST']._serialized_end=4481 + _globals['_GRADIENTSAVERESPONSE']._serialized_start=4483 + _globals['_GRADIENTSAVERESPONSE']._serialized_end=4586 + _globals['_GRADIENTREMOVEREQUEST']._serialized_start=4588 + _globals['_GRADIENTREMOVEREQUEST']._serialized_end=4637 + _globals['_GRADIENTREMOVERESPONSE']._serialized_start=4639 + _globals['_GRADIENTREMOVERESPONSE']._serialized_end=4697 + _globals['_GRADIENTSYNCREQUEST']._serialized_start=4699 + _globals['_GRADIENTSYNCREQUEST']._serialized_end=4746 + _globals['_GRADIENTSYNCRESPONSE']._serialized_start=4748 + _globals['_GRADIENTSYNCRESPONSE']._serialized_end=4851 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_start=4853 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONREQUEST']._serialized_end=4931 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_start=4933 + _globals['_NETPROMOTERSCORESURVEYSUBMISSIONRESPONSE']._serialized_end=4975 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_start=4977 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULEREQUEST']._serialized_end=5015 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_start=5017 + _globals['_NETPROMOTERSCOREPOPUPSCHEDULERESPONSE']._serialized_end=5076 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_start=5078 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALREQUEST']._serialized_end=5117 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_start=5119 + _globals['_NETPROMOTERSCOREPOPUPDISMISSALRESPONSE']._serialized_end=5159 + _globals['_KCMLICENSEREQUEST']._serialized_start=5161 + _globals['_KCMLICENSEREQUEST']._serialized_end=5206 + _globals['_KCMLICENSERESPONSE']._serialized_start=5208 + _globals['_KCMLICENSERESPONSE']._serialized_end=5245 + _globals['_EVENTREQUEST']._serialized_start=5248 + _globals['_EVENTREQUEST']._serialized_end=5380 + _globals['_EVENTSREQUEST']._serialized_start=5382 + _globals['_EVENTSREQUEST']._serialized_end=5430 + _globals['_EVENTRESPONSE']._serialized_start=5432 + _globals['_EVENTRESPONSE']._serialized_end=5478 + _globals['_EVENTSRESPONSE']._serialized_start=5480 + _globals['_EVENTSRESPONSE']._serialized_end=5533 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_start=5536 + _globals['_CUSTOMERCAPTUREREQUEST']._serialized_end=5705 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_start=5707 + _globals['_CUSTOMERCAPTURERESPONSE']._serialized_end=5732 + _globals['_ERROR']._serialized_start=5734 + _globals['_ERROR']._serialized_end=5858 + _globals['_ERROR_EXTRASENTRY']._serialized_start=5813 + _globals['_ERROR_EXTRASENTRY']._serialized_end=5858 + _globals['_QUOTEPURCHASE']._serialized_start=5861 + _globals['_QUOTEPURCHASE']._serialized_end=6011 + _globals['_PURCHASEOPTIONS']._serialized_start=6013 + _globals['_PURCHASEOPTIONS']._serialized_end=6120 + _globals['_ADDONPURCHASEOPTIONS']._serialized_start=6123 + _globals['_ADDONPURCHASEOPTIONS']._serialized_end=6937 + _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_start=6940 + _globals['_AVAILABLEPURCHASEOPTIONS']._serialized_end=7083 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_start=7085 + _globals['_UPGRADELICENSESTATUSREQUEST']._serialized_end=7114 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_start=7117 + _globals['_UPGRADELICENSESTATUSRESPONSE']._serialized_end=7262 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_start=7264 + _globals['_UPGRADELICENSEQUOTEPURCHASEREQUEST']._serialized_end=7378 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_start=7381 + _globals['_UPGRADELICENSEQUOTEPURCHASERESPONSE']._serialized_end=7528 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_start=7531 + _globals['_UPGRADELICENSECOMPLETEPURCHASEREQUEST']._serialized_end=7690 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_start=7693 + _globals['_UPGRADELICENSECOMPLETEPURCHASERESPONSE']._serialized_end=7841 + _globals['_ENTERPRISEBASEPLAN']._serialized_start=7844 + _globals['_ENTERPRISEBASEPLAN']._serialized_end=8057 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_start=7965 + _globals['_ENTERPRISEBASEPLAN_ENTERPRISEBASEPLANVERSION']._serialized_end=8057 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_start=8059 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGREQUEST']._serialized_end=8097 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_start=8100 + _globals['_SUBSCRIPTIONENTERPRISEPRICINGRESPONSE']._serialized_end=8242 + _globals['_SINGULARDEVICEIDENTIFIER']._serialized_start=8244 + _globals['_SINGULARDEVICEIDENTIFIER']._serialized_end=8318 + _globals['_SINGULARSHAREDDATA']._serialized_start=8321 + _globals['_SINGULARSHAREDDATA']._serialized_end=8493 + _globals['_SINGULARSESSIONREQUEST']._serialized_start=8496 + _globals['_SINGULARSESSIONREQUEST']._serialized_end=8891 + _globals['_SINGULAREVENTREQUEST']._serialized_start=8894 + _globals['_SINGULAREVENTREQUEST']._serialized_end=9036 + _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_start=9038 + _globals['_ACTIVEPAMCOUNTREQUEST']._serialized_end=9083 + _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_start=9085 + _globals['_ACTIVEPAMCOUNTRESPONSE']._serialized_end=9127 + _globals['_NHIENTERPRISEREQUEST']._serialized_start=9129 + _globals['_NHIENTERPRISEREQUEST']._serialized_end=9209 + _globals['_NHIMETRICSREQUEST']._serialized_start=9212 + _globals['_NHIMETRICSREQUEST']._serialized_end=9349 # @@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 39f992b6..d077ba08 100644 --- a/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/BI_pb2.pyi @@ -3,8 +3,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -109,7 +108,7 @@ class ValidateSessionTokenRequest(_message.Message): encryptedSessionToken: bytes returnMcEnterpiseIds: bool ip: str - def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., returnMcEnterpiseIds: _Optional[bool] = ..., ip: _Optional[str] = ...) -> None: ... + def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., returnMcEnterpiseIds: bool = ..., ip: _Optional[str] = ...) -> None: ... class ValidateSessionTokenResponse(_message.Message): __slots__ = ("username", "userId", "enterpriseUserId", "status", "statusMessage", "mcEnterpriseIds", "hasMSPPermission", "deletedMcEnterpriseIds") @@ -141,14 +140,14 @@ class ValidateSessionTokenResponse(_message.Message): mcEnterpriseIds: _containers.RepeatedScalarFieldContainer[int] hasMSPPermission: bool deletedMcEnterpriseIds: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, username: _Optional[str] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., status: _Optional[_Union[ValidateSessionTokenResponse.Status, str]] = ..., statusMessage: _Optional[str] = ..., mcEnterpriseIds: _Optional[_Iterable[int]] = ..., hasMSPPermission: _Optional[bool] = ..., deletedMcEnterpriseIds: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., userId: _Optional[int] = ..., enterpriseUserId: _Optional[int] = ..., status: _Optional[_Union[ValidateSessionTokenResponse.Status, str]] = ..., statusMessage: _Optional[str] = ..., mcEnterpriseIds: _Optional[_Iterable[int]] = ..., hasMSPPermission: bool = ..., deletedMcEnterpriseIds: _Optional[_Iterable[int]] = ...) -> None: ... class SubscriptionStatusRequest(_message.Message): __slots__ = () def __init__(self) -> None: ... class SubscriptionStatusResponse(_message.Message): - __slots__ = ("autoRenewal", "currentPaymentMethod", "checkoutLink", "licenseCreateDate", "isDistributor", "isLegacyMsp", "licenseStats", "gradientStatus", "hideTrialBanner", "gradientLastSyncDate", "gradientNextSyncDate", "isGradientMappingPending") + __slots__ = ("autoRenewal", "currentPaymentMethod", "checkoutLink", "licenseCreateDate", "isDistributor", "isLegacyMsp", "licenseStats", "gradientStatus", "hideTrialBanner", "gradientLastSyncDate", "gradientNextSyncDate", "isGradientMappingPending", "nhi", "freeKsmApiCallsCount") AUTORENEWAL_FIELD_NUMBER: _ClassVar[int] CURRENTPAYMENTMETHOD_FIELD_NUMBER: _ClassVar[int] CHECKOUTLINK_FIELD_NUMBER: _ClassVar[int] @@ -161,6 +160,8 @@ class SubscriptionStatusResponse(_message.Message): GRADIENTLASTSYNCDATE_FIELD_NUMBER: _ClassVar[int] GRADIENTNEXTSYNCDATE_FIELD_NUMBER: _ClassVar[int] ISGRADIENTMAPPINGPENDING_FIELD_NUMBER: _ClassVar[int] + NHI_FIELD_NUMBER: _ClassVar[int] + FREEKSMAPICALLSCOUNT_FIELD_NUMBER: _ClassVar[int] autoRenewal: AutoRenewal currentPaymentMethod: PaymentMethod checkoutLink: str @@ -173,7 +174,23 @@ class SubscriptionStatusResponse(_message.Message): gradientLastSyncDate: str gradientNextSyncDate: str isGradientMappingPending: bool - def __init__(self, autoRenewal: _Optional[_Union[AutoRenewal, _Mapping]] = ..., currentPaymentMethod: _Optional[_Union[PaymentMethod, _Mapping]] = ..., checkoutLink: _Optional[str] = ..., licenseCreateDate: _Optional[int] = ..., isDistributor: _Optional[bool] = ..., isLegacyMsp: _Optional[bool] = ..., licenseStats: _Optional[_Iterable[_Union[LicenseStats, _Mapping]]] = ..., gradientStatus: _Optional[_Union[GradientIntegrationStatus, str]] = ..., hideTrialBanner: _Optional[bool] = ..., gradientLastSyncDate: _Optional[str] = ..., gradientNextSyncDate: _Optional[str] = ..., isGradientMappingPending: _Optional[bool] = ...) -> None: ... + nhi: NhiBilling + freeKsmApiCallsCount: int + def __init__(self, autoRenewal: _Optional[_Union[AutoRenewal, _Mapping]] = ..., currentPaymentMethod: _Optional[_Union[PaymentMethod, _Mapping]] = ..., checkoutLink: _Optional[str] = ..., licenseCreateDate: _Optional[int] = ..., isDistributor: bool = ..., isLegacyMsp: bool = ..., licenseStats: _Optional[_Iterable[_Union[LicenseStats, _Mapping]]] = ..., gradientStatus: _Optional[_Union[GradientIntegrationStatus, str]] = ..., hideTrialBanner: bool = ..., gradientLastSyncDate: _Optional[str] = ..., gradientNextSyncDate: _Optional[str] = ..., isGradientMappingPending: bool = ..., nhi: _Optional[_Union[NhiBilling, _Mapping]] = ..., freeKsmApiCallsCount: _Optional[int] = ...) -> None: ... + +class NhiBilling(_message.Message): + __slots__ = ("billingStartTimestamp", "billingEndTimestamp", "currentTierId", "enterpriseBlocks", "currentTierCeiling") + BILLINGSTARTTIMESTAMP_FIELD_NUMBER: _ClassVar[int] + BILLINGENDTIMESTAMP_FIELD_NUMBER: _ClassVar[int] + CURRENTTIERID_FIELD_NUMBER: _ClassVar[int] + ENTERPRISEBLOCKS_FIELD_NUMBER: _ClassVar[int] + CURRENTTIERCEILING_FIELD_NUMBER: _ClassVar[int] + billingStartTimestamp: int + billingEndTimestamp: int + currentTierId: int + enterpriseBlocks: int + currentTierCeiling: int + def __init__(self, billingStartTimestamp: _Optional[int] = ..., billingEndTimestamp: _Optional[int] = ..., currentTierId: _Optional[int] = ..., enterpriseBlocks: _Optional[int] = ..., currentTierCeiling: _Optional[int] = ...) -> None: ... class LicenseStats(_message.Message): __slots__ = ("type", "available", "used") @@ -213,7 +230,7 @@ class AutoRenewal(_message.Message): nextOn: int daysLeft: int isTrial: bool - def __init__(self, nextOn: _Optional[int] = ..., daysLeft: _Optional[int] = ..., isTrial: _Optional[bool] = ...) -> None: ... + def __init__(self, nextOn: _Optional[int] = ..., daysLeft: _Optional[int] = ..., isTrial: bool = ...) -> None: ... class PaymentMethod(_message.Message): __slots__ = ("type", "card", "sepa", "paypal", "failedBilling", "vendor", "purchaseOrder") @@ -272,7 +289,7 @@ class PaymentMethod(_message.Message): failedBilling: bool vendor: PaymentMethod.Vendor purchaseOrder: PaymentMethod.PurchaseOrder - def __init__(self, type: _Optional[_Union[PaymentMethod.Type, str]] = ..., card: _Optional[_Union[PaymentMethod.Card, _Mapping]] = ..., sepa: _Optional[_Union[PaymentMethod.Sepa, _Mapping]] = ..., paypal: _Optional[_Union[PaymentMethod.Paypal, _Mapping]] = ..., failedBilling: _Optional[bool] = ..., vendor: _Optional[_Union[PaymentMethod.Vendor, _Mapping]] = ..., purchaseOrder: _Optional[_Union[PaymentMethod.PurchaseOrder, _Mapping]] = ...) -> None: ... + def __init__(self, type: _Optional[_Union[PaymentMethod.Type, str]] = ..., card: _Optional[_Union[PaymentMethod.Card, _Mapping]] = ..., sepa: _Optional[_Union[PaymentMethod.Sepa, _Mapping]] = ..., paypal: _Optional[_Union[PaymentMethod.Paypal, _Mapping]] = ..., failedBilling: bool = ..., vendor: _Optional[_Union[PaymentMethod.Vendor, _Mapping]] = ..., purchaseOrder: _Optional[_Union[PaymentMethod.PurchaseOrder, _Mapping]] = ...) -> None: ... class SubscriptionMspPricingRequest(_message.Message): __slots__ = () @@ -364,7 +381,7 @@ class InvoiceSearchRequest(_message.Message): size: int startingAfterId: int allInvoicesUnfiltered: bool - def __init__(self, size: _Optional[int] = ..., startingAfterId: _Optional[int] = ..., allInvoicesUnfiltered: _Optional[bool] = ...) -> None: ... + def __init__(self, size: _Optional[int] = ..., startingAfterId: _Optional[int] = ..., allInvoicesUnfiltered: bool = ...) -> None: ... class InvoiceSearchResponse(_message.Message): __slots__ = ("invoices",) @@ -542,7 +559,7 @@ class GradientValidateKeyResponse(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] success: bool message: str - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ... class GradientSaveRequest(_message.Message): __slots__ = ("gradientKey", "enterpriseUserId") @@ -560,7 +577,7 @@ class GradientSaveResponse(_message.Message): success: bool status: GradientIntegrationStatus message: str - def __init__(self, success: _Optional[bool] = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: bool = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... class GradientRemoveRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -574,7 +591,7 @@ class GradientRemoveResponse(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] success: bool message: str - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ...) -> None: ... class GradientSyncRequest(_message.Message): __slots__ = ("enterpriseUserId",) @@ -590,7 +607,7 @@ class GradientSyncResponse(_message.Message): success: bool status: GradientIntegrationStatus message: str - def __init__(self, success: _Optional[bool] = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, success: bool = ..., status: _Optional[_Union[GradientIntegrationStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... class NetPromoterScoreSurveySubmissionRequest(_message.Message): __slots__ = ("survey_score", "notes") @@ -612,7 +629,7 @@ class NetPromoterScorePopupScheduleResponse(_message.Message): __slots__ = ("show_popup",) SHOW_POPUP_FIELD_NUMBER: _ClassVar[int] show_popup: bool - def __init__(self, show_popup: _Optional[bool] = ...) -> None: ... + def __init__(self, show_popup: bool = ...) -> None: ... class NetPromoterScorePopupDismissalRequest(_message.Message): __slots__ = () @@ -658,7 +675,7 @@ class EventResponse(_message.Message): STATUS_FIELD_NUMBER: _ClassVar[int] index: int status: bool - def __init__(self, index: _Optional[int] = ..., status: _Optional[bool] = ...) -> None: ... + def __init__(self, index: _Optional[int] = ..., status: bool = ...) -> None: ... class EventsResponse(_message.Message): __slots__ = ("response",) @@ -686,7 +703,7 @@ class CustomerCaptureRequest(_message.Message): 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: _Optional[bool] = ..., issueType: _Optional[str] = ..., notes: _Optional[str] = ...) -> None: ... + 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__ = () @@ -723,7 +740,7 @@ class QuotePurchase(_message.Message): taxAmount: float taxLabel: str purchaseIdentifier: str - def __init__(self, quoteTotal: _Optional[float] = ..., includedTax: _Optional[bool] = ..., includedOtherAddons: _Optional[bool] = ..., taxAmount: _Optional[float] = ..., taxLabel: _Optional[str] = ..., purchaseIdentifier: _Optional[str] = ...) -> None: ... + def __init__(self, quoteTotal: _Optional[float] = ..., includedTax: bool = ..., includedOtherAddons: bool = ..., taxAmount: _Optional[float] = ..., taxLabel: _Optional[str] = ..., purchaseIdentifier: _Optional[str] = ...) -> None: ... class PurchaseOptions(_message.Message): __slots__ = ("inConsole", "externalCheckout") @@ -731,7 +748,7 @@ class PurchaseOptions(_message.Message): EXTERNALCHECKOUT_FIELD_NUMBER: _ClassVar[int] inConsole: bool externalCheckout: bool - def __init__(self, inConsole: _Optional[bool] = ..., externalCheckout: _Optional[bool] = ...) -> None: ... + def __init__(self, inConsole: bool = ..., externalCheckout: bool = ...) -> None: ... class AddonPurchaseOptions(_message.Message): __slots__ = ("storage", "audit", "breachwatch", "chat", "compliance", "professionalServicesSilver", "professionalServicesPlatinum", "pam", "epm", "secretsManager", "connectionManager", "remoteBrowserIsolation") @@ -783,7 +800,7 @@ class UpgradeLicenseStatusResponse(_message.Message): allowPurchaseFromConsole: bool purchaseOptions: AvailablePurchaseOptions error: Error - def __init__(self, allowPurchaseFromConsole: _Optional[bool] = ..., purchaseOptions: _Optional[_Union[AvailablePurchaseOptions, _Mapping]] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... + def __init__(self, allowPurchaseFromConsole: bool = ..., purchaseOptions: _Optional[_Union[AvailablePurchaseOptions, _Mapping]] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... class UpgradeLicenseQuotePurchaseRequest(_message.Message): __slots__ = ("productType", "quantity", "tier") @@ -805,7 +822,7 @@ class UpgradeLicenseQuotePurchaseResponse(_message.Message): quotePurchase: QuotePurchase viewSummaryLink: str error: Error - def __init__(self, success: _Optional[bool] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ..., viewSummaryLink: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ...) -> None: ... + 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", "tier") @@ -829,7 +846,7 @@ class UpgradeLicenseCompletePurchaseResponse(_message.Message): invoiceNumber: str error: Error quotePurchase: QuotePurchase - def __init__(self, success: _Optional[bool] = ..., invoiceNumber: _Optional[str] = ..., error: _Optional[_Union[Error, _Mapping]] = ..., quotePurchase: _Optional[_Union[QuotePurchase, _Mapping]] = ...) -> None: ... + 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") @@ -921,7 +938,7 @@ class SingularSessionRequest(_message.Message): installRef: str metaRef: str attributionToken: str - def __init__(self, deviceIdentifiers: _Optional[_Iterable[_Union[SingularDeviceIdentifier, _Mapping]]] = ..., sharedData: _Optional[_Union[SingularSharedData, _Mapping]] = ..., applicationVersion: _Optional[str] = ..., install: _Optional[bool] = ..., installTime: _Optional[int] = ..., updateTime: _Optional[int] = ..., installSource: _Optional[str] = ..., installReceipt: _Optional[str] = ..., openuri: _Optional[str] = ..., ddlEnabled: _Optional[bool] = ..., singularLinkResolveRequired: _Optional[bool] = ..., installRef: _Optional[str] = ..., metaRef: _Optional[str] = ..., attributionToken: _Optional[str] = ...) -> None: ... + def __init__(self, deviceIdentifiers: _Optional[_Iterable[_Union[SingularDeviceIdentifier, _Mapping]]] = ..., sharedData: _Optional[_Union[SingularSharedData, _Mapping]] = ..., applicationVersion: _Optional[str] = ..., install: bool = ..., installTime: _Optional[int] = ..., updateTime: _Optional[int] = ..., installSource: _Optional[str] = ..., installReceipt: _Optional[str] = ..., openuri: _Optional[str] = ..., ddlEnabled: bool = ..., singularLinkResolveRequired: bool = ..., installRef: _Optional[str] = ..., metaRef: _Optional[str] = ..., attributionToken: _Optional[str] = ...) -> None: ... class SingularEventRequest(_message.Message): __slots__ = ("deviceIdentifiers", "sharedData", "eventName") @@ -944,3 +961,25 @@ class ActivePamCountResponse(_message.Message): PAMCOUNT_FIELD_NUMBER: _ClassVar[int] pamCount: int def __init__(self, pamCount: _Optional[int] = ...) -> None: ... + +class NhiEnterpriseRequest(_message.Message): + __slots__ = ("enterpriseId", "startTime", "endTime") + ENTERPRISEID_FIELD_NUMBER: _ClassVar[int] + STARTTIME_FIELD_NUMBER: _ClassVar[int] + ENDTIME_FIELD_NUMBER: _ClassVar[int] + enterpriseId: int + startTime: int + endTime: int + def __init__(self, enterpriseId: _Optional[int] = ..., startTime: _Optional[int] = ..., endTime: _Optional[int] = ...) -> None: ... + +class NhiMetricsRequest(_message.Message): + __slots__ = ("enterpriseIds", "startTime", "endTime", "enterprises") + ENTERPRISEIDS_FIELD_NUMBER: _ClassVar[int] + STARTTIME_FIELD_NUMBER: _ClassVar[int] + ENDTIME_FIELD_NUMBER: _ClassVar[int] + ENTERPRISES_FIELD_NUMBER: _ClassVar[int] + enterpriseIds: _containers.RepeatedScalarFieldContainer[int] + startTime: int + endTime: int + enterprises: _containers.RepeatedCompositeFieldContainer[NhiEnterpriseRequest] + def __init__(self, enterpriseIds: _Optional[_Iterable[int]] = ..., startTime: _Optional[int] = ..., endTime: _Optional[int] = ..., enterprises: _Optional[_Iterable[_Union[NhiEnterpriseRequest, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py index e05a8dc1..410a127a 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: GraphSync.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'GraphSync.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi index 26c41dc3..594a4eec 100644 --- a/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/GraphSync_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -138,7 +137,7 @@ class GraphSyncResult(_message.Message): syncPoint: int data: _containers.RepeatedCompositeFieldContainer[GraphSyncDataPlus] hasMore: bool - def __init__(self, streamId: _Optional[bytes] = ..., syncPoint: _Optional[int] = ..., data: _Optional[_Iterable[_Union[GraphSyncDataPlus, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... + def __init__(self, streamId: _Optional[bytes] = ..., syncPoint: _Optional[int] = ..., data: _Optional[_Iterable[_Union[GraphSyncDataPlus, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... class GraphSyncMultiQuery(_message.Message): __slots__ = ("queries",) diff --git a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py index b10a1c8e..5ed6b97b 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: NotificationCenter.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'NotificationCenter.proto' ) @@ -25,7 +25,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\"2\n\x15NotificationParameter\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xa1\x03\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\x12=\n\nparameters\x18\x08 \x03(\x0b\x32).NotificationCenter.NotificationParameter\"\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\"\xd6\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\x12\x1a\n\rpredefinedUid\x18\x05 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_predefinedUid\"^\n\x18NotificationsSendRequest\x12\x42\n\rnotifications\x18\x01 \x03(\x0b\x32+.NotificationCenter.NotificationSendRequest\",\n\x17NotificationSyncRequest\x12\x11\n\tsyncPoint\x18\x01 \x01(\x03\"9\n\x10SentNotification\x12\x0c\n\x04user\x18\x01 \x01(\x05\x12\x17\n\x0fnotificationUid\x18\x02 \x01(\x0c\"\xa7\x01\n(NotificationsApprovalStatusUpdateRequest\x12>\n\x06status\x18\x01 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\x12;\n\rnotifications\x18\x02 \x03(\x0b\x32$.NotificationCenter.SentNotification*\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*\x9e\x04\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\x12\x1f\n\x1bNT_DEVICE_APPROVAL_APPROVED\x10\x0e\x12\x1d\n\x19NT_DEVICE_APPROVAL_DENIED\x10\x0f\x12\x16\n\x12NT_ACCOUNT_CREATED\x10\x10\x12\x12\n\x0eNT_2FA_ENABLED\x10\x11\x12\x13\n\x0fNT_2FA_DISABLED\x10\x12\x12\x1c\n\x18NT_SECURITY_KEYS_ENABLED\x10\x13\x12\x1d\n\x19NT_SECURITY_KEYS_DISABLED\x10\x14*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\"2\n\x15NotificationParameter\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xa1\x03\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\x12=\n\nparameters\x18\x08 \x03(\x0b\x32).NotificationCenter.NotificationParameter\"\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\"\xd6\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\x12\x1a\n\rpredefinedUid\x18\x05 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_predefinedUid\"^\n\x18NotificationsSendRequest\x12\x42\n\rnotifications\x18\x01 \x03(\x0b\x32+.NotificationCenter.NotificationSendRequest\",\n\x17NotificationSyncRequest\x12\x11\n\tsyncPoint\x18\x01 \x01(\x03\"9\n\x10SentNotification\x12\x0c\n\x04user\x18\x01 \x01(\x05\x12\x17\n\x0fnotificationUid\x18\x02 \x01(\x0c\"\xa7\x01\n(NotificationsApprovalStatusUpdateRequest\x12>\n\x06status\x18\x01 \x01(\x0e\x32..NotificationCenter.NotificationApprovalStatus\x12;\n\rnotifications\x18\x02 \x03(\x0b\x32$.NotificationCenter.SentNotification*\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*\xe3\x04\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\x12\x1f\n\x1bNT_DEVICE_APPROVAL_APPROVED\x10\x0e\x12\x1d\n\x19NT_DEVICE_APPROVAL_DENIED\x10\x0f\x12\x16\n\x12NT_ACCOUNT_CREATED\x10\x10\x12\x12\n\x0eNT_2FA_ENABLED\x10\x11\x12\x13\n\x0fNT_2FA_DISABLED\x10\x12\x12\x1c\n\x18NT_SECURITY_KEYS_ENABLED\x10\x13\x12\x1d\n\x19NT_SECURITY_KEYS_DISABLED\x10\x14\x12#\n\x1fNT_SSL_CERTIFICATE_EXPIRES_SOON\x10\x15\x12\x1e\n\x1aNT_SSL_CERTIFICATE_EXPIRED\x10\x16*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) @@ -38,11 +38,11 @@ _globals['_NOTIFICATIONCATEGORY']._serialized_start=2163 _globals['_NOTIFICATIONCATEGORY']._serialized_end=2322 _globals['_NOTIFICATIONTYPE']._serialized_start=2325 - _globals['_NOTIFICATIONTYPE']._serialized_end=2867 - _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2869 - _globals['_NOTIFICATIONREADSTATUS']._serialized_end=2958 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=2961 - _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=3095 + _globals['_NOTIFICATIONTYPE']._serialized_end=2936 + _globals['_NOTIFICATIONREADSTATUS']._serialized_start=2938 + _globals['_NOTIFICATIONREADSTATUS']._serialized_end=3027 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_start=3030 + _globals['_NOTIFICATIONAPPROVALSTATUS']._serialized_end=3164 _globals['_ENCRYPTEDDATA']._serialized_start=65 _globals['_ENCRYPTEDDATA']._serialized_end=111 _globals['_NOTIFICATIONPARAMETER']._serialized_start=113 diff --git a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi index dfcecea3..ea89c059 100644 --- a/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/NotificationCenter_pb2.pyi @@ -3,8 +3,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -42,6 +41,8 @@ class NotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NT_2FA_DISABLED: _ClassVar[NotificationType] NT_SECURITY_KEYS_ENABLED: _ClassVar[NotificationType] NT_SECURITY_KEYS_DISABLED: _ClassVar[NotificationType] + NT_SSL_CERTIFICATE_EXPIRES_SOON: _ClassVar[NotificationType] + NT_SSL_CERTIFICATE_EXPIRED: _ClassVar[NotificationType] class NotificationReadStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -86,6 +87,8 @@ NT_2FA_ENABLED: NotificationType NT_2FA_DISABLED: NotificationType NT_SECURITY_KEYS_ENABLED: NotificationType NT_SECURITY_KEYS_DISABLED: NotificationType +NT_SSL_CERTIFICATE_EXPIRES_SOON: NotificationType +NT_SSL_CERTIFICATE_EXPIRED: NotificationType NRS_UNSPECIFIED: NotificationReadStatus NRS_LAST: NotificationReadStatus NRS_READ: NotificationReadStatus @@ -158,7 +161,7 @@ class NotificationContent(_message.Message): 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]] = ..., trimmingPoint: _Optional[bool] = ..., 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") @@ -178,7 +181,7 @@ class NotificationSync(_message.Message): data: _containers.RepeatedCompositeFieldContainer[NotificationWrapper] syncPoint: int hasMore: bool - def __init__(self, data: _Optional[_Iterable[_Union[NotificationWrapper, _Mapping]]] = ..., syncPoint: _Optional[int] = ..., hasMore: _Optional[bool] = ...) -> None: ... + def __init__(self, data: _Optional[_Iterable[_Union[NotificationWrapper, _Mapping]]] = ..., syncPoint: _Optional[int] = ..., hasMore: bool = ...) -> None: ... class ReadStatusUpdate(_message.Message): __slots__ = ("notificationUid", "status") diff --git a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py index a1d6e25b..b1b143e5 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: SyncDown.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'SyncDown.proto' ) @@ -27,9 +27,12 @@ from . import APIRequest_pb2 as APIRequest__pb2 from . import enterprise_pb2 as enterprise__pb2 from . import NotificationCenter_pb2 as NotificationCenter__pb2 +from . import dag_pb2 as dag__pb2 +from . import folder_pb2 as folder__pb2 +from . import record_sharing_pb2 as record__sharing__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\"O\n\x17\x42reachWatchSecurityData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07removed\x18\x03 \x01(\x08\"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') +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\x1a\tdag.proto\x1a\x0c\x66older.proto\x1a\x14record_sharing.proto\"P\n\x0fSyncDownRequest\x12\x19\n\x11\x63ontinuationToken\x18\x01 \x01(\x0c\x12\x13\n\x0b\x64\x61taVersion\x18\x02 \x01(\x05\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\"\xb2\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\x12/\n\x0fkeeperDriveData\x18\x30 \x01(\x0b\x32\x16.Vault.KeeperDriveData\"\x98\x01\n\x0b\x44riveRecord\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\x10\n\x08\x66ileSize\x18\x06 \x01(\x03\x12\x15\n\rthumbnailSize\x18\x07 \x01(\x03\"F\n\x12\x46olderSharingState\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x0e\n\x06shared\x18\x02 \x01(\x08\x12\r\n\x05\x63ount\x18\x03 \x01(\x05\"\x9b\x08\n\x0fKeeperDriveData\x12#\n\x07\x66olders\x18\n \x03(\x0b\x32\x12.Folder.FolderData\x12%\n\nfolderKeys\x18\r \x03(\x0b\x32\x11.Folder.FolderKey\x12\x30\n\x0e\x66olderAccesses\x18\x0f \x03(\x0b\x32\x18.Folder.FolderAccessData\x12\x34\n\x15revokedFolderAccesses\x18\x11 \x03(\x0b\x32\x15.Folder.RevokedAccess\x12&\n\nrecordData\x18\x14 \x03(\x0b\x32\x12.Folder.RecordData\x12+\n\rnonSharedData\x18\x15 \x03(\x0b\x32\x14.Vault.NonSharedData\x12\x30\n\x0erecordAccesses\x18\x19 \x03(\x0b\x32\x18.Folder.RecordAccessData\x12?\n\x15revokedRecordAccesses\x18\x1b \x03(\x0b\x32 .record.v3.sharing.RevokedAccess\x12\x42\n\x13recordSharingStates\x18\x1c \x03(\x0b\x32%.record.v3.sharing.RecordSharingState\x12&\n\x0brecordLinks\x18\x1e \x03(\x0b\x32\x11.Vault.RecordLink\x12-\n\x12removedRecordLinks\x18 \x03(\x0b\x32\x11.Vault.RecordLink\x12\x34\n\x12\x62reachWatchRecords\x18( \x03(\x0b\x32\x18.Vault.BreachWatchRecord\x12\x33\n\x11securityScoreData\x18) \x03(\x0b\x32\x18.Vault.SecurityScoreData\x12?\n\x17\x62reachWatchSecurityData\x18* \x03(\x0b\x32\x1e.Vault.BreachWatchSecurityData\x12-\n\x0eremovedFolders\x18\x30 \x03(\x0b\x32\x15.Folder.FolderRemoved\x12\x36\n\x14removedFolderRecords\x18\x34 \x03(\x0b\x32\x18.Records.FolderRecordKey\x12+\n\rfolderRecords\x18\x36 \x03(\x0b\x32\x14.Folder.FolderRecord\x12\x31\n\x12recordRotationData\x18\x38 \x03(\x0b\x32\x15.Vault.RecordRotation\x12#\n\x07records\x18: \x03(\x0b\x32\x12.Vault.DriveRecord\x12\x35\n\x12\x66olderSharingState\x18< \x03(\x0b\x32\x19.Vault.FolderSharingState\x12\"\n\nrawDagData\x18\x65 \x03(\x0b\x32\x0e.Dag.DebugData\"\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\"O\n\x17\x42reachWatchSecurityData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0f\n\x07removed\x18\x03 \x01(\x08\"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,76 +40,82 @@ 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=6887 - _globals['_CACHESTATUS']._serialized_end=6921 - _globals['_RECORDROTATIONSTATUS']._serialized_start=6923 - _globals['_RECORDROTATIONSTATUS']._serialized_end=7025 - _globals['_SYNCDOWNREQUEST']._serialized_start=120 - _globals['_SYNCDOWNREQUEST']._serialized_end=185 - _globals['_SYNCDOWNRESPONSE']._serialized_start=188 - _globals['_SYNCDOWNRESPONSE']._serialized_end=2365 - _globals['_USERFOLDER']._serialized_start=2368 - _globals['_USERFOLDER']._serialized_end=2514 - _globals['_SHAREDFOLDER']._serialized_start=2517 - _globals['_SHAREDFOLDER']._serialized_end=2858 - _globals['_USERFOLDERSHAREDFOLDER']._serialized_start=2860 - _globals['_USERFOLDERSHAREDFOLDER']._serialized_end=2946 - _globals['_SHAREDFOLDERFOLDER']._serialized_start=2949 - _globals['_SHAREDFOLDERFOLDER']._serialized_end=3136 - _globals['_SHAREDFOLDERKEY']._serialized_start=3138 - _globals['_SHAREDFOLDERKEY']._serialized_end=3246 - _globals['_TEAM']._serialized_start=3249 - _globals['_TEAM']._serialized_end=3572 - _globals['_RECORD']._serialized_start=3575 - _globals['_RECORD']._serialized_end=3766 - _globals['_RECORDLINK']._serialized_start=3768 - _globals['_RECORDLINK']._serialized_end=3866 - _globals['_USERFOLDERRECORD']._serialized_start=3868 - _globals['_USERFOLDERRECORD']._serialized_end=3942 - _globals['_SHAREDFOLDERFOLDERRECORD']._serialized_start=3944 - _globals['_SHAREDFOLDERFOLDERRECORD']._serialized_end=4051 - _globals['_NONSHAREDDATA']._serialized_start=4053 - _globals['_NONSHAREDDATA']._serialized_end=4101 - _globals['_RECORDMETADATA']._serialized_start=4104 - _globals['_RECORDMETADATA']._serialized_end=4391 - _globals['_SHARINGCHANGE']._serialized_start=4393 - _globals['_SHARINGCHANGE']._serialized_end=4443 - _globals['_PROFILE']._serialized_start=4445 - _globals['_PROFILE']._serialized_end=4507 - _globals['_PROFILEPIC']._serialized_start=4509 - _globals['_PROFILEPIC']._serialized_end=4552 - _globals['_PENDINGTEAMMEMBER']._serialized_start=4554 - _globals['_PENDINGTEAMMEMBER']._serialized_end=4666 - _globals['_BREACHWATCHRECORD']._serialized_start=4669 - _globals['_BREACHWATCHRECORD']._serialized_end=4835 - _globals['_USERAUTH']._serialized_start=4838 - _globals['_USERAUTH']._serialized_end=5018 - _globals['_BREACHWATCHSECURITYDATA']._serialized_start=5020 - _globals['_BREACHWATCHSECURITYDATA']._serialized_end=5099 - _globals['_REUSEDPASSWORDS']._serialized_start=5101 - _globals['_REUSEDPASSWORDS']._serialized_end=5151 - _globals['_SHAREDFOLDERRECORD']._serialized_start=5154 - _globals['_SHAREDFOLDERRECORD']._serialized_end=5451 - _globals['_SHAREDFOLDERUSER']._serialized_start=5454 - _globals['_SHAREDFOLDERUSER']._serialized_end=5695 - _globals['_SHAREDFOLDERTEAM']._serialized_start=5698 - _globals['_SHAREDFOLDERTEAM']._serialized_end=5932 - _globals['_KSMCHANGE']._serialized_start=5935 - _globals['_KSMCHANGE']._serialized_end=6073 - _globals['_SHAREINVITATION']._serialized_start=6075 - _globals['_SHAREINVITATION']._serialized_end=6110 - _globals['_USER']._serialized_start=6112 - _globals['_USER']._serialized_end=6156 - _globals['_SYNCDIAGNOSTICS']._serialized_start=6158 - _globals['_SYNCDIAGNOSTICS']._serialized_end=6281 - _globals['_RECORDROTATION']._serialized_start=6284 - _globals['_RECORDROTATION']._serialized_end=6522 - _globals['_SECURITYSCOREDATA']._serialized_start=6524 - _globals['_SECURITYSCOREDATA']._serialized_end=6594 - _globals['_BREACHWATCHGETSYNCDATAREQUEST']._serialized_start=6596 - _globals['_BREACHWATCHGETSYNCDATAREQUEST']._serialized_end=6647 - _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_start=6650 - _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_end=6829 - _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_start=6831 - _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_end=6885 + _globals['_CACHESTATUS']._serialized_start=8279 + _globals['_CACHESTATUS']._serialized_end=8313 + _globals['_RECORDROTATIONSTATUS']._serialized_start=8315 + _globals['_RECORDROTATIONSTATUS']._serialized_end=8417 + _globals['_SYNCDOWNREQUEST']._serialized_start=167 + _globals['_SYNCDOWNREQUEST']._serialized_end=247 + _globals['_SYNCDOWNRESPONSE']._serialized_start=250 + _globals['_SYNCDOWNRESPONSE']._serialized_end=2476 + _globals['_DRIVERECORD']._serialized_start=2479 + _globals['_DRIVERECORD']._serialized_end=2631 + _globals['_FOLDERSHARINGSTATE']._serialized_start=2633 + _globals['_FOLDERSHARINGSTATE']._serialized_end=2703 + _globals['_KEEPERDRIVEDATA']._serialized_start=2706 + _globals['_KEEPERDRIVEDATA']._serialized_end=3757 + _globals['_USERFOLDER']._serialized_start=3760 + _globals['_USERFOLDER']._serialized_end=3906 + _globals['_SHAREDFOLDER']._serialized_start=3909 + _globals['_SHAREDFOLDER']._serialized_end=4250 + _globals['_USERFOLDERSHAREDFOLDER']._serialized_start=4252 + _globals['_USERFOLDERSHAREDFOLDER']._serialized_end=4338 + _globals['_SHAREDFOLDERFOLDER']._serialized_start=4341 + _globals['_SHAREDFOLDERFOLDER']._serialized_end=4528 + _globals['_SHAREDFOLDERKEY']._serialized_start=4530 + _globals['_SHAREDFOLDERKEY']._serialized_end=4638 + _globals['_TEAM']._serialized_start=4641 + _globals['_TEAM']._serialized_end=4964 + _globals['_RECORD']._serialized_start=4967 + _globals['_RECORD']._serialized_end=5158 + _globals['_RECORDLINK']._serialized_start=5160 + _globals['_RECORDLINK']._serialized_end=5258 + _globals['_USERFOLDERRECORD']._serialized_start=5260 + _globals['_USERFOLDERRECORD']._serialized_end=5334 + _globals['_SHAREDFOLDERFOLDERRECORD']._serialized_start=5336 + _globals['_SHAREDFOLDERFOLDERRECORD']._serialized_end=5443 + _globals['_NONSHAREDDATA']._serialized_start=5445 + _globals['_NONSHAREDDATA']._serialized_end=5493 + _globals['_RECORDMETADATA']._serialized_start=5496 + _globals['_RECORDMETADATA']._serialized_end=5783 + _globals['_SHARINGCHANGE']._serialized_start=5785 + _globals['_SHARINGCHANGE']._serialized_end=5835 + _globals['_PROFILE']._serialized_start=5837 + _globals['_PROFILE']._serialized_end=5899 + _globals['_PROFILEPIC']._serialized_start=5901 + _globals['_PROFILEPIC']._serialized_end=5944 + _globals['_PENDINGTEAMMEMBER']._serialized_start=5946 + _globals['_PENDINGTEAMMEMBER']._serialized_end=6058 + _globals['_BREACHWATCHRECORD']._serialized_start=6061 + _globals['_BREACHWATCHRECORD']._serialized_end=6227 + _globals['_USERAUTH']._serialized_start=6230 + _globals['_USERAUTH']._serialized_end=6410 + _globals['_BREACHWATCHSECURITYDATA']._serialized_start=6412 + _globals['_BREACHWATCHSECURITYDATA']._serialized_end=6491 + _globals['_REUSEDPASSWORDS']._serialized_start=6493 + _globals['_REUSEDPASSWORDS']._serialized_end=6543 + _globals['_SHAREDFOLDERRECORD']._serialized_start=6546 + _globals['_SHAREDFOLDERRECORD']._serialized_end=6843 + _globals['_SHAREDFOLDERUSER']._serialized_start=6846 + _globals['_SHAREDFOLDERUSER']._serialized_end=7087 + _globals['_SHAREDFOLDERTEAM']._serialized_start=7090 + _globals['_SHAREDFOLDERTEAM']._serialized_end=7324 + _globals['_KSMCHANGE']._serialized_start=7327 + _globals['_KSMCHANGE']._serialized_end=7465 + _globals['_SHAREINVITATION']._serialized_start=7467 + _globals['_SHAREINVITATION']._serialized_end=7502 + _globals['_USER']._serialized_start=7504 + _globals['_USER']._serialized_end=7548 + _globals['_SYNCDIAGNOSTICS']._serialized_start=7550 + _globals['_SYNCDIAGNOSTICS']._serialized_end=7673 + _globals['_RECORDROTATION']._serialized_start=7676 + _globals['_RECORDROTATION']._serialized_end=7914 + _globals['_SECURITYSCOREDATA']._serialized_start=7916 + _globals['_SECURITYSCOREDATA']._serialized_end=7986 + _globals['_BREACHWATCHGETSYNCDATAREQUEST']._serialized_start=7988 + _globals['_BREACHWATCHGETSYNCDATAREQUEST']._serialized_end=8039 + _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_start=8042 + _globals['_BREACHWATCHGETSYNCDATARESPONSE']._serialized_end=8221 + _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_start=8223 + _globals['_GETACCOUNTUIDMAPRESPONSE']._serialized_end=8277 # @@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 933b685d..8c1b6222 100644 --- a/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/SyncDown_pb2.pyi @@ -3,12 +3,14 @@ import breachwatch_pb2 as _breachwatch_pb2 import APIRequest_pb2 as _APIRequest_pb2 import enterprise_pb2 as _enterprise_pb2 import NotificationCenter_pb2 as _NotificationCenter_pb2 +import dag_pb2 as _dag_pb2 +import folder_pb2 as _folder_pb2 +import record_sharing_pb2 as _record_sharing_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 from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -31,15 +33,17 @@ RRST_SUCCESS: RecordRotationStatus RRST_FAILURE: RecordRotationStatus class SyncDownRequest(_message.Message): - __slots__ = ("continuationToken", "dataVersion") + __slots__ = ("continuationToken", "dataVersion", "debug") CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] DATAVERSION_FIELD_NUMBER: _ClassVar[int] + DEBUG_FIELD_NUMBER: _ClassVar[int] continuationToken: bytes dataVersion: int - def __init__(self, continuationToken: _Optional[bytes] = ..., dataVersion: _Optional[int] = ...) -> None: ... + debug: bool + def __init__(self, continuationToken: _Optional[bytes] = ..., dataVersion: _Optional[int] = ..., debug: bool = ...) -> None: ... class SyncDownResponse(_message.Message): - __slots__ = ("continuationToken", "hasMore", "cacheStatus", "userFolders", "sharedFolders", "userFolderSharedFolders", "sharedFolderFolders", "records", "recordMetaData", "nonSharedData", "recordLinks", "userFolderRecords", "sharedFolderRecords", "sharedFolderFolderRecords", "sharedFolderUsers", "sharedFolderTeams", "recordAddAuditData", "teams", "sharingChanges", "profile", "profilePic", "pendingTeamMembers", "breachWatchRecords", "userAuths", "breachWatchSecurityData", "reusedPasswords", "removedUserFolders", "removedSharedFolders", "removedUserFolderSharedFolders", "removedSharedFolderFolders", "removedRecords", "removedRecordLinks", "removedUserFolderRecords", "removedSharedFolderRecords", "removedSharedFolderFolderRecords", "removedSharedFolderUsers", "removedSharedFolderTeams", "removedTeams", "ksmAppShares", "ksmAppClients", "shareInvitations", "diagnostics", "recordRotations", "users", "removedUsers", "securityScoreData", "notificationSync") + __slots__ = ("continuationToken", "hasMore", "cacheStatus", "userFolders", "sharedFolders", "userFolderSharedFolders", "sharedFolderFolders", "records", "recordMetaData", "nonSharedData", "recordLinks", "userFolderRecords", "sharedFolderRecords", "sharedFolderFolderRecords", "sharedFolderUsers", "sharedFolderTeams", "recordAddAuditData", "teams", "sharingChanges", "profile", "profilePic", "pendingTeamMembers", "breachWatchRecords", "userAuths", "breachWatchSecurityData", "reusedPasswords", "removedUserFolders", "removedSharedFolders", "removedUserFolderSharedFolders", "removedSharedFolderFolders", "removedRecords", "removedRecordLinks", "removedUserFolderRecords", "removedSharedFolderRecords", "removedSharedFolderFolderRecords", "removedSharedFolderUsers", "removedSharedFolderTeams", "removedTeams", "ksmAppShares", "ksmAppClients", "shareInvitations", "diagnostics", "recordRotations", "users", "removedUsers", "securityScoreData", "notificationSync", "keeperDriveData") CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] HASMORE_FIELD_NUMBER: _ClassVar[int] CACHESTATUS_FIELD_NUMBER: _ClassVar[int] @@ -87,6 +91,7 @@ class SyncDownResponse(_message.Message): REMOVEDUSERS_FIELD_NUMBER: _ClassVar[int] SECURITYSCOREDATA_FIELD_NUMBER: _ClassVar[int] NOTIFICATIONSYNC_FIELD_NUMBER: _ClassVar[int] + KEEPERDRIVEDATA_FIELD_NUMBER: _ClassVar[int] continuationToken: bytes hasMore: bool cacheStatus: CacheStatus @@ -134,7 +139,82 @@ class SyncDownResponse(_message.Message): removedUsers: _containers.RepeatedScalarFieldContainer[bytes] securityScoreData: _containers.RepeatedCompositeFieldContainer[SecurityScoreData] notificationSync: _containers.RepeatedCompositeFieldContainer[_NotificationCenter_pb2.NotificationWrapper] - def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., userFolders: _Optional[_Iterable[_Union[UserFolder, _Mapping]]] = ..., sharedFolders: _Optional[_Iterable[_Union[SharedFolder, _Mapping]]] = ..., userFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., sharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., records: _Optional[_Iterable[_Union[Record, _Mapping]]] = ..., recordMetaData: _Optional[_Iterable[_Union[RecordMetaData, _Mapping]]] = ..., nonSharedData: _Optional[_Iterable[_Union[NonSharedData, _Mapping]]] = ..., recordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., userFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., sharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., sharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., sharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., sharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., recordAddAuditData: _Optional[_Iterable[bytes]] = ..., teams: _Optional[_Iterable[_Union[Team, _Mapping]]] = ..., sharingChanges: _Optional[_Iterable[_Union[SharingChange, _Mapping]]] = ..., profile: _Optional[_Union[Profile, _Mapping]] = ..., profilePic: _Optional[_Union[ProfilePic, _Mapping]] = ..., pendingTeamMembers: _Optional[_Iterable[_Union[PendingTeamMember, _Mapping]]] = ..., breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., userAuths: _Optional[_Iterable[_Union[UserAuth, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., reusedPasswords: _Optional[_Union[ReusedPasswords, _Mapping]] = ..., removedUserFolders: _Optional[_Iterable[bytes]] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., removedUserFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., removedSharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., removedRecords: _Optional[_Iterable[bytes]] = ..., removedRecordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., removedUserFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., removedSharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., removedSharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., removedSharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., removedSharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., removedTeams: _Optional[_Iterable[bytes]] = ..., ksmAppShares: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., ksmAppClients: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., shareInvitations: _Optional[_Iterable[_Union[ShareInvitation, _Mapping]]] = ..., diagnostics: _Optional[_Union[SyncDiagnostics, _Mapping]] = ..., recordRotations: _Optional[_Iterable[_Union[RecordRotation, _Mapping]]] = ..., users: _Optional[_Iterable[_Union[User, _Mapping]]] = ..., removedUsers: _Optional[_Iterable[bytes]] = ..., securityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ..., notificationSync: _Optional[_Iterable[_Union[_NotificationCenter_pb2.NotificationWrapper, _Mapping]]] = ...) -> None: ... + keeperDriveData: KeeperDriveData + def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., userFolders: _Optional[_Iterable[_Union[UserFolder, _Mapping]]] = ..., sharedFolders: _Optional[_Iterable[_Union[SharedFolder, _Mapping]]] = ..., userFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., sharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., records: _Optional[_Iterable[_Union[Record, _Mapping]]] = ..., recordMetaData: _Optional[_Iterable[_Union[RecordMetaData, _Mapping]]] = ..., nonSharedData: _Optional[_Iterable[_Union[NonSharedData, _Mapping]]] = ..., recordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., userFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., sharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., sharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., sharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., sharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., recordAddAuditData: _Optional[_Iterable[bytes]] = ..., teams: _Optional[_Iterable[_Union[Team, _Mapping]]] = ..., sharingChanges: _Optional[_Iterable[_Union[SharingChange, _Mapping]]] = ..., profile: _Optional[_Union[Profile, _Mapping]] = ..., profilePic: _Optional[_Union[ProfilePic, _Mapping]] = ..., pendingTeamMembers: _Optional[_Iterable[_Union[PendingTeamMember, _Mapping]]] = ..., breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., userAuths: _Optional[_Iterable[_Union[UserAuth, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., reusedPasswords: _Optional[_Union[ReusedPasswords, _Mapping]] = ..., removedUserFolders: _Optional[_Iterable[bytes]] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., removedUserFolderSharedFolders: _Optional[_Iterable[_Union[UserFolderSharedFolder, _Mapping]]] = ..., removedSharedFolderFolders: _Optional[_Iterable[_Union[SharedFolderFolder, _Mapping]]] = ..., removedRecords: _Optional[_Iterable[bytes]] = ..., removedRecordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., removedUserFolderRecords: _Optional[_Iterable[_Union[UserFolderRecord, _Mapping]]] = ..., removedSharedFolderRecords: _Optional[_Iterable[_Union[SharedFolderRecord, _Mapping]]] = ..., removedSharedFolderFolderRecords: _Optional[_Iterable[_Union[SharedFolderFolderRecord, _Mapping]]] = ..., removedSharedFolderUsers: _Optional[_Iterable[_Union[SharedFolderUser, _Mapping]]] = ..., removedSharedFolderTeams: _Optional[_Iterable[_Union[SharedFolderTeam, _Mapping]]] = ..., removedTeams: _Optional[_Iterable[bytes]] = ..., ksmAppShares: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., ksmAppClients: _Optional[_Iterable[_Union[KsmChange, _Mapping]]] = ..., shareInvitations: _Optional[_Iterable[_Union[ShareInvitation, _Mapping]]] = ..., diagnostics: _Optional[_Union[SyncDiagnostics, _Mapping]] = ..., recordRotations: _Optional[_Iterable[_Union[RecordRotation, _Mapping]]] = ..., users: _Optional[_Iterable[_Union[User, _Mapping]]] = ..., removedUsers: _Optional[_Iterable[bytes]] = ..., securityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ..., notificationSync: _Optional[_Iterable[_Union[_NotificationCenter_pb2.NotificationWrapper, _Mapping]]] = ..., keeperDriveData: _Optional[_Union[KeeperDriveData, _Mapping]] = ...) -> None: ... + +class DriveRecord(_message.Message): + __slots__ = ("recordUid", "revision", "version", "shared", "clientModifiedTime", "fileSize", "thumbnailSize") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + REVISION_FIELD_NUMBER: _ClassVar[int] + VERSION_FIELD_NUMBER: _ClassVar[int] + SHARED_FIELD_NUMBER: _ClassVar[int] + CLIENTMODIFIEDTIME_FIELD_NUMBER: _ClassVar[int] + FILESIZE_FIELD_NUMBER: _ClassVar[int] + THUMBNAILSIZE_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + revision: int + version: int + shared: bool + clientModifiedTime: int + fileSize: int + thumbnailSize: int + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: bool = ..., clientModifiedTime: _Optional[int] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ...) -> None: ... + +class FolderSharingState(_message.Message): + __slots__ = ("folderUid", "shared", "count") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + SHARED_FIELD_NUMBER: _ClassVar[int] + COUNT_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + shared: bool + count: int + def __init__(self, folderUid: _Optional[bytes] = ..., shared: bool = ..., count: _Optional[int] = ...) -> None: ... + +class KeeperDriveData(_message.Message): + __slots__ = ("folders", "folderKeys", "folderAccesses", "revokedFolderAccesses", "recordData", "nonSharedData", "recordAccesses", "revokedRecordAccesses", "recordSharingStates", "recordLinks", "removedRecordLinks", "breachWatchRecords", "securityScoreData", "breachWatchSecurityData", "removedFolders", "removedFolderRecords", "folderRecords", "recordRotationData", "records", "folderSharingState", "rawDagData") + FOLDERS_FIELD_NUMBER: _ClassVar[int] + FOLDERKEYS_FIELD_NUMBER: _ClassVar[int] + FOLDERACCESSES_FIELD_NUMBER: _ClassVar[int] + REVOKEDFOLDERACCESSES_FIELD_NUMBER: _ClassVar[int] + RECORDDATA_FIELD_NUMBER: _ClassVar[int] + NONSHAREDDATA_FIELD_NUMBER: _ClassVar[int] + RECORDACCESSES_FIELD_NUMBER: _ClassVar[int] + REVOKEDRECORDACCESSES_FIELD_NUMBER: _ClassVar[int] + RECORDSHARINGSTATES_FIELD_NUMBER: _ClassVar[int] + RECORDLINKS_FIELD_NUMBER: _ClassVar[int] + REMOVEDRECORDLINKS_FIELD_NUMBER: _ClassVar[int] + BREACHWATCHRECORDS_FIELD_NUMBER: _ClassVar[int] + SECURITYSCOREDATA_FIELD_NUMBER: _ClassVar[int] + BREACHWATCHSECURITYDATA_FIELD_NUMBER: _ClassVar[int] + REMOVEDFOLDERS_FIELD_NUMBER: _ClassVar[int] + REMOVEDFOLDERRECORDS_FIELD_NUMBER: _ClassVar[int] + FOLDERRECORDS_FIELD_NUMBER: _ClassVar[int] + RECORDROTATIONDATA_FIELD_NUMBER: _ClassVar[int] + RECORDS_FIELD_NUMBER: _ClassVar[int] + FOLDERSHARINGSTATE_FIELD_NUMBER: _ClassVar[int] + RAWDAGDATA_FIELD_NUMBER: _ClassVar[int] + folders: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderData] + folderKeys: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderKey] + folderAccesses: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderAccessData] + revokedFolderAccesses: _containers.RepeatedCompositeFieldContainer[_folder_pb2.RevokedAccess] + recordData: _containers.RepeatedCompositeFieldContainer[_folder_pb2.RecordData] + nonSharedData: _containers.RepeatedCompositeFieldContainer[NonSharedData] + recordAccesses: _containers.RepeatedCompositeFieldContainer[_folder_pb2.RecordAccessData] + revokedRecordAccesses: _containers.RepeatedCompositeFieldContainer[_record_sharing_pb2.RevokedAccess] + recordSharingStates: _containers.RepeatedCompositeFieldContainer[_record_sharing_pb2.RecordSharingState] + recordLinks: _containers.RepeatedCompositeFieldContainer[RecordLink] + removedRecordLinks: _containers.RepeatedCompositeFieldContainer[RecordLink] + breachWatchRecords: _containers.RepeatedCompositeFieldContainer[BreachWatchRecord] + securityScoreData: _containers.RepeatedCompositeFieldContainer[SecurityScoreData] + breachWatchSecurityData: _containers.RepeatedCompositeFieldContainer[BreachWatchSecurityData] + removedFolders: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderRemoved] + removedFolderRecords: _containers.RepeatedCompositeFieldContainer[_record_pb2.FolderRecordKey] + folderRecords: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderRecord] + recordRotationData: _containers.RepeatedCompositeFieldContainer[RecordRotation] + records: _containers.RepeatedCompositeFieldContainer[DriveRecord] + folderSharingState: _containers.RepeatedCompositeFieldContainer[FolderSharingState] + rawDagData: _containers.RepeatedCompositeFieldContainer[_dag_pb2.DebugData] + def __init__(self, folders: _Optional[_Iterable[_Union[_folder_pb2.FolderData, _Mapping]]] = ..., folderKeys: _Optional[_Iterable[_Union[_folder_pb2.FolderKey, _Mapping]]] = ..., folderAccesses: _Optional[_Iterable[_Union[_folder_pb2.FolderAccessData, _Mapping]]] = ..., revokedFolderAccesses: _Optional[_Iterable[_Union[_folder_pb2.RevokedAccess, _Mapping]]] = ..., recordData: _Optional[_Iterable[_Union[_folder_pb2.RecordData, _Mapping]]] = ..., nonSharedData: _Optional[_Iterable[_Union[NonSharedData, _Mapping]]] = ..., recordAccesses: _Optional[_Iterable[_Union[_folder_pb2.RecordAccessData, _Mapping]]] = ..., revokedRecordAccesses: _Optional[_Iterable[_Union[_record_sharing_pb2.RevokedAccess, _Mapping]]] = ..., recordSharingStates: _Optional[_Iterable[_Union[_record_sharing_pb2.RecordSharingState, _Mapping]]] = ..., recordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., removedRecordLinks: _Optional[_Iterable[_Union[RecordLink, _Mapping]]] = ..., breachWatchRecords: _Optional[_Iterable[_Union[BreachWatchRecord, _Mapping]]] = ..., securityScoreData: _Optional[_Iterable[_Union[SecurityScoreData, _Mapping]]] = ..., breachWatchSecurityData: _Optional[_Iterable[_Union[BreachWatchSecurityData, _Mapping]]] = ..., removedFolders: _Optional[_Iterable[_Union[_folder_pb2.FolderRemoved, _Mapping]]] = ..., removedFolderRecords: _Optional[_Iterable[_Union[_record_pb2.FolderRecordKey, _Mapping]]] = ..., folderRecords: _Optional[_Iterable[_Union[_folder_pb2.FolderRecord, _Mapping]]] = ..., recordRotationData: _Optional[_Iterable[_Union[RecordRotation, _Mapping]]] = ..., records: _Optional[_Iterable[_Union[DriveRecord, _Mapping]]] = ..., folderSharingState: _Optional[_Iterable[_Union[FolderSharingState, _Mapping]]] = ..., rawDagData: _Optional[_Iterable[_Union[_dag_pb2.DebugData, _Mapping]]] = ...) -> None: ... class UserFolder(_message.Message): __slots__ = ("folderUid", "parentUid", "userFolderKey", "keyType", "revision", "data") @@ -180,7 +260,7 @@ class SharedFolder(_message.Message): owner: str ownerAccountUid: bytes name: bytes - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., sharedFolderKey: _Optional[bytes] = ..., keyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., data: _Optional[bytes] = ..., defaultManageRecords: _Optional[bool] = ..., defaultManageUsers: _Optional[bool] = ..., defaultCanEdit: _Optional[bool] = ..., defaultCanReshare: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., owner: _Optional[str] = ..., ownerAccountUid: _Optional[bytes] = ..., name: _Optional[bytes] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., sharedFolderKey: _Optional[bytes] = ..., keyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., data: _Optional[bytes] = ..., defaultManageRecords: bool = ..., defaultManageUsers: bool = ..., defaultCanEdit: bool = ..., defaultCanReshare: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., owner: _Optional[str] = ..., ownerAccountUid: _Optional[bytes] = ..., name: _Optional[bytes] = ...) -> None: ... class UserFolderSharedFolder(_message.Message): __slots__ = ("folderUid", "sharedFolderUid", "revision") @@ -246,7 +326,7 @@ class Team(_message.Message): sharedFolderKeys: _containers.RepeatedCompositeFieldContainer[SharedFolderKey] teamEccPrivateKey: bytes teamEccPublicKey: bytes - def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., teamKey: _Optional[bytes] = ..., teamKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., teamPrivateKey: _Optional[bytes] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ..., restrictView: _Optional[bool] = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., sharedFolderKeys: _Optional[_Iterable[_Union[SharedFolderKey, _Mapping]]] = ..., teamEccPrivateKey: _Optional[bytes] = ..., teamEccPublicKey: _Optional[bytes] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., teamKey: _Optional[bytes] = ..., teamKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., teamPrivateKey: _Optional[bytes] = ..., restrictEdit: bool = ..., restrictShare: bool = ..., restrictView: bool = ..., removedSharedFolders: _Optional[_Iterable[bytes]] = ..., sharedFolderKeys: _Optional[_Iterable[_Union[SharedFolderKey, _Mapping]]] = ..., teamEccPrivateKey: _Optional[bytes] = ..., teamEccPublicKey: _Optional[bytes] = ...) -> None: ... class Record(_message.Message): __slots__ = ("recordUid", "revision", "version", "shared", "clientModifiedTime", "data", "extra", "udata", "fileSize", "thumbnailSize") @@ -270,7 +350,7 @@ class Record(_message.Message): udata: str fileSize: int thumbnailSize: int - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: _Optional[bool] = ..., clientModifiedTime: _Optional[int] = ..., data: _Optional[bytes] = ..., extra: _Optional[bytes] = ..., udata: _Optional[str] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: bool = ..., clientModifiedTime: _Optional[int] = ..., data: _Optional[bytes] = ..., extra: _Optional[bytes] = ..., udata: _Optional[str] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ...) -> None: ... class RecordLink(_message.Message): __slots__ = ("parentRecordUid", "childRecordUid", "recordKey", "revision") @@ -336,7 +416,7 @@ class RecordMetaData(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType ownerUsername: str - def __init__(self, recordUid: _Optional[bytes] = ..., owner: _Optional[bool] = ..., recordKey: _Optional[bytes] = ..., recordKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., canShare: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., owner: bool = ..., recordKey: _Optional[bytes] = ..., recordKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ..., canShare: bool = ..., canEdit: bool = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ...) -> None: ... class SharingChange(_message.Message): __slots__ = ("recordUid", "shared") @@ -344,7 +424,7 @@ class SharingChange(_message.Message): SHARED_FIELD_NUMBER: _ClassVar[int] recordUid: bytes shared: bool - def __init__(self, recordUid: _Optional[bytes] = ..., shared: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., shared: bool = ...) -> None: ... class Profile(_message.Message): __slots__ = ("data", "profileName", "revision") @@ -410,7 +490,7 @@ class UserAuth(_message.Message): encryptedClientKey: bytes revision: int name: str - def __init__(self, uid: _Optional[bytes] = ..., loginType: _Optional[_Union[_APIRequest_pb2.LoginType, str]] = ..., deleted: _Optional[bool] = ..., iterations: _Optional[int] = ..., salt: _Optional[bytes] = ..., encryptedClientKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., name: _Optional[str] = ...) -> None: ... + def __init__(self, uid: _Optional[bytes] = ..., loginType: _Optional[_Union[_APIRequest_pb2.LoginType, str]] = ..., deleted: bool = ..., iterations: _Optional[int] = ..., salt: _Optional[bytes] = ..., encryptedClientKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., name: _Optional[str] = ...) -> None: ... class BreachWatchSecurityData(_message.Message): __slots__ = ("recordUid", "revision", "removed") @@ -420,7 +500,7 @@ class BreachWatchSecurityData(_message.Message): recordUid: bytes revision: int removed: bool - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., removed: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., removed: bool = ...) -> None: ... class ReusedPasswords(_message.Message): __slots__ = ("count", "revision") @@ -454,7 +534,7 @@ class SharedFolderRecord(_message.Message): expirationNotificationType: _record_pb2.TimerNotificationType ownerUsername: str rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., canShare: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., owner: _Optional[bool] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., canShare: bool = ..., canEdit: bool = ..., ownerAccountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., owner: bool = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., ownerUsername: _Optional[str] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderUser(_message.Message): __slots__ = ("sharedFolderUid", "username", "manageRecords", "manageUsers", "accountUid", "expiration", "expirationNotificationType", "rotateOnExpiration") @@ -474,7 +554,7 @@ class SharedFolderUser(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., username: _Optional[str] = ..., manageRecords: _Optional[bool] = ..., manageUsers: _Optional[bool] = ..., accountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., username: _Optional[str] = ..., manageRecords: bool = ..., manageUsers: bool = ..., accountUid: _Optional[bytes] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderTeam(_message.Message): __slots__ = ("sharedFolderUid", "teamUid", "name", "manageRecords", "manageUsers", "expiration", "expirationNotificationType", "rotateOnExpiration") @@ -494,7 +574,7 @@ class SharedFolderTeam(_message.Message): expiration: int expirationNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., manageRecords: _Optional[bool] = ..., manageUsers: _Optional[bool] = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., manageRecords: bool = ..., manageUsers: bool = ..., expiration: _Optional[int] = ..., expirationNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class KsmChange(_message.Message): __slots__ = ("appRecordUid", "detailId", "removed", "appClientType", "expiration") @@ -508,7 +588,7 @@ class KsmChange(_message.Message): removed: bool appClientType: _enterprise_pb2.AppClientType expiration: int - def __init__(self, appRecordUid: _Optional[bytes] = ..., detailId: _Optional[bytes] = ..., removed: _Optional[bool] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., expiration: _Optional[int] = ...) -> None: ... + def __init__(self, appRecordUid: _Optional[bytes] = ..., detailId: _Optional[bytes] = ..., removed: bool = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., expiration: _Optional[int] = ...) -> None: ... class ShareInvitation(_message.Message): __slots__ = ("username",) @@ -558,7 +638,7 @@ class RecordRotation(_message.Message): resourceUid: bytes lastRotation: int lastRotationStatus: RecordRotationStatus - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., resourceUid: _Optional[bytes] = ..., lastRotation: _Optional[int] = ..., lastRotationStatus: _Optional[_Union[RecordRotationStatus, str]] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: bool = ..., resourceUid: _Optional[bytes] = ..., lastRotation: _Optional[int] = ..., lastRotationStatus: _Optional[_Union[RecordRotationStatus, str]] = ...) -> None: ... class SecurityScoreData(_message.Message): __slots__ = ("recordUid", "data", "revision") diff --git a/keepersdk-package/src/keepersdk/proto/automator_pb2.py b/keepersdk-package/src/keepersdk/proto/automator_pb2.py index b17ba298..3cba9106 100644 --- a/keepersdk-package/src/keepersdk/proto/automator_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/automator_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: automator.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'automator.proto' ) @@ -27,7 +27,7 @@ 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\"\x8f\x04\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\x42\x02\x18\x01\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\x12\x39\n\x12sslCertificateInfo\x18\x0f \x03(\x0b\x32\x1d.Automator.SSLCertificateInfo\" \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\"\x93\x01\n\x12SSLCertificateInfo\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x04\x12\x0f\n\x07hostUrl\x18\x02 \x01(\t\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\x0e\n\x06issuer\x18\x04 \x01(\t\x12\x10\n\x08issuedOn\x18\x05 \x01(\t\x12\x11\n\texpiresOn\x18\x06 \x01(\t\x12\x11\n\tcheckedOn\x18\x07 \x01(\t*I\n\x1dSsoAuthenticationProtocolType\x12\x14\n\x10UNKNOWN_PROTOCOL\x10\x00\x12\t\n\x05SAML2\x10\x01\x12\x07\n\x03JWT\x10\x02*<\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') +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\"\x8f\x04\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\x42\x02\x18\x01\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\x12\x39\n\x12sslCertificateInfo\x18\x0f \x03(\x0b\x32\x1d.Automator.SSLCertificateInfo\" \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\"\x94\x03\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\x12$\n\x1csslCertificateExpirationDate\x18\x0c \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\"\x93\x01\n\x12SSLCertificateInfo\x12\x13\n\x0b\x61utomatorId\x18\x01 \x01(\x04\x12\x0f\n\x07hostUrl\x18\x02 \x01(\t\x12\x0f\n\x07subject\x18\x03 \x01(\t\x12\x0e\n\x06issuer\x18\x04 \x01(\t\x12\x10\n\x08issuedOn\x18\x05 \x01(\x04\x12\x11\n\texpiresOn\x18\x06 \x01(\x04\x12\x11\n\tcheckedOn\x18\x07 \x01(\x04*I\n\x1dSsoAuthenticationProtocolType\x12\x14\n\x10UNKNOWN_PROTOCOL\x10\x00\x12\t\n\x05SAML2\x10\x01\x12\x07\n\x03JWT\x10\x02*<\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') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,14 +37,14 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\tAutomator' _globals['_STATUSRESPONSE'].fields_by_name['sslCertificateExpiration']._loaded_options = None _globals['_STATUSRESPONSE'].fields_by_name['sslCertificateExpiration']._serialized_options = b'\030\001' - _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_start=7406 - _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_end=7479 - _globals['_CERTIFICATEFORMAT']._serialized_start=7481 - _globals['_CERTIFICATEFORMAT']._serialized_end=7541 - _globals['_SKILLTYPE']._serialized_start=7543 - _globals['_SKILLTYPE']._serialized_end=7646 - _globals['_AUTOMATORSTATE']._serialized_start=7649 - _globals['_AUTOMATORSTATE']._serialized_end=7784 + _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_start=7444 + _globals['_SSOAUTHENTICATIONPROTOCOLTYPE']._serialized_end=7517 + _globals['_CERTIFICATEFORMAT']._serialized_start=7519 + _globals['_CERTIFICATEFORMAT']._serialized_end=7579 + _globals['_SKILLTYPE']._serialized_start=7581 + _globals['_SKILLTYPE']._serialized_end=7684 + _globals['_AUTOMATORSTATE']._serialized_start=7687 + _globals['_AUTOMATORSTATE']._serialized_end=7822 _globals['_AUTOMATORSETTINGVALUE']._serialized_start=80 _globals['_AUTOMATORSETTINGVALUE']._serialized_end=399 _globals['_APPROVEDEVICEREQUEST']._serialized_start=402 @@ -70,53 +70,53 @@ _globals['_ADMINRESPONSE']._serialized_start=3365 _globals['_ADMINRESPONSE']._serialized_end=3463 _globals['_AUTOMATORINFO']._serialized_start=3466 - _globals['_AUTOMATORINFO']._serialized_end=3832 - _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_start=3834 - _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_end=3935 - _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_start=3937 - _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_end=3987 - _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_start=3989 - _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_end=4038 - _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_start=4040 - _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_end=4102 - _globals['_ADMINGETAUTOMATORREQUEST']._serialized_start=4104 - _globals['_ADMINGETAUTOMATORREQUEST']._serialized_end=4151 - _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_start=4153 - _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_end=4220 - _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_start=4223 - _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_end=4423 - _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_start=4426 - _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_end=4678 - _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_start=4681 - _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_end=4847 - _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_start=4849 - _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_end=4899 - _globals['_AUTOMATORSKILL']._serialized_start=4901 - _globals['_AUTOMATORSKILL']._serialized_end=4996 - _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_start=4998 - _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_end=5114 - _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_start=5116 - _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_end=5165 - _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_start=5167 - _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_end=5221 - _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_start=5223 - _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_end=5270 - _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_start=5272 - _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_end=5324 - _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_start=5327 - _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_end=5682 - _globals['_TEAMDESCRIPTION']._serialized_start=5685 - _globals['_TEAMDESCRIPTION']._serialized_end=5823 - _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_start=5826 - _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_end=5979 - _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_start=5982 - _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_end=6281 - _globals['_APPROVETEAMSREQUEST']._serialized_start=6284 - _globals['_APPROVETEAMSREQUEST']._serialized_end=6583 - _globals['_APPROVETEAMSRESPONSE']._serialized_start=6585 - _globals['_APPROVETEAMSRESPONSE']._serialized_end=6709 - _globals['_APPROVEONETEAMRESPONSE']._serialized_start=6712 - _globals['_APPROVEONETEAMRESPONSE']._serialized_end=7254 - _globals['_SSLCERTIFICATEINFO']._serialized_start=7257 - _globals['_SSLCERTIFICATEINFO']._serialized_end=7404 + _globals['_AUTOMATORINFO']._serialized_end=3870 + _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_start=3872 + _globals['_ADMINCREATEAUTOMATORREQUEST']._serialized_end=3973 + _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_start=3975 + _globals['_ADMINDELETEAUTOMATORREQUEST']._serialized_end=4025 + _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_start=4027 + _globals['_ADMINGETAUTOMATORSONNODEREQUEST']._serialized_end=4076 + _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_start=4078 + _globals['_ADMINGETAUTOMATORSFORENTERPRISEREQUEST']._serialized_end=4140 + _globals['_ADMINGETAUTOMATORREQUEST']._serialized_start=4142 + _globals['_ADMINGETAUTOMATORREQUEST']._serialized_end=4189 + _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_start=4191 + _globals['_ADMINENABLEAUTOMATORREQUEST']._serialized_end=4258 + _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_start=4261 + _globals['_ADMINEDITAUTOMATORREQUEST']._serialized_end=4461 + _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_start=4464 + _globals['_ADMINSETUPAUTOMATORREQUEST']._serialized_end=4716 + _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_start=4719 + _globals['_ADMINSETUPAUTOMATORRESPONSE']._serialized_end=4885 + _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_start=4887 + _globals['_ADMINAUTOMATORSKILLSREQUEST']._serialized_end=4937 + _globals['_AUTOMATORSKILL']._serialized_start=4939 + _globals['_AUTOMATORSKILL']._serialized_end=5034 + _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_start=5036 + _globals['_ADMINAUTOMATORSKILLSRESPONSE']._serialized_end=5152 + _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_start=5154 + _globals['_ADMINRESETAUTOMATORREQUEST']._serialized_end=5203 + _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_start=5205 + _globals['_ADMININITIALIZEAUTOMATORREQUEST']._serialized_end=5259 + _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_start=5261 + _globals['_ADMINAUTOMATORLOGREQUEST']._serialized_end=5308 + _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_start=5310 + _globals['_ADMINAUTOMATORLOGCLEARREQUEST']._serialized_end=5362 + _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_start=5365 + _globals['_APPROVETEAMSFORUSERREQUEST']._serialized_end=5720 + _globals['_TEAMDESCRIPTION']._serialized_start=5723 + _globals['_TEAMDESCRIPTION']._serialized_end=5861 + _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_start=5864 + _globals['_APPROVETEAMSFORUSERRESPONSE']._serialized_end=6017 + _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_start=6020 + _globals['_APPROVEONETEAMFORUSERRESPONSE']._serialized_end=6319 + _globals['_APPROVETEAMSREQUEST']._serialized_start=6322 + _globals['_APPROVETEAMSREQUEST']._serialized_end=6621 + _globals['_APPROVETEAMSRESPONSE']._serialized_start=6623 + _globals['_APPROVETEAMSRESPONSE']._serialized_end=6747 + _globals['_APPROVEONETEAMRESPONSE']._serialized_start=6750 + _globals['_APPROVEONETEAMRESPONSE']._serialized_end=7292 + _globals['_SSLCERTIFICATEINFO']._serialized_start=7295 + _globals['_SSLCERTIFICATEINFO']._serialized_end=7442 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi b/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi index 024af8a7..c83a01ae 100644 --- a/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/automator_pb2.pyi @@ -5,8 +5,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -84,7 +83,7 @@ class AutomatorSettingValue(_message.Message): translated: bool userVisible: bool required: bool - def __init__(self, settingId: _Optional[int] = ..., settingTypeId: _Optional[int] = ..., settingTag: _Optional[str] = ..., settingName: _Optional[str] = ..., settingValue: _Optional[str] = ..., dataType: _Optional[_Union[_ssocloud_pb2.DataType, str]] = ..., lastModified: _Optional[str] = ..., fromFile: _Optional[bool] = ..., encrypted: _Optional[bool] = ..., encoded: _Optional[bool] = ..., editable: _Optional[bool] = ..., translated: _Optional[bool] = ..., userVisible: _Optional[bool] = ..., required: _Optional[bool] = ...) -> None: ... + def __init__(self, settingId: _Optional[int] = ..., settingTypeId: _Optional[int] = ..., settingTag: _Optional[str] = ..., settingName: _Optional[str] = ..., settingValue: _Optional[str] = ..., dataType: _Optional[_Union[_ssocloud_pb2.DataType, str]] = ..., lastModified: _Optional[str] = ..., fromFile: bool = ..., encrypted: bool = ..., encoded: bool = ..., editable: bool = ..., translated: bool = ..., userVisible: bool = ..., required: bool = ...) -> None: ... class ApproveDeviceRequest(_message.Message): __slots__ = ("automatorId", "ssoAuthenticationProtocolType", "authMessage", "email", "devicePublicKey", "serverEccPublicKeyId", "userEncryptedDataKey", "userEncryptedDataKeyType", "ipAddress", "isTesting", "isEccOnly") @@ -110,7 +109,7 @@ class ApproveDeviceRequest(_message.Message): ipAddress: str isTesting: bool isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., serverEccPublicKeyId: _Optional[int] = ..., userEncryptedDataKey: _Optional[bytes] = ..., userEncryptedDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., ipAddress: _Optional[str] = ..., isTesting: _Optional[bool] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., devicePublicKey: _Optional[bytes] = ..., serverEccPublicKeyId: _Optional[int] = ..., userEncryptedDataKey: _Optional[bytes] = ..., userEncryptedDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., ipAddress: _Optional[str] = ..., isTesting: bool = ..., isEccOnly: bool = ...) -> None: ... class SetupRequest(_message.Message): __slots__ = ("automatorId", "serverEccPublicKeyId", "automatorState", "encryptedEnterprisePrivateEccKey", "encryptedEnterprisePrivateRsaKey", "automatorSkills", "encryptedTreeKey", "isEccOnly") @@ -130,7 +129,7 @@ class SetupRequest(_message.Message): automatorSkills: _containers.RepeatedCompositeFieldContainer[AutomatorSkill] encryptedTreeKey: bytes isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., encryptedEnterprisePrivateEccKey: _Optional[bytes] = ..., encryptedEnterprisePrivateRsaKey: _Optional[bytes] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., encryptedTreeKey: _Optional[bytes] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., encryptedEnterprisePrivateEccKey: _Optional[bytes] = ..., encryptedEnterprisePrivateRsaKey: _Optional[bytes] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., encryptedTreeKey: _Optional[bytes] = ..., isEccOnly: bool = ...) -> None: ... class StatusRequest(_message.Message): __slots__ = ("automatorId", "serverEccPublicKeyId", "isEccOnly") @@ -140,7 +139,7 @@ class StatusRequest(_message.Message): automatorId: int serverEccPublicKeyId: int isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., serverEccPublicKeyId: _Optional[int] = ..., isEccOnly: bool = ...) -> None: ... class InitializeRequest(_message.Message): __slots__ = ("automatorId", "idpMetadata", "idpSigningCertificate", "ssoEntityId", "emailMapping", "firstnameMapping", "lastnameMapping", "disabled", "serverEccPublicKeyId", "config", "sslMode", "persistState", "disableSniCheck", "sslCertificateFilename", "sslCertificateFilePassword", "sslCertificateKeyPassword", "sslCertificateContents", "automatorHost", "automatorPort", "ipAllow", "ipDeny", "isEccOnly") @@ -188,7 +187,7 @@ class InitializeRequest(_message.Message): ipAllow: str ipDeny: str isEccOnly: bool - def __init__(self, automatorId: _Optional[int] = ..., idpMetadata: _Optional[str] = ..., idpSigningCertificate: _Optional[bytes] = ..., ssoEntityId: _Optional[str] = ..., emailMapping: _Optional[str] = ..., firstnameMapping: _Optional[str] = ..., lastnameMapping: _Optional[str] = ..., disabled: _Optional[bool] = ..., serverEccPublicKeyId: _Optional[int] = ..., config: _Optional[bytes] = ..., sslMode: _Optional[str] = ..., persistState: _Optional[bool] = ..., disableSniCheck: _Optional[bool] = ..., sslCertificateFilename: _Optional[str] = ..., sslCertificateFilePassword: _Optional[str] = ..., sslCertificateKeyPassword: _Optional[str] = ..., sslCertificateContents: _Optional[bytes] = ..., automatorHost: _Optional[str] = ..., automatorPort: _Optional[str] = ..., ipAllow: _Optional[str] = ..., ipDeny: _Optional[str] = ..., isEccOnly: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., idpMetadata: _Optional[str] = ..., idpSigningCertificate: _Optional[bytes] = ..., ssoEntityId: _Optional[str] = ..., emailMapping: _Optional[str] = ..., firstnameMapping: _Optional[str] = ..., lastnameMapping: _Optional[str] = ..., disabled: bool = ..., serverEccPublicKeyId: _Optional[int] = ..., config: _Optional[bytes] = ..., sslMode: _Optional[str] = ..., persistState: bool = ..., disableSniCheck: bool = ..., sslCertificateFilename: _Optional[str] = ..., sslCertificateFilePassword: _Optional[str] = ..., sslCertificateKeyPassword: _Optional[str] = ..., sslCertificateContents: _Optional[bytes] = ..., automatorHost: _Optional[str] = ..., automatorPort: _Optional[str] = ..., ipAllow: _Optional[str] = ..., ipDeny: _Optional[str] = ..., isEccOnly: bool = ...) -> None: ... class NotInitializedResponse(_message.Message): __slots__ = ("automatorTransmissionKey", "signingCertificate", "signingCertificateFilename", "signingCertificatePassword", "signingKeyPassword", "signingCertificateFormat", "automatorPublicKey", "config") @@ -236,7 +235,7 @@ class AutomatorResponse(_message.Message): automatorState: AutomatorState automatorPublicEccKey: bytes version: _version_pb2.Version - def __init__(self, automatorId: _Optional[int] = ..., enabled: _Optional[bool] = ..., timestamp: _Optional[int] = ..., approveDevice: _Optional[_Union[ApproveDeviceResponse, _Mapping]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., notInitialized: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., error: _Optional[_Union[ErrorResponse, _Mapping]] = ..., approveTeamsForUser: _Optional[_Union[ApproveTeamsForUserResponse, _Mapping]] = ..., approveTeams: _Optional[_Union[ApproveTeamsResponse, _Mapping]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorPublicEccKey: _Optional[bytes] = ..., version: _Optional[_Union[_version_pb2.Version, _Mapping]] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., enabled: bool = ..., timestamp: _Optional[int] = ..., approveDevice: _Optional[_Union[ApproveDeviceResponse, _Mapping]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., notInitialized: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., error: _Optional[_Union[ErrorResponse, _Mapping]] = ..., approveTeamsForUser: _Optional[_Union[ApproveTeamsForUserResponse, _Mapping]] = ..., approveTeams: _Optional[_Union[ApproveTeamsResponse, _Mapping]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorPublicEccKey: _Optional[bytes] = ..., version: _Optional[_Union[_version_pb2.Version, _Mapping]] = ...) -> None: ... class ApproveDeviceResponse(_message.Message): __slots__ = ("approved", "encryptedUserDataKey", "message", "encryptedUserDataKeyType") @@ -248,7 +247,7 @@ class ApproveDeviceResponse(_message.Message): encryptedUserDataKey: bytes message: str encryptedUserDataKeyType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: _Optional[bool] = ..., encryptedUserDataKey: _Optional[bytes] = ..., message: _Optional[str] = ..., encryptedUserDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: bool = ..., encryptedUserDataKey: _Optional[bytes] = ..., message: _Optional[str] = ..., encryptedUserDataKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... class StatusResponse(_message.Message): __slots__ = ("initialized", "enabledTimestamp", "initializedTimestamp", "updatedTimestamp", "numberOfDevicesApproved", "numberOfDevicesDenied", "numberOfErrors", "sslCertificateExpiration", "notInitializedResponse", "config", "numberOfTeamMembershipsApproved", "numberOfTeamMembershipsDenied", "numberOfTeamsApproved", "numberOfTeamsDenied", "sslCertificateInfo") @@ -282,7 +281,7 @@ class StatusResponse(_message.Message): numberOfTeamsApproved: int numberOfTeamsDenied: int sslCertificateInfo: _containers.RepeatedCompositeFieldContainer[SSLCertificateInfo] - def __init__(self, initialized: _Optional[bool] = ..., enabledTimestamp: _Optional[int] = ..., initializedTimestamp: _Optional[int] = ..., updatedTimestamp: _Optional[int] = ..., numberOfDevicesApproved: _Optional[int] = ..., numberOfDevicesDenied: _Optional[int] = ..., numberOfErrors: _Optional[int] = ..., sslCertificateExpiration: _Optional[int] = ..., notInitializedResponse: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., config: _Optional[bytes] = ..., numberOfTeamMembershipsApproved: _Optional[int] = ..., numberOfTeamMembershipsDenied: _Optional[int] = ..., numberOfTeamsApproved: _Optional[int] = ..., numberOfTeamsDenied: _Optional[int] = ..., sslCertificateInfo: _Optional[_Iterable[_Union[SSLCertificateInfo, _Mapping]]] = ...) -> None: ... + def __init__(self, initialized: bool = ..., enabledTimestamp: _Optional[int] = ..., initializedTimestamp: _Optional[int] = ..., updatedTimestamp: _Optional[int] = ..., numberOfDevicesApproved: _Optional[int] = ..., numberOfDevicesDenied: _Optional[int] = ..., numberOfErrors: _Optional[int] = ..., sslCertificateExpiration: _Optional[int] = ..., notInitializedResponse: _Optional[_Union[NotInitializedResponse, _Mapping]] = ..., config: _Optional[bytes] = ..., numberOfTeamMembershipsApproved: _Optional[int] = ..., numberOfTeamMembershipsDenied: _Optional[int] = ..., numberOfTeamsApproved: _Optional[int] = ..., numberOfTeamsDenied: _Optional[int] = ..., sslCertificateInfo: _Optional[_Iterable[_Union[SSLCertificateInfo, _Mapping]]] = ...) -> None: ... class ErrorResponse(_message.Message): __slots__ = ("message",) @@ -310,10 +309,10 @@ class AdminResponse(_message.Message): success: bool message: str automatorInfo: _containers.RepeatedCompositeFieldContainer[AutomatorInfo] - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorInfo: _Optional[_Iterable[_Union[AutomatorInfo, _Mapping]]] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorInfo: _Optional[_Iterable[_Union[AutomatorInfo, _Mapping]]] = ...) -> None: ... class AutomatorInfo(_message.Message): - __slots__ = ("automatorId", "nodeId", "name", "enabled", "url", "automatorSkills", "automatorSettingValues", "status", "logEntries", "automatorState", "version") + __slots__ = ("automatorId", "nodeId", "name", "enabled", "url", "automatorSkills", "automatorSettingValues", "status", "logEntries", "automatorState", "version", "sslCertificateExpirationDate") AUTOMATORID_FIELD_NUMBER: _ClassVar[int] NODEID_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] @@ -325,6 +324,7 @@ class AutomatorInfo(_message.Message): LOGENTRIES_FIELD_NUMBER: _ClassVar[int] AUTOMATORSTATE_FIELD_NUMBER: _ClassVar[int] VERSION_FIELD_NUMBER: _ClassVar[int] + SSLCERTIFICATEEXPIRATIONDATE_FIELD_NUMBER: _ClassVar[int] automatorId: int nodeId: int name: str @@ -336,7 +336,8 @@ class AutomatorInfo(_message.Message): logEntries: _containers.RepeatedCompositeFieldContainer[LogEntry] automatorState: AutomatorState version: str - def __init__(self, automatorId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: _Optional[bool] = ..., url: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., logEntries: _Optional[_Iterable[_Union[LogEntry, _Mapping]]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., version: _Optional[str] = ...) -> None: ... + sslCertificateExpirationDate: str + def __init__(self, automatorId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: bool = ..., url: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ..., status: _Optional[_Union[StatusResponse, _Mapping]] = ..., logEntries: _Optional[_Iterable[_Union[LogEntry, _Mapping]]] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., version: _Optional[str] = ..., sslCertificateExpirationDate: _Optional[str] = ...) -> None: ... class AdminCreateAutomatorRequest(_message.Message): __slots__ = ("nodeId", "name", "skill") @@ -378,7 +379,7 @@ class AdminEnableAutomatorRequest(_message.Message): ENABLED_FIELD_NUMBER: _ClassVar[int] automatorId: int enabled: bool - def __init__(self, automatorId: _Optional[int] = ..., enabled: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., enabled: bool = ...) -> None: ... class AdminEditAutomatorRequest(_message.Message): __slots__ = ("automatorId", "name", "enabled", "url", "skillTypes", "automatorSettingValues") @@ -394,7 +395,7 @@ class AdminEditAutomatorRequest(_message.Message): url: str skillTypes: _containers.RepeatedScalarFieldContainer[SkillType] automatorSettingValues: _containers.RepeatedCompositeFieldContainer[AutomatorSettingValue] - def __init__(self, automatorId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: _Optional[bool] = ..., url: _Optional[str] = ..., skillTypes: _Optional[_Iterable[_Union[SkillType, str]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., name: _Optional[str] = ..., enabled: bool = ..., url: _Optional[str] = ..., skillTypes: _Optional[_Iterable[_Union[SkillType, str]]] = ..., automatorSettingValues: _Optional[_Iterable[_Union[AutomatorSettingValue, _Mapping]]] = ...) -> None: ... class AdminSetupAutomatorRequest(_message.Message): __slots__ = ("automatorId", "automatorState", "encryptedEccEnterprisePrivateKey", "encryptedRsaEnterprisePrivateKey", "skillTypes", "encryptedTreeKey") @@ -424,7 +425,7 @@ class AdminSetupAutomatorResponse(_message.Message): automatorId: int automatorState: AutomatorState automatorEccPublicKey: bytes - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorEccPublicKey: _Optional[bytes] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorId: _Optional[int] = ..., automatorState: _Optional[_Union[AutomatorState, str]] = ..., automatorEccPublicKey: _Optional[bytes] = ...) -> None: ... class AdminAutomatorSkillsRequest(_message.Message): __slots__ = ("automatorId",) @@ -450,7 +451,7 @@ class AdminAutomatorSkillsResponse(_message.Message): success: bool message: str automatorSkills: _containers.RepeatedCompositeFieldContainer[AutomatorSkill] - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ..., automatorSkills: _Optional[_Iterable[_Union[AutomatorSkill, _Mapping]]] = ...) -> None: ... class AdminResetAutomatorRequest(_message.Message): __slots__ = ("automatorId",) @@ -500,7 +501,7 @@ class ApproveTeamsForUserRequest(_message.Message): isTesting: bool isEccOnly: bool userPublicKeyEcc: bytes - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., userPublicKey: _Optional[bytes] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isTesting: _Optional[bool] = ..., isEccOnly: _Optional[bool] = ..., userPublicKeyEcc: _Optional[bytes] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., userPublicKey: _Optional[bytes] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isTesting: bool = ..., isEccOnly: bool = ..., userPublicKeyEcc: _Optional[bytes] = ...) -> None: ... class TeamDescription(_message.Message): __slots__ = ("teamUid", "teamName", "encryptedTeamKey", "encryptedTeamKeyType") @@ -544,7 +545,7 @@ class ApproveOneTeamForUserResponse(_message.Message): userEncryptedTeamKeyType: _enterprise_pb2.EncryptedKeyType userEncryptedTeamKeyByEcc: bytes userEncryptedTeamKeyByEccType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: _Optional[bool] = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., userEncryptedTeamKey: _Optional[bytes] = ..., userEncryptedTeamKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., userEncryptedTeamKeyByEcc: _Optional[bytes] = ..., userEncryptedTeamKeyByEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: bool = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., userEncryptedTeamKey: _Optional[bytes] = ..., userEncryptedTeamKeyType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., userEncryptedTeamKeyByEcc: _Optional[bytes] = ..., userEncryptedTeamKeyByEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... class ApproveTeamsRequest(_message.Message): __slots__ = ("automatorId", "ssoAuthenticationProtocolType", "authMessage", "email", "serverEccPublicKeyId", "ipAddress", "teamDescription", "isEccOnly", "isTesting") @@ -566,7 +567,7 @@ class ApproveTeamsRequest(_message.Message): teamDescription: _containers.RepeatedCompositeFieldContainer[TeamDescription] isEccOnly: bool isTesting: bool - def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isEccOnly: _Optional[bool] = ..., isTesting: _Optional[bool] = ...) -> None: ... + def __init__(self, automatorId: _Optional[int] = ..., ssoAuthenticationProtocolType: _Optional[_Union[SsoAuthenticationProtocolType, str]] = ..., authMessage: _Optional[str] = ..., email: _Optional[str] = ..., serverEccPublicKeyId: _Optional[int] = ..., ipAddress: _Optional[str] = ..., teamDescription: _Optional[_Iterable[_Union[TeamDescription, _Mapping]]] = ..., isEccOnly: bool = ..., isTesting: bool = ...) -> None: ... class ApproveTeamsResponse(_message.Message): __slots__ = ("automatorId", "message", "approveTeamResponse") @@ -608,7 +609,7 @@ class ApproveOneTeamResponse(_message.Message): teamPublicKeyEcc: bytes encryptedTeamPrivateKeyEcc: bytes encryptedTeamPrivateKeyEccType: _enterprise_pb2.EncryptedKeyType - def __init__(self, approved: _Optional[bool] = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., encryptedTeamKeyCbc: _Optional[bytes] = ..., encryptedTeamKeyCbcType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., encryptedTeamKeyGcm: _Optional[bytes] = ..., encryptedTeamKeyGcmType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsaType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... + def __init__(self, approved: bool = ..., message: _Optional[str] = ..., teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., encryptedTeamKeyCbc: _Optional[bytes] = ..., encryptedTeamKeyCbcType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., encryptedTeamKeyGcm: _Optional[bytes] = ..., encryptedTeamKeyGcmType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsa: _Optional[bytes] = ..., encryptedTeamPrivateKeyRsaType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ..., teamPublicKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEcc: _Optional[bytes] = ..., encryptedTeamPrivateKeyEccType: _Optional[_Union[_enterprise_pb2.EncryptedKeyType, str]] = ...) -> None: ... class SSLCertificateInfo(_message.Message): __slots__ = ("automatorId", "hostUrl", "subject", "issuer", "issuedOn", "expiresOn", "checkedOn") @@ -623,7 +624,7 @@ class SSLCertificateInfo(_message.Message): hostUrl: str subject: str issuer: str - issuedOn: str - expiresOn: str - checkedOn: str - def __init__(self, automatorId: _Optional[int] = ..., hostUrl: _Optional[str] = ..., subject: _Optional[str] = ..., issuer: _Optional[str] = ..., issuedOn: _Optional[str] = ..., expiresOn: _Optional[str] = ..., checkedOn: _Optional[str] = ...) -> None: ... + issuedOn: int + expiresOn: int + checkedOn: int + def __init__(self, automatorId: _Optional[int] = ..., hostUrl: _Optional[str] = ..., subject: _Optional[str] = ..., issuer: _Optional[str] = ..., issuedOn: _Optional[int] = ..., expiresOn: _Optional[int] = ..., checkedOn: _Optional[int] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py index 77b8c0cb..a62a1639 100644 --- a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: breachwatch.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'breachwatch.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi index d7e60f07..c888e6f2 100644 --- a/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/breachwatch_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -24,7 +23,7 @@ class BreachWatchRecordRequest(_message.Message): encryptedData: bytes breachWatchInfoType: BreachWatchInfoType updateUserWhoScanned: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: bool = ...) -> None: ... class BreachWatchUpdateRequest(_message.Message): __slots__ = ("breachWatchRecordRequest", "encryptedData") @@ -62,7 +61,7 @@ class BreachWatchTokenResponse(_message.Message): CLIENTENCRYPTED_FIELD_NUMBER: _ClassVar[int] breachWatchToken: bytes clientEncrypted: bool - def __init__(self, breachWatchToken: _Optional[bytes] = ..., clientEncrypted: _Optional[bool] = ...) -> None: ... + def __init__(self, breachWatchToken: _Optional[bytes] = ..., clientEncrypted: bool = ...) -> None: ... class AnonymizedTokenResponse(_message.Message): __slots__ = ("domainToken", "emailToken", "passwordToken") @@ -100,7 +99,7 @@ class HashStatus(_message.Message): hash1: bytes euid: bytes breachDetected: bool - def __init__(self, hash1: _Optional[bytes] = ..., euid: _Optional[bytes] = ..., breachDetected: _Optional[bool] = ...) -> None: ... + def __init__(self, hash1: _Optional[bytes] = ..., euid: _Optional[bytes] = ..., breachDetected: bool = ...) -> None: ... class BreachWatchStatusResponse(_message.Message): __slots__ = ("hashStatus",) @@ -140,7 +139,7 @@ class PaidUserResponse(_message.Message): __slots__ = ("paidUser",) PAIDUSER_FIELD_NUMBER: _ClassVar[int] paidUser: bool - def __init__(self, paidUser: _Optional[bool] = ...) -> None: ... + def __init__(self, paidUser: bool = ...) -> None: ... class DetailedScanRequest(_message.Message): __slots__ = ("email",) @@ -166,7 +165,7 @@ class BreachEvent(_message.Message): passwordInBreach: bool date: str description: str - def __init__(self, site: _Optional[str] = ..., email: _Optional[str] = ..., passwordInBreach: _Optional[bool] = ..., date: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... + def __init__(self, site: _Optional[str] = ..., email: _Optional[str] = ..., passwordInBreach: bool = ..., date: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... class UseOneTimeTokenResponse(_message.Message): __slots__ = ("emailBreaches", "passwordBreaches", "breachEvents", "email") diff --git a/keepersdk-package/src/keepersdk/proto/client_pb2.py b/keepersdk-package/src/keepersdk/proto/client_pb2.py index 2e0ec061..17d89514 100644 --- a/keepersdk-package/src/keepersdk/proto/client_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/client_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: client.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'client.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/client_pb2.pyi b/keepersdk-package/src/keepersdk/proto/client_pb2.pyi index 888b5f66..d04a2e6d 100644 --- a/keepersdk-package/src/keepersdk/proto/client_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/client_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -45,7 +44,7 @@ class BreachWatchRecordRequest(_message.Message): encryptedData: bytes breachWatchInfoType: BreachWatchInfoType updateUserWhoScanned: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., breachWatchInfoType: _Optional[_Union[BreachWatchInfoType, str]] = ..., updateUserWhoScanned: bool = ...) -> None: ... class BreachWatchData(_message.Message): __slots__ = ("passwords", "emails", "domains") diff --git a/keepersdk-package/src/keepersdk/proto/dag_pb2.py b/keepersdk-package/src/keepersdk/proto/dag_pb2.py new file mode 100644 index 00000000..38f9bd88 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/dag_pb2.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: dag.proto +# 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, + 29, + 3, + '', + 'dag.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tdag.proto\x12\x03\x44\x61g\">\n\x03Ref\x12\x1a\n\x04type\x18\x01 \x01(\x0e\x32\x0c.Dag.RefType\x12\r\n\x05value\x18\x02 \x01(\x0c\x12\x0c\n\x04name\x18\x03 \x01(\t\"z\n\x04\x44\x61ta\x12\x1f\n\x08\x64\x61taType\x18\x01 \x01(\x0e\x32\r.Dag.DataType\x12\x15\n\x03ref\x18\x02 \x01(\x0b\x32\x08.Dag.Ref\x12\x1b\n\tparentRef\x18\x03 \x01(\x0b\x32\x08.Dag.Ref\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\x0c\x12\x0c\n\x04path\x18\x05 \x01(\t\"G\n\x08SyncData\x12\x17\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\t.Dag.Data\x12\x11\n\tsyncPoint\x18\x02 \x01(\x03\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\"q\n\tDebugData\x12\x10\n\x08\x64\x61taType\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x04 \x01(\t\x12\x1e\n\x03ref\x18\x02 \x01(\x0b\x32\x11.Dag.DebugRefInfo\x12$\n\tparentRef\x18\x03 \x01(\x0b\x32\x11.Dag.DebugRefInfo\".\n\x0c\x44\x65\x62ugRefInfo\x12\x0f\n\x07refType\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c*\x8d\x01\n\x07RefType\x12\x0b\n\x07GENERAL\x10\x00\x12\x08\n\x04USER\x10\x01\x12\n\n\x06\x44\x45VICE\x10\x02\x12\x07\n\x03REC\x10\x03\x12\n\n\x06\x46OLDER\x10\x04\x12\x08\n\x04TEAM\x10\x05\x12\x0e\n\nENTERPRISE\x10\x06\x12\x11\n\rPAM_DIRECTORY\x10\x07\x12\x0f\n\x0bPAM_MACHINE\x10\x08\x12\x0c\n\x08PAM_USER\x10\t*X\n\x08\x44\x61taType\x12\x08\n\x04\x44\x41TA\x10\x00\x12\x07\n\x03KEY\x10\x01\x12\x08\n\x04LINK\x10\x02\x12\x07\n\x03\x41\x43L\x10\x03\x12\x0c\n\x08\x44\x45LETION\x10\x04\x12\n\n\x06\x44\x45NIAL\x10\x05\x12\x0c\n\x08UNDENIAL\x10\x06\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03\x44\x61gb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'dag_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\003Dag' + _globals['_REFTYPE']._serialized_start=443 + _globals['_REFTYPE']._serialized_end=584 + _globals['_DATATYPE']._serialized_start=586 + _globals['_DATATYPE']._serialized_end=674 + _globals['_REF']._serialized_start=18 + _globals['_REF']._serialized_end=80 + _globals['_DATA']._serialized_start=82 + _globals['_DATA']._serialized_end=204 + _globals['_SYNCDATA']._serialized_start=206 + _globals['_SYNCDATA']._serialized_end=277 + _globals['_DEBUGDATA']._serialized_start=279 + _globals['_DEBUGDATA']._serialized_end=392 + _globals['_DEBUGREFINFO']._serialized_start=394 + _globals['_DEBUGREFINFO']._serialized_end=440 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/dag_pb2.pyi b/keepersdk-package/src/keepersdk/proto/dag_pb2.pyi new file mode 100644 index 00000000..8738ef40 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/dag_pb2.pyi @@ -0,0 +1,101 @@ +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class RefType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + 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_USER: _ClassVar[RefType] + +class DataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DATA: _ClassVar[DataType] + KEY: _ClassVar[DataType] + LINK: _ClassVar[DataType] + ACL: _ClassVar[DataType] + DELETION: _ClassVar[DataType] + DENIAL: _ClassVar[DataType] + UNDENIAL: _ClassVar[DataType] +GENERAL: RefType +USER: RefType +DEVICE: RefType +REC: RefType +FOLDER: RefType +TEAM: RefType +ENTERPRISE: RefType +PAM_DIRECTORY: RefType +PAM_MACHINE: RefType +PAM_USER: RefType +DATA: DataType +KEY: DataType +LINK: DataType +ACL: DataType +DELETION: DataType +DENIAL: DataType +UNDENIAL: DataType + +class Ref(_message.Message): + __slots__ = ("type", "value", "name") + TYPE_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + type: RefType + value: bytes + name: str + def __init__(self, type: _Optional[_Union[RefType, str]] = ..., value: _Optional[bytes] = ..., name: _Optional[str] = ...) -> None: ... + +class Data(_message.Message): + __slots__ = ("dataType", "ref", "parentRef", "content", "path") + DATATYPE_FIELD_NUMBER: _ClassVar[int] + REF_FIELD_NUMBER: _ClassVar[int] + PARENTREF_FIELD_NUMBER: _ClassVar[int] + CONTENT_FIELD_NUMBER: _ClassVar[int] + PATH_FIELD_NUMBER: _ClassVar[int] + dataType: DataType + ref: Ref + parentRef: Ref + content: bytes + path: str + def __init__(self, dataType: _Optional[_Union[DataType, str]] = ..., ref: _Optional[_Union[Ref, _Mapping]] = ..., parentRef: _Optional[_Union[Ref, _Mapping]] = ..., content: _Optional[bytes] = ..., path: _Optional[str] = ...) -> None: ... + +class SyncData(_message.Message): + __slots__ = ("data", "syncPoint", "hasMore") + DATA_FIELD_NUMBER: _ClassVar[int] + SYNCPOINT_FIELD_NUMBER: _ClassVar[int] + HASMORE_FIELD_NUMBER: _ClassVar[int] + data: _containers.RepeatedCompositeFieldContainer[Data] + syncPoint: int + hasMore: bool + def __init__(self, data: _Optional[_Iterable[_Union[Data, _Mapping]]] = ..., syncPoint: _Optional[int] = ..., hasMore: bool = ...) -> None: ... + +class DebugData(_message.Message): + __slots__ = ("dataType", "path", "ref", "parentRef") + DATATYPE_FIELD_NUMBER: _ClassVar[int] + PATH_FIELD_NUMBER: _ClassVar[int] + REF_FIELD_NUMBER: _ClassVar[int] + PARENTREF_FIELD_NUMBER: _ClassVar[int] + dataType: str + path: str + ref: DebugRefInfo + parentRef: DebugRefInfo + def __init__(self, dataType: _Optional[str] = ..., path: _Optional[str] = ..., ref: _Optional[_Union[DebugRefInfo, _Mapping]] = ..., parentRef: _Optional[_Union[DebugRefInfo, _Mapping]] = ...) -> None: ... + +class DebugRefInfo(_message.Message): + __slots__ = ("refType", "value") + REFTYPE_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + refType: str + value: bytes + def __init__(self, refType: _Optional[str] = ..., value: _Optional[bytes] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py index f6bfe9b7..2646c89c 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: enterprise.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'enterprise.proto' ) @@ -24,7 +24,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\"/\n\x0bTeamsByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\"<\n\x12ManagedNodesByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x03(\x03\"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\"d\n\x11LoginToMcResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTreeKey\x18\x02 \x01(\t\x12\x16\n\x0e\x66orbidKeyType2\x18\x04 \x01(\x08\"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\"T\n\x17PrivilegesByManagedNode\x12\x15\n\rmanagedNodeId\x18\x01 \x01(\x03\x12\x0e\n\x06roleId\x18\x02 \x01(\x03\x12\x12\n\nprivileges\x18\x03 \x03(\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\"E\n\x18TeamEnterpriseUserRemove\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"j\n TeamEnterpriseUserRemovesRequest\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x03(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\"{\n!TeamEnterpriseUserRemovesResponse\x12V\n teamEnterpriseUserRemoveResponse\x18\x01 \x03(\x0b\x32,.Enterprise.TeamEnterpriseUserRemoveResponse\"\xb8\x01\n TeamEnterpriseUserRemoveResponse\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x01(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\nresultCode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \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\".\n\x0bRolesByTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0e\n\x06roleId\x18\x02 \x03(\x03\"\x8d\x01\n\x10LockUsersRequest\x12\x1d\n\x15lockEnterpriseUserIds\x18\x01 \x03(\x03\x12 \n\x18\x64isableEnterpriseUserIds\x18\x02 \x03(\x03\x12\x1f\n\x17unlockEnterpriseUserIds\x18\x03 \x03(\x03\x12\x17\n\x0f\x64\x65leteIfPending\x18\x04 \x01(\x08\"C\n\x11LockUsersResponse\x12.\n\x08response\x18\x01 \x03(\x0b\x32\x1c.Enterprise.LockUserResponse\"n\n\x10LockUserResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.Enterprise.UserLockStatus\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x01(\t*\x1b\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x07\n\x03\x45\x43\x43\x10\x01*\x9a\x02\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\x12\x13\n\x0fINVALID_ROLE_ID\x10\x08\x12\x1d\n\x19PAM_LICENSE_SEAT_EXCEEDED\x10\t*=\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*\xe1\x03\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*\"\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*\xa5\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\x12\x15\n\x11\x46ORBID_KEY_TYPE_1\x10\x0b*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*s\n\x0eUserLockStatus\x12\x17\n\x13UNKNOWN_LOCK_STATUS\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x0c\n\x08\x44ISABLED\x10\x02\x12\x0c\n\x08UNLOCKED\x10\x03\x12\x0b\n\x07\x44\x45LETED\x10\x04\x12\x13\n\x0f\x43\x41NT_BE_PENDING\x10\x05\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\"/\n\x0bTeamsByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x0f\n\x07teamUid\x18\x02 \x03(\x0c\"<\n\x12ManagedNodesByRole\x12\x0f\n\x07role_id\x18\x01 \x01(\x03\x12\x15\n\rmanagedNodeId\x18\x02 \x03(\x03\"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\"w\n\x11LoginToMcResponse\x12\x1d\n\x15\x65ncryptedSessionToken\x18\x01 \x01(\x0c\x12\x18\n\x10\x65ncryptedTreeKey\x18\x02 \x01(\t\x12\x11\n\tkeyTypeId\x18\x03 \x01(\x05\x12\x16\n\x0e\x66orbidKeyType2\x18\x04 \x01(\x08\"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\"T\n\x17PrivilegesByManagedNode\x12\x15\n\rmanagedNodeId\x18\x01 \x01(\x03\x12\x0e\n\x06roleId\x18\x02 \x01(\x03\x12\x12\n\nprivileges\x18\x03 \x03(\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\"\xa8\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\x12\x15\n\rtreeKeyTypeId\x18\x0c \x01(\x05\"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\"\x84\x02\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\x12\x16\n\x0enhiTierAddOnId\x18\x0c \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\"E\n\x18TeamEnterpriseUserRemove\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x65nterpriseUserId\x18\x02 \x01(\x03\"j\n TeamEnterpriseUserRemovesRequest\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x03(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\"{\n!TeamEnterpriseUserRemovesResponse\x12V\n teamEnterpriseUserRemoveResponse\x18\x01 \x03(\x0b\x32,.Enterprise.TeamEnterpriseUserRemoveResponse\"\xb8\x01\n TeamEnterpriseUserRemoveResponse\x12\x46\n\x18teamEnterpriseUserRemove\x18\x01 \x01(\x0b\x32$.Enterprise.TeamEnterpriseUserRemove\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x12\n\nresultCode\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\x12\x16\n\x0e\x61\x64\x64itionalInfo\x18\x05 \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\"%\n\x13ListDomainsResponse\x12\x0e\n\x06\x64omain\x18\x01 \x03(\t\"d\n\x14ReserveDomainRequest\x12<\n\x13reserveDomainAction\x18\x01 \x01(\x0e\x32\x1f.Enterprise.ReserveDomainAction\x12\x0e\n\x06\x64omain\x18\x02 \x01(\t\"&\n\x15ReserveDomainResponse\x12\r\n\x05token\x18\x01 \x01(\t\".\n\x0bRolesByTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0e\n\x06roleId\x18\x02 \x03(\x03\"\x8d\x01\n\x10LockUsersRequest\x12\x1d\n\x15lockEnterpriseUserIds\x18\x01 \x03(\x03\x12 \n\x18\x64isableEnterpriseUserIds\x18\x02 \x03(\x03\x12\x1f\n\x17unlockEnterpriseUserIds\x18\x03 \x03(\x03\x12\x17\n\x0f\x64\x65leteIfPending\x18\x04 \x01(\x08\"C\n\x11LockUsersResponse\x12.\n\x08response\x18\x01 \x03(\x0b\x32\x1c.Enterprise.LockUserResponse\"n\n\x10LockUserResponse\x12\x18\n\x10\x65nterpriseUserId\x18\x01 \x01(\x03\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.Enterprise.UserLockStatus\x12\x14\n\x0c\x65rrorMessage\x18\x03 \x01(\t*\x1b\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x07\n\x03\x45\x43\x43\x10\x01*\x9a\x02\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\x12\x13\n\x0fINVALID_ROLE_ID\x10\x08\x12\x1d\n\x19PAM_LICENSE_SEAT_EXCEEDED\x10\t*=\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*\xe1\x03\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*\"\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*\xb7\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\x12\x15\n\x11\x46ORBID_KEY_TYPE_1\x10\x0b\x12\x10\n\x0cKEEPER_DRIVE\x10\x0c*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*J\n\x13ReserveDomainAction\x12\x10\n\x0c\x44OMAIN_TOKEN\x10\x00\x12\x0e\n\nDOMAIN_ADD\x10\x01\x12\x11\n\rDOMAIN_DELETE\x10\x02*s\n\x0eUserLockStatus\x12\x17\n\x13UNKNOWN_LOCK_STATUS\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x0c\n\x08\x44ISABLED\x10\x02\x12\x0c\n\x08UNLOCKED\x10\x03\x12\x0b\n\x07\x44\x45LETED\x10\x04\x12\x13\n\x0f\x43\x41NT_BE_PENDING\x10\x05*\x80\x01\n\x1d\x45xternalCloudSecretsStoreType\x12\x16\n\x12UNKNOWN_STORE_TYPE\x10\x00\x12\x17\n\x13\x41WS_SECRETS_MANAGER\x10\x01\x12\x13\n\x0f\x41ZURE_KEY_VAULT\x10\x02\x12\x19\n\x15GOOGLE_SECRET_MANAGER\x10\x03\x42&\n\x18\x63om.keepersecurity.protoB\nEnterpriseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -38,40 +38,44 @@ _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=20402 - _globals['_KEYTYPE']._serialized_end=20429 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=20432 - _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=20714 - _globals['_ENTERPRISETYPE']._serialized_start=20716 - _globals['_ENTERPRISETYPE']._serialized_end=20777 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=20779 - _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=20894 - _globals['_ENTERPRISEDATAENTITY']._serialized_start=20897 - _globals['_ENTERPRISEDATAENTITY']._serialized_end=21378 - _globals['_CACHESTATUS']._serialized_start=21380 - _globals['_CACHESTATUS']._serialized_end=21414 - _globals['_BACKUPKEYTYPE']._serialized_start=21417 - _globals['_BACKUPKEYTYPE']._serialized_end=21564 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=21566 - _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=21624 - _globals['_ENCRYPTEDKEYTYPE']._serialized_start=21627 - _globals['_ENCRYPTEDKEYTYPE']._serialized_end=21792 - _globals['_ENTERPRISEFLAGTYPE']._serialized_start=21795 - _globals['_ENTERPRISEFLAGTYPE']._serialized_end=22088 - _globals['_USERUPDATESTATUS']._serialized_start=22090 - _globals['_USERUPDATESTATUS']._serialized_end=22159 - _globals['_AUDITUSERSTATUS']._serialized_start=22161 - _globals['_AUDITUSERSTATUS']._serialized_end=22234 - _globals['_TEAMUSERTYPE']._serialized_start=22236 - _globals['_TEAMUSERTYPE']._serialized_end=22287 - _globals['_APPCLIENTTYPE']._serialized_start=22289 - _globals['_APPCLIENTTYPE']._serialized_end=22409 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=22412 - _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=22555 - _globals['_CLEARSECURITYDATATYPE']._serialized_start=22558 - _globals['_CLEARSECURITYDATATYPE']._serialized_end=22693 - _globals['_USERLOCKSTATUS']._serialized_start=22695 - _globals['_USERLOCKSTATUS']._serialized_end=22810 + _globals['_KEYTYPE']._serialized_start=20649 + _globals['_KEYTYPE']._serialized_end=20676 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_start=20679 + _globals['_ROLEUSERMODIFYSTATUS']._serialized_end=20961 + _globals['_ENTERPRISETYPE']._serialized_start=20963 + _globals['_ENTERPRISETYPE']._serialized_end=21024 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_start=21026 + _globals['_TRANSFERACCEPTANCESTATUS']._serialized_end=21141 + _globals['_ENTERPRISEDATAENTITY']._serialized_start=21144 + _globals['_ENTERPRISEDATAENTITY']._serialized_end=21625 + _globals['_CACHESTATUS']._serialized_start=21627 + _globals['_CACHESTATUS']._serialized_end=21661 + _globals['_BACKUPKEYTYPE']._serialized_start=21664 + _globals['_BACKUPKEYTYPE']._serialized_end=21811 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_start=21813 + _globals['_BACKUPUSERDATAKEYTYPE']._serialized_end=21871 + _globals['_ENCRYPTEDKEYTYPE']._serialized_start=21874 + _globals['_ENCRYPTEDKEYTYPE']._serialized_end=22039 + _globals['_ENTERPRISEFLAGTYPE']._serialized_start=22042 + _globals['_ENTERPRISEFLAGTYPE']._serialized_end=22353 + _globals['_USERUPDATESTATUS']._serialized_start=22355 + _globals['_USERUPDATESTATUS']._serialized_end=22424 + _globals['_AUDITUSERSTATUS']._serialized_start=22426 + _globals['_AUDITUSERSTATUS']._serialized_end=22499 + _globals['_TEAMUSERTYPE']._serialized_start=22501 + _globals['_TEAMUSERTYPE']._serialized_end=22552 + _globals['_APPCLIENTTYPE']._serialized_start=22554 + _globals['_APPCLIENTTYPE']._serialized_end=22674 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_start=22677 + _globals['_DELETEENTERPRISEUSERSRESULT']._serialized_end=22820 + _globals['_CLEARSECURITYDATATYPE']._serialized_start=22823 + _globals['_CLEARSECURITYDATATYPE']._serialized_end=22958 + _globals['_RESERVEDOMAINACTION']._serialized_start=22960 + _globals['_RESERVEDOMAINACTION']._serialized_end=23034 + _globals['_USERLOCKSTATUS']._serialized_start=23036 + _globals['_USERLOCKSTATUS']._serialized_end=23151 + _globals['_EXTERNALCLOUDSECRETSSTORETYPE']._serialized_start=23154 + _globals['_EXTERNALCLOUDSECRETSSTORETYPE']._serialized_end=23282 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_start=33 _globals['_ENTERPRISEKEYPAIRREQUEST']._serialized_end=165 _globals['_GETTEAMMEMBERREQUEST']._serialized_start=167 @@ -129,275 +133,281 @@ _globals['_LOGINTOMCREQUEST']._serialized_start=2873 _globals['_LOGINTOMCREQUEST']._serialized_end=2942 _globals['_LOGINTOMCRESPONSE']._serialized_start=2944 - _globals['_LOGINTOMCRESPONSE']._serialized_end=3044 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=3046 - _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=3149 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=3152 - _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3288 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3290 - _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3406 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3408 - _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3497 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3499 - _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3591 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3594 - _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3729 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3731 - _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3804 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3806 - _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3909 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3911 - _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=4011 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=4013 - _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=4107 - _globals['_ENTERPRISEDATAREQUEST']._serialized_start=4109 - _globals['_ENTERPRISEDATAREQUEST']._serialized_end=4159 - _globals['_SPECIALPROVISIONING']._serialized_start=4161 - _globals['_SPECIALPROVISIONING']._serialized_end=4209 - _globals['_GENERALDATAENTITY']._serialized_start=4212 - _globals['_GENERALDATAENTITY']._serialized_end=4472 - _globals['_NODE']._serialized_start=4475 - _globals['_NODE']._serialized_end=4728 - _globals['_ROLE']._serialized_start=4731 - _globals['_ROLE']._serialized_end=4873 - _globals['_USER']._serialized_start=4876 - _globals['_USER']._serialized_end=5188 - _globals['_USERALIAS']._serialized_start=5190 - _globals['_USERALIAS']._serialized_end=5245 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5248 - _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5420 - _globals['_MANAGEDNODE']._serialized_start=5422 - _globals['_MANAGEDNODE']._serialized_end=5505 - _globals['_USERMANAGEDNODE']._serialized_start=5507 - _globals['_USERMANAGEDNODE']._serialized_end=5591 - _globals['_USERPRIVILEGE']._serialized_start=5593 - _globals['_USERPRIVILEGE']._serialized_end=5712 - _globals['_ROLEUSER']._serialized_start=5714 - _globals['_ROLEUSER']._serialized_end=5766 - _globals['_ROLEPRIVILEGE']._serialized_start=5768 - _globals['_ROLEPRIVILEGE']._serialized_end=5845 - _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_start=5847 - _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_end=5931 - _globals['_ROLEENFORCEMENT']._serialized_start=5933 - _globals['_ROLEENFORCEMENT']._serialized_end=6006 - _globals['_TEAM']._serialized_start=6009 - _globals['_TEAM']._serialized_end=6178 - _globals['_TEAMUSER']._serialized_start=6180 - _globals['_TEAMUSER']._serialized_end=6251 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6253 - _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6328 - _globals['_DISTRIBUTOR']._serialized_start=6330 - _globals['_DISTRIBUTOR']._serialized_end=6396 - _globals['_MSPINFO']._serialized_start=6399 - _globals['_MSPINFO']._serialized_end=6684 - _globals['_MANAGEDCOMPANY']._serialized_start=6687 - _globals['_MANAGEDCOMPANY']._serialized_end=6960 - _globals['_MSPPOOL']._serialized_start=6962 - _globals['_MSPPOOL']._serialized_end=7044 - _globals['_MSPCONTACT']._serialized_start=7046 - _globals['_MSPCONTACT']._serialized_end=7104 - _globals['_LICENSEADDON']._serialized_start=7107 - _globals['_LICENSEADDON']._serialized_end=7343 - _globals['_MCDEFAULT']._serialized_start=7345 - _globals['_MCDEFAULT']._serialized_end=7460 - _globals['_MSPPERMITS']._serialized_start=7463 - _globals['_MSPPERMITS']._serialized_end=7673 - _globals['_LICENSE']._serialized_start=7676 - _globals['_LICENSE']._serialized_end=8220 - _globals['_BRIDGE']._serialized_start=8222 - _globals['_BRIDGE']._serialized_end=8332 - _globals['_SCIM']._serialized_start=8334 - _globals['_SCIM']._serialized_end=8450 - _globals['_EMAILPROVISION']._serialized_start=8452 - _globals['_EMAILPROVISION']._serialized_end=8528 - _globals['_QUEUEDTEAM']._serialized_start=8530 - _globals['_QUEUEDTEAM']._serialized_end=8612 - _globals['_QUEUEDTEAMUSER']._serialized_start=8614 - _globals['_QUEUEDTEAMUSER']._serialized_end=8662 - _globals['_TEAMSADDRESULT']._serialized_start=8665 - _globals['_TEAMSADDRESULT']._serialized_end=8829 - _globals['_TEAMADDRESULT']._serialized_start=8831 - _globals['_TEAMADDRESULT']._serialized_end=8916 - _globals['_SSOSERVICE']._serialized_start=8919 - _globals['_SSOSERVICE']._serialized_end=9064 - _globals['_REPORTFILTERUSER']._serialized_start=9066 - _globals['_REPORTFILTERUSER']._serialized_end=9115 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=9118 - _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9397 - _globals['_ENTERPRISEDATA']._serialized_start=9399 - _globals['_ENTERPRISEDATA']._serialized_end=9495 - _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9498 - _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9706 - _globals['_BACKUPREQUEST']._serialized_start=9708 - _globals['_BACKUPREQUEST']._serialized_end=9750 - _globals['_BACKUPRECORD']._serialized_start=9753 - _globals['_BACKUPRECORD']._serialized_end=9905 - _globals['_BACKUPKEY']._serialized_start=9907 - _globals['_BACKUPKEY']._serialized_end=9953 - _globals['_BACKUPUSER']._serialized_start=9956 - _globals['_BACKUPUSER']._serialized_end=10225 - _globals['_BACKUPRESPONSE']._serialized_start=10228 - _globals['_BACKUPRESPONSE']._serialized_end=10386 - _globals['_BACKUPFILE']._serialized_start=10388 - _globals['_BACKUPFILE']._serialized_end=10489 - _globals['_BACKUPSRESPONSE']._serialized_start=10491 - _globals['_BACKUPSRESPONSE']._serialized_end=10547 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10549 - _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10595 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10598 - _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10853 - _globals['_ROLEKEY']._serialized_start=10855 - _globals['_ROLEKEY']._serialized_end=10949 - _globals['_MSPKEY']._serialized_start=10951 - _globals['_MSPKEY']._serialized_end=11051 - _globals['_ENTERPRISEKEYS']._serialized_start=11053 - _globals['_ENTERPRISEKEYS']._serialized_end=11177 - _globals['_TREEKEY']._serialized_start=11179 - _globals['_TREEKEY']._serialized_end=11251 - _globals['_SHAREDRECORDRESPONSE']._serialized_start=11253 - _globals['_SHAREDRECORDRESPONSE']._serialized_end=11322 - _globals['_SHAREDRECORDEVENT']._serialized_start=11324 - _globals['_SHAREDRECORDEVENT']._serialized_end=11436 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11438 - _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11484 - _globals['_USERADDREQUEST']._serialized_start=11487 - _globals['_USERADDREQUEST']._serialized_end=11695 - _globals['_USERUPDATEREQUEST']._serialized_start=11697 - _globals['_USERUPDATEREQUEST']._serialized_end=11755 - _globals['_USERUPDATE']._serialized_start=11758 - _globals['_USERUPDATE']._serialized_end=11933 - _globals['_USERUPDATERESPONSE']._serialized_start=11935 - _globals['_USERUPDATERESPONSE']._serialized_end=12000 - _globals['_USERUPDATERESULT']._serialized_start=12002 - _globals['_USERUPDATERESULT']._serialized_end=12092 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=12094 - _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=12168 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=12170 - _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12249 - _globals['_RECORDOWNER']._serialized_start=12251 - _globals['_RECORDOWNER']._serialized_end=12306 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12309 - _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12475 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12478 - _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12637 - _globals['_AUDITUSERRECORD']._serialized_start=12639 - _globals['_AUDITUSERRECORD']._serialized_end=12714 - _globals['_AUDITUSERDATA']._serialized_start=12717 - _globals['_AUDITUSERDATA']._serialized_end=12858 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12860 - _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=12987 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=12989 - _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=13116 - _globals['_COMPLIANCEREPORTRUN']._serialized_start=13119 - _globals['_COMPLIANCEREPORTRUN']._serialized_end=13252 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13255 - _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13507 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13509 - _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13607 - _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13609 - _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13729 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13732 - _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14405 - _globals['_AUDITRECORD']._serialized_start=14408 - _globals['_AUDITRECORD']._serialized_end=14537 - _globals['_AUDITROLE']._serialized_start=14540 - _globals['_AUDITROLE']._serialized_end=14796 - _globals['_ROLENODEMANAGEMENT']._serialized_start=14798 - _globals['_ROLENODEMANAGEMENT']._serialized_end=14892 - _globals['_USERPROFILE']._serialized_start=14894 - _globals['_USERPROFILE']._serialized_end=15001 - _globals['_RECORDPERMISSION']._serialized_start=15003 - _globals['_RECORDPERMISSION']._serialized_end=15064 - _globals['_USERRECORD']._serialized_start=15066 - _globals['_USERRECORD']._serialized_end=15161 - _globals['_AUDITTEAM']._serialized_start=15163 - _globals['_AUDITTEAM']._serialized_end=15254 - _globals['_AUDITTEAMUSER']._serialized_start=15256 - _globals['_AUDITTEAMUSER']._serialized_end=15315 - _globals['_SHAREDFOLDERRECORD']._serialized_start=15318 - _globals['_SHAREDFOLDERRECORD']._serialized_end=15477 - _globals['_SHAREADMINRECORD']._serialized_start=15479 - _globals['_SHAREADMINRECORD']._serialized_end=15556 - _globals['_SHAREDFOLDERUSER']._serialized_start=15558 - _globals['_SHAREDFOLDERUSER']._serialized_end=15628 - _globals['_SHAREDFOLDERTEAM']._serialized_start=15630 - _globals['_SHAREDFOLDERTEAM']._serialized_end=15691 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15693 - _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15740 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15742 - _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15792 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15794 - _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15848 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15850 - _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15909 - _globals['_LINKEDRECORD']._serialized_start=15911 - _globals['_LINKEDRECORD']._serialized_end=15963 - _globals['_GETSHARINGADMINSREQUEST']._serialized_start=15965 - _globals['_GETSHARINGADMINSREQUEST']._serialized_end=16052 - _globals['_USERPROFILEEXT']._serialized_start=16055 - _globals['_USERPROFILEEXT']._serialized_end=16279 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16281 - _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16360 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16362 - _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16457 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16459 - _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16575 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16578 - _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16749 - _globals['_TYPEDKEY']._serialized_start=16751 - _globals['_TYPEDKEY']._serialized_end=16821 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16823 - _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=16938 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=16941 - _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=17137 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=17140 - _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17299 - _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_start=17301 - _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_end=17370 - _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_start=17372 - _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_end=17478 - _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_start=17480 - _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_end=17603 - _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_start=17606 - _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_end=17790 - _globals['_DOMAINALIAS']._serialized_start=17792 - _globals['_DOMAINALIAS']._serialized_end=17869 - _globals['_DOMAINALIASREQUEST']._serialized_start=17871 - _globals['_DOMAINALIASREQUEST']._serialized_end=17937 - _globals['_DOMAINALIASRESPONSE']._serialized_start=17939 - _globals['_DOMAINALIASRESPONSE']._serialized_end=18006 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=18008 - _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=18117 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=18120 - _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=18558 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=18560 - _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=18655 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=18657 - _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18770 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18772 - _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18869 - _globals['_ENTERPRISEUSERSADD']._serialized_start=18872 - _globals['_ENTERPRISEUSERSADD']._serialized_end=19140 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=19143 - _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=19298 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=19301 - _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=19451 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=19454 - _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=19639 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=19641 - _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=19698 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=19700 - _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19811 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19813 - _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19906 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19908 - _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=20027 - _globals['_ROLESBYTEAM']._serialized_start=20029 - _globals['_ROLESBYTEAM']._serialized_end=20075 - _globals['_LOCKUSERSREQUEST']._serialized_start=20078 - _globals['_LOCKUSERSREQUEST']._serialized_end=20219 - _globals['_LOCKUSERSRESPONSE']._serialized_start=20221 - _globals['_LOCKUSERSRESPONSE']._serialized_end=20288 - _globals['_LOCKUSERRESPONSE']._serialized_start=20290 - _globals['_LOCKUSERRESPONSE']._serialized_end=20400 + _globals['_LOGINTOMCRESPONSE']._serialized_end=3063 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_start=3065 + _globals['_DOMAINPASSWORDRULESRESPONSE']._serialized_end=3168 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_start=3171 + _globals['_APPROVEUSERDEVICEREQUEST']._serialized_end=3307 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_start=3309 + _globals['_APPROVEUSERDEVICERESPONSE']._serialized_end=3425 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_start=3427 + _globals['_APPROVEUSERDEVICESREQUEST']._serialized_end=3516 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_start=3518 + _globals['_APPROVEUSERDEVICESRESPONSE']._serialized_end=3610 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_start=3613 + _globals['_ENTERPRISEUSERDATAKEY']._serialized_end=3748 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_start=3750 + _globals['_ENTERPRISEUSERDATAKEYS']._serialized_end=3823 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_start=3825 + _globals['_ENTERPRISEUSERDATAKEYLIGHT']._serialized_end=3928 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_start=3930 + _globals['_ENTERPRISEUSERDATAKEYSBYNODE']._serialized_end=4030 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_start=4032 + _globals['_ENTERPRISEUSERDATAKEYSBYNODERESPONSE']._serialized_end=4126 + _globals['_ENTERPRISEDATAREQUEST']._serialized_start=4128 + _globals['_ENTERPRISEDATAREQUEST']._serialized_end=4178 + _globals['_SPECIALPROVISIONING']._serialized_start=4180 + _globals['_SPECIALPROVISIONING']._serialized_end=4228 + _globals['_GENERALDATAENTITY']._serialized_start=4231 + _globals['_GENERALDATAENTITY']._serialized_end=4491 + _globals['_NODE']._serialized_start=4494 + _globals['_NODE']._serialized_end=4747 + _globals['_ROLE']._serialized_start=4750 + _globals['_ROLE']._serialized_end=4892 + _globals['_USER']._serialized_start=4895 + _globals['_USER']._serialized_end=5207 + _globals['_USERALIAS']._serialized_start=5209 + _globals['_USERALIAS']._serialized_end=5264 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_start=5267 + _globals['_COMPLIANCEREPORTMETADATA']._serialized_end=5439 + _globals['_MANAGEDNODE']._serialized_start=5441 + _globals['_MANAGEDNODE']._serialized_end=5524 + _globals['_USERMANAGEDNODE']._serialized_start=5526 + _globals['_USERMANAGEDNODE']._serialized_end=5610 + _globals['_USERPRIVILEGE']._serialized_start=5612 + _globals['_USERPRIVILEGE']._serialized_end=5731 + _globals['_ROLEUSER']._serialized_start=5733 + _globals['_ROLEUSER']._serialized_end=5785 + _globals['_ROLEPRIVILEGE']._serialized_start=5787 + _globals['_ROLEPRIVILEGE']._serialized_end=5864 + _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_start=5866 + _globals['_PRIVILEGESBYMANAGEDNODE']._serialized_end=5950 + _globals['_ROLEENFORCEMENT']._serialized_start=5952 + _globals['_ROLEENFORCEMENT']._serialized_end=6025 + _globals['_TEAM']._serialized_start=6028 + _globals['_TEAM']._serialized_end=6197 + _globals['_TEAMUSER']._serialized_start=6199 + _globals['_TEAMUSER']._serialized_end=6270 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_start=6272 + _globals['_GETDISTRIBUTORINFORESPONSE']._serialized_end=6347 + _globals['_DISTRIBUTOR']._serialized_start=6349 + _globals['_DISTRIBUTOR']._serialized_end=6415 + _globals['_MSPINFO']._serialized_start=6418 + _globals['_MSPINFO']._serialized_end=6703 + _globals['_MANAGEDCOMPANY']._serialized_start=6706 + _globals['_MANAGEDCOMPANY']._serialized_end=7002 + _globals['_MSPPOOL']._serialized_start=7004 + _globals['_MSPPOOL']._serialized_end=7086 + _globals['_MSPCONTACT']._serialized_start=7088 + _globals['_MSPCONTACT']._serialized_end=7146 + _globals['_LICENSEADDON']._serialized_start=7149 + _globals['_LICENSEADDON']._serialized_end=7409 + _globals['_MCDEFAULT']._serialized_start=7411 + _globals['_MCDEFAULT']._serialized_end=7526 + _globals['_MSPPERMITS']._serialized_start=7529 + _globals['_MSPPERMITS']._serialized_end=7739 + _globals['_LICENSE']._serialized_start=7742 + _globals['_LICENSE']._serialized_end=8286 + _globals['_BRIDGE']._serialized_start=8288 + _globals['_BRIDGE']._serialized_end=8398 + _globals['_SCIM']._serialized_start=8400 + _globals['_SCIM']._serialized_end=8516 + _globals['_EMAILPROVISION']._serialized_start=8518 + _globals['_EMAILPROVISION']._serialized_end=8594 + _globals['_QUEUEDTEAM']._serialized_start=8596 + _globals['_QUEUEDTEAM']._serialized_end=8678 + _globals['_QUEUEDTEAMUSER']._serialized_start=8680 + _globals['_QUEUEDTEAMUSER']._serialized_end=8728 + _globals['_TEAMSADDRESULT']._serialized_start=8731 + _globals['_TEAMSADDRESULT']._serialized_end=8895 + _globals['_TEAMADDRESULT']._serialized_start=8897 + _globals['_TEAMADDRESULT']._serialized_end=8982 + _globals['_SSOSERVICE']._serialized_start=8985 + _globals['_SSOSERVICE']._serialized_end=9130 + _globals['_REPORTFILTERUSER']._serialized_start=9132 + _globals['_REPORTFILTERUSER']._serialized_end=9181 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_start=9184 + _globals['_DEVICEREQUESTFORADMINAPPROVAL']._serialized_end=9463 + _globals['_ENTERPRISEDATA']._serialized_start=9465 + _globals['_ENTERPRISEDATA']._serialized_end=9561 + _globals['_ENTERPRISEDATARESPONSE']._serialized_start=9564 + _globals['_ENTERPRISEDATARESPONSE']._serialized_end=9772 + _globals['_BACKUPREQUEST']._serialized_start=9774 + _globals['_BACKUPREQUEST']._serialized_end=9816 + _globals['_BACKUPRECORD']._serialized_start=9819 + _globals['_BACKUPRECORD']._serialized_end=9971 + _globals['_BACKUPKEY']._serialized_start=9973 + _globals['_BACKUPKEY']._serialized_end=10019 + _globals['_BACKUPUSER']._serialized_start=10022 + _globals['_BACKUPUSER']._serialized_end=10291 + _globals['_BACKUPRESPONSE']._serialized_start=10294 + _globals['_BACKUPRESPONSE']._serialized_end=10452 + _globals['_BACKUPFILE']._serialized_start=10454 + _globals['_BACKUPFILE']._serialized_end=10555 + _globals['_BACKUPSRESPONSE']._serialized_start=10557 + _globals['_BACKUPSRESPONSE']._serialized_end=10613 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_start=10615 + _globals['_GETENTERPRISEDATAKEYSREQUEST']._serialized_end=10661 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_start=10664 + _globals['_GETENTERPRISEDATAKEYSRESPONSE']._serialized_end=10919 + _globals['_ROLEKEY']._serialized_start=10921 + _globals['_ROLEKEY']._serialized_end=11015 + _globals['_MSPKEY']._serialized_start=11017 + _globals['_MSPKEY']._serialized_end=11117 + _globals['_ENTERPRISEKEYS']._serialized_start=11119 + _globals['_ENTERPRISEKEYS']._serialized_end=11243 + _globals['_TREEKEY']._serialized_start=11245 + _globals['_TREEKEY']._serialized_end=11317 + _globals['_SHAREDRECORDRESPONSE']._serialized_start=11319 + _globals['_SHAREDRECORDRESPONSE']._serialized_end=11388 + _globals['_SHAREDRECORDEVENT']._serialized_start=11390 + _globals['_SHAREDRECORDEVENT']._serialized_end=11502 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_start=11504 + _globals['_SETRESTRICTVISIBILITYREQUEST']._serialized_end=11550 + _globals['_USERADDREQUEST']._serialized_start=11553 + _globals['_USERADDREQUEST']._serialized_end=11761 + _globals['_USERUPDATEREQUEST']._serialized_start=11763 + _globals['_USERUPDATEREQUEST']._serialized_end=11821 + _globals['_USERUPDATE']._serialized_start=11824 + _globals['_USERUPDATE']._serialized_end=11999 + _globals['_USERUPDATERESPONSE']._serialized_start=12001 + _globals['_USERUPDATERESPONSE']._serialized_end=12066 + _globals['_USERUPDATERESULT']._serialized_start=12068 + _globals['_USERUPDATERESULT']._serialized_end=12158 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_start=12160 + _globals['_COMPLIANCERECORDOWNERSREQUEST']._serialized_end=12234 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_start=12236 + _globals['_COMPLIANCERECORDOWNERSRESPONSE']._serialized_end=12315 + _globals['_RECORDOWNER']._serialized_start=12317 + _globals['_RECORDOWNER']._serialized_end=12372 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_start=12375 + _globals['_PRELIMINARYCOMPLIANCEDATAREQUEST']._serialized_end=12541 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_start=12544 + _globals['_PRELIMINARYCOMPLIANCEDATARESPONSE']._serialized_end=12703 + _globals['_AUDITUSERRECORD']._serialized_start=12705 + _globals['_AUDITUSERRECORD']._serialized_end=12780 + _globals['_AUDITUSERDATA']._serialized_start=12783 + _globals['_AUDITUSERDATA']._serialized_end=12924 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_start=12926 + _globals['_COMPLIANCEREPORTFILTERS']._serialized_end=13053 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_start=13055 + _globals['_COMPLIANCEREPORTREQUEST']._serialized_end=13182 + _globals['_COMPLIANCEREPORTRUN']._serialized_start=13185 + _globals['_COMPLIANCEREPORTRUN']._serialized_end=13318 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_start=13321 + _globals['_COMPLIANCEREPORTCRITERIAANDFILTER']._serialized_end=13573 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_start=13575 + _globals['_COMPLIANCEREPORTCRITERIA']._serialized_end=13673 + _globals['_COMPLIANCEREPORTFILTER']._serialized_start=13675 + _globals['_COMPLIANCEREPORTFILTER']._serialized_end=13795 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_start=13798 + _globals['_COMPLIANCEREPORTRESPONSE']._serialized_end=14471 + _globals['_AUDITRECORD']._serialized_start=14474 + _globals['_AUDITRECORD']._serialized_end=14603 + _globals['_AUDITROLE']._serialized_start=14606 + _globals['_AUDITROLE']._serialized_end=14862 + _globals['_ROLENODEMANAGEMENT']._serialized_start=14864 + _globals['_ROLENODEMANAGEMENT']._serialized_end=14958 + _globals['_USERPROFILE']._serialized_start=14960 + _globals['_USERPROFILE']._serialized_end=15067 + _globals['_RECORDPERMISSION']._serialized_start=15069 + _globals['_RECORDPERMISSION']._serialized_end=15130 + _globals['_USERRECORD']._serialized_start=15132 + _globals['_USERRECORD']._serialized_end=15227 + _globals['_AUDITTEAM']._serialized_start=15229 + _globals['_AUDITTEAM']._serialized_end=15320 + _globals['_AUDITTEAMUSER']._serialized_start=15322 + _globals['_AUDITTEAMUSER']._serialized_end=15381 + _globals['_SHAREDFOLDERRECORD']._serialized_start=15384 + _globals['_SHAREDFOLDERRECORD']._serialized_end=15543 + _globals['_SHAREADMINRECORD']._serialized_start=15545 + _globals['_SHAREADMINRECORD']._serialized_end=15622 + _globals['_SHAREDFOLDERUSER']._serialized_start=15624 + _globals['_SHAREDFOLDERUSER']._serialized_end=15694 + _globals['_SHAREDFOLDERTEAM']._serialized_start=15696 + _globals['_SHAREDFOLDERTEAM']._serialized_end=15757 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_start=15759 + _globals['_GETCOMPLIANCEREPORTREQUEST']._serialized_end=15806 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_start=15808 + _globals['_GETCOMPLIANCEREPORTRESPONSE']._serialized_end=15858 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_start=15860 + _globals['_COMPLIANCEREPORTCRITERIAREQUEST']._serialized_end=15914 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_start=15916 + _globals['_SAVECOMPLIANCEREPORTCRITERIARESPONSE']._serialized_end=15975 + _globals['_LINKEDRECORD']._serialized_start=15977 + _globals['_LINKEDRECORD']._serialized_end=16029 + _globals['_GETSHARINGADMINSREQUEST']._serialized_start=16031 + _globals['_GETSHARINGADMINSREQUEST']._serialized_end=16118 + _globals['_USERPROFILEEXT']._serialized_start=16121 + _globals['_USERPROFILEEXT']._serialized_end=16345 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_start=16347 + _globals['_GETSHARINGADMINSRESPONSE']._serialized_end=16426 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_start=16428 + _globals['_TEAMSENTERPRISEUSERSADDREQUEST']._serialized_end=16523 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_start=16525 + _globals['_TEAMSENTERPRISEUSERSADDTEAMREQUEST']._serialized_end=16641 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_start=16644 + _globals['_TEAMSENTERPRISEUSERSADDUSERREQUEST']._serialized_end=16815 + _globals['_TYPEDKEY']._serialized_start=16817 + _globals['_TYPEDKEY']._serialized_end=16887 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_start=16889 + _globals['_TEAMSENTERPRISEUSERSADDRESPONSE']._serialized_end=17004 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_start=17007 + _globals['_TEAMSENTERPRISEUSERSADDTEAMRESPONSE']._serialized_end=17203 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_start=17206 + _globals['_TEAMSENTERPRISEUSERSADDUSERRESPONSE']._serialized_end=17365 + _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_start=17367 + _globals['_TEAMENTERPRISEUSERREMOVE']._serialized_end=17436 + _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_start=17438 + _globals['_TEAMENTERPRISEUSERREMOVESREQUEST']._serialized_end=17544 + _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_start=17546 + _globals['_TEAMENTERPRISEUSERREMOVESRESPONSE']._serialized_end=17669 + _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_start=17672 + _globals['_TEAMENTERPRISEUSERREMOVERESPONSE']._serialized_end=17856 + _globals['_DOMAINALIAS']._serialized_start=17858 + _globals['_DOMAINALIAS']._serialized_end=17935 + _globals['_DOMAINALIASREQUEST']._serialized_start=17937 + _globals['_DOMAINALIASREQUEST']._serialized_end=18003 + _globals['_DOMAINALIASRESPONSE']._serialized_start=18005 + _globals['_DOMAINALIASRESPONSE']._serialized_end=18072 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_start=18074 + _globals['_ENTERPRISEUSERSPROVISIONREQUEST']._serialized_end=18183 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_start=18186 + _globals['_ENTERPRISEUSERSPROVISION']._serialized_end=18624 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_start=18626 + _globals['_ENTERPRISEUSERSPROVISIONRESPONSE']._serialized_end=18721 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_start=18723 + _globals['_ENTERPRISEUSERSPROVISIONRESULT']._serialized_end=18836 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_start=18838 + _globals['_ENTERPRISEUSERSADDREQUEST']._serialized_end=18935 + _globals['_ENTERPRISEUSERSADD']._serialized_start=18938 + _globals['_ENTERPRISEUSERSADD']._serialized_end=19206 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_start=19209 + _globals['_ENTERPRISEUSERSADDRESPONSE']._serialized_end=19364 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_start=19367 + _globals['_ENTERPRISEUSERSADDRESULT']._serialized_end=19517 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_start=19520 + _globals['_UPDATEMSPPERMITSREQUEST']._serialized_end=19705 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_start=19707 + _globals['_DELETEENTERPRISEUSERSREQUEST']._serialized_end=19764 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_start=19766 + _globals['_DELETEENTERPRISEUSERSTATUS']._serialized_end=19877 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_start=19879 + _globals['_DELETEENTERPRISEUSERSRESPONSE']._serialized_end=19972 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_start=19974 + _globals['_CLEARSECURITYDATAREQUEST']._serialized_end=20093 + _globals['_LISTDOMAINSRESPONSE']._serialized_start=20095 + _globals['_LISTDOMAINSRESPONSE']._serialized_end=20132 + _globals['_RESERVEDOMAINREQUEST']._serialized_start=20134 + _globals['_RESERVEDOMAINREQUEST']._serialized_end=20234 + _globals['_RESERVEDOMAINRESPONSE']._serialized_start=20236 + _globals['_RESERVEDOMAINRESPONSE']._serialized_end=20274 + _globals['_ROLESBYTEAM']._serialized_start=20276 + _globals['_ROLESBYTEAM']._serialized_end=20322 + _globals['_LOCKUSERSREQUEST']._serialized_start=20325 + _globals['_LOCKUSERSREQUEST']._serialized_end=20466 + _globals['_LOCKUSERSRESPONSE']._serialized_start=20468 + _globals['_LOCKUSERSRESPONSE']._serialized_end=20535 + _globals['_LOCKUSERRESPONSE']._serialized_start=20537 + _globals['_LOCKUSERRESPONSE']._serialized_end=20647 # @@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 55a62f59..2d693182 100644 --- a/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/enterprise_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -105,6 +104,7 @@ class EnterpriseFlagType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): NPS_POPUP_OPT_OUT: _ClassVar[EnterpriseFlagType] SHOW_USER_ONBOARD: _ClassVar[EnterpriseFlagType] FORBID_KEY_TYPE_1: _ClassVar[EnterpriseFlagType] + KEEPER_DRIVE: _ClassVar[EnterpriseFlagType] class UserUpdateStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -145,6 +145,12 @@ class ClearSecurityDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): FORCE_CLIENT_CHECK_FOR_MISSING_DATA: _ClassVar[ClearSecurityDataType] FORCE_CLIENT_RESEND_SECURITY_DATA: _ClassVar[ClearSecurityDataType] +class ReserveDomainAction(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DOMAIN_TOKEN: _ClassVar[ReserveDomainAction] + DOMAIN_ADD: _ClassVar[ReserveDomainAction] + DOMAIN_DELETE: _ClassVar[ReserveDomainAction] + class UserLockStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () UNKNOWN_LOCK_STATUS: _ClassVar[UserLockStatus] @@ -153,6 +159,13 @@ class UserLockStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): UNLOCKED: _ClassVar[UserLockStatus] DELETED: _ClassVar[UserLockStatus] CANT_BE_PENDING: _ClassVar[UserLockStatus] + +class ExternalCloudSecretsStoreType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UNKNOWN_STORE_TYPE: _ClassVar[ExternalCloudSecretsStoreType] + AWS_SECRETS_MANAGER: _ClassVar[ExternalCloudSecretsStoreType] + AZURE_KEY_VAULT: _ClassVar[ExternalCloudSecretsStoreType] + GOOGLE_SECRET_MANAGER: _ClassVar[ExternalCloudSecretsStoreType] RSA: KeyType ECC: KeyType ROLE_EXISTS: RoleUserModifyStatus @@ -222,6 +235,7 @@ FORBID_ACCOUNT_TRANSFER: EnterpriseFlagType NPS_POPUP_OPT_OUT: EnterpriseFlagType SHOW_USER_ONBOARD: EnterpriseFlagType FORBID_KEY_TYPE_1: EnterpriseFlagType +KEEPER_DRIVE: EnterpriseFlagType USER_UPDATE_OK: UserUpdateStatus USER_UPDATE_ACCESS_DENIED: UserUpdateStatus OK: AuditUserStatus @@ -243,12 +257,19 @@ ERROR: DeleteEnterpriseUsersResult RECALCULATE_SUMMARY_REPORT: ClearSecurityDataType FORCE_CLIENT_CHECK_FOR_MISSING_DATA: ClearSecurityDataType FORCE_CLIENT_RESEND_SECURITY_DATA: ClearSecurityDataType +DOMAIN_TOKEN: ReserveDomainAction +DOMAIN_ADD: ReserveDomainAction +DOMAIN_DELETE: ReserveDomainAction UNKNOWN_LOCK_STATUS: UserLockStatus LOCKED: UserLockStatus DISABLED: UserLockStatus UNLOCKED: UserLockStatus DELETED: UserLockStatus CANT_BE_PENDING: UserLockStatus +UNKNOWN_STORE_TYPE: ExternalCloudSecretsStoreType +AWS_SECRETS_MANAGER: ExternalCloudSecretsStoreType +AZURE_KEY_VAULT: ExternalCloudSecretsStoreType +GOOGLE_SECRET_MANAGER: ExternalCloudSecretsStoreType class EnterpriseKeyPairRequest(_message.Message): __slots__ = ("enterprisePublicKey", "encryptedEnterprisePrivateKey", "keyType") @@ -278,7 +299,7 @@ class EnterpriseUser(_message.Message): enterpriseUsername: str isShareAdmin: bool username: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., email: _Optional[str] = ..., enterpriseUsername: _Optional[str] = ..., isShareAdmin: _Optional[bool] = ..., username: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., email: _Optional[str] = ..., enterpriseUsername: _Optional[str] = ..., isShareAdmin: bool = ..., username: _Optional[str] = ...) -> None: ... class GetTeamMemberResponse(_message.Message): __slots__ = ("enterpriseUser",) @@ -308,7 +329,7 @@ class EncryptedTeamKeyRequest(_message.Message): teamUid: bytes encryptedTeamKey: bytes force: bool - def __init__(self, teamUid: _Optional[bytes] = ..., encryptedTeamKey: _Optional[bytes] = ..., force: _Optional[bool] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., encryptedTeamKey: _Optional[bytes] = ..., force: bool = ...) -> None: ... class ReEncryptedData(_message.Message): __slots__ = ("id", "data") @@ -510,7 +531,7 @@ class DomainPasswordRulesFields(_message.Message): minimum: int maximum: int allowed: bool - def __init__(self, type: _Optional[str] = ..., minimum: _Optional[int] = ..., maximum: _Optional[int] = ..., allowed: _Optional[bool] = ...) -> None: ... + def __init__(self, type: _Optional[str] = ..., minimum: _Optional[int] = ..., maximum: _Optional[int] = ..., allowed: bool = ...) -> None: ... class LoginToMcRequest(_message.Message): __slots__ = ("mcEnterpriseId", "messageSessionUid") @@ -521,14 +542,16 @@ class LoginToMcRequest(_message.Message): def __init__(self, mcEnterpriseId: _Optional[int] = ..., messageSessionUid: _Optional[bytes] = ...) -> None: ... class LoginToMcResponse(_message.Message): - __slots__ = ("encryptedSessionToken", "encryptedTreeKey", "forbidKeyType2") + __slots__ = ("encryptedSessionToken", "encryptedTreeKey", "keyTypeId", "forbidKeyType2") ENCRYPTEDSESSIONTOKEN_FIELD_NUMBER: _ClassVar[int] ENCRYPTEDTREEKEY_FIELD_NUMBER: _ClassVar[int] + KEYTYPEID_FIELD_NUMBER: _ClassVar[int] FORBIDKEYTYPE2_FIELD_NUMBER: _ClassVar[int] encryptedSessionToken: bytes encryptedTreeKey: str + keyTypeId: int forbidKeyType2: bool - def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., encryptedTreeKey: _Optional[str] = ..., forbidKeyType2: _Optional[bool] = ...) -> None: ... + def __init__(self, encryptedSessionToken: _Optional[bytes] = ..., encryptedTreeKey: _Optional[str] = ..., keyTypeId: _Optional[int] = ..., forbidKeyType2: bool = ...) -> None: ... class DomainPasswordRulesResponse(_message.Message): __slots__ = ("domainPasswordRulesFields",) @@ -546,7 +569,7 @@ class ApproveUserDeviceRequest(_message.Message): encryptedDeviceToken: bytes encryptedDeviceDataKey: bytes denyApproval: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., encryptedDeviceDataKey: _Optional[bytes] = ..., denyApproval: bool = ...) -> None: ... class ApproveUserDeviceResponse(_message.Message): __slots__ = ("enterpriseUserId", "encryptedDeviceToken", "failed", "message") @@ -558,7 +581,7 @@ class ApproveUserDeviceResponse(_message.Message): encryptedDeviceToken: bytes failed: bool message: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., failed: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., encryptedDeviceToken: _Optional[bytes] = ..., failed: bool = ..., message: _Optional[str] = ...) -> None: ... class ApproveUserDevicesRequest(_message.Message): __slots__ = ("deviceRequests",) @@ -646,7 +669,7 @@ class GeneralDataEntity(_message.Message): distributor: bool forbidAccountTransfer: bool showUserOnboard: bool - def __init__(self, enterpriseName: _Optional[str] = ..., restrictVisibility: _Optional[bool] = ..., specialProvisioning: _Optional[_Union[SpecialProvisioning, _Mapping]] = ..., userPrivilege: _Optional[_Union[UserPrivilege, _Mapping]] = ..., distributor: _Optional[bool] = ..., forbidAccountTransfer: _Optional[bool] = ..., showUserOnboard: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseName: _Optional[str] = ..., restrictVisibility: bool = ..., specialProvisioning: _Optional[_Union[SpecialProvisioning, _Mapping]] = ..., userPrivilege: _Optional[_Union[UserPrivilege, _Mapping]] = ..., distributor: bool = ..., forbidAccountTransfer: bool = ..., showUserOnboard: bool = ...) -> None: ... class Node(_message.Message): __slots__ = ("nodeId", "parentId", "bridgeId", "scimId", "licenseId", "encryptedData", "duoEnabled", "rsaEnabled", "ssoServiceProviderId", "restrictVisibility", "ssoServiceProviderIds") @@ -672,7 +695,7 @@ class Node(_message.Message): ssoServiceProviderId: int restrictVisibility: bool ssoServiceProviderIds: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, nodeId: _Optional[int] = ..., parentId: _Optional[int] = ..., bridgeId: _Optional[int] = ..., scimId: _Optional[int] = ..., licenseId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., duoEnabled: _Optional[bool] = ..., rsaEnabled: _Optional[bool] = ..., ssoServiceProviderId: _Optional[int] = ..., restrictVisibility: _Optional[bool] = ..., ssoServiceProviderIds: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., parentId: _Optional[int] = ..., bridgeId: _Optional[int] = ..., scimId: _Optional[int] = ..., licenseId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., duoEnabled: bool = ..., rsaEnabled: bool = ..., ssoServiceProviderId: _Optional[int] = ..., restrictVisibility: bool = ..., ssoServiceProviderIds: _Optional[_Iterable[int]] = ...) -> None: ... class Role(_message.Message): __slots__ = ("roleId", "nodeId", "encryptedData", "keyType", "visibleBelow", "newUserInherit", "roleType") @@ -690,7 +713,7 @@ class Role(_message.Message): visibleBelow: bool newUserInherit: bool roleType: str - def __init__(self, roleId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., visibleBelow: _Optional[bool] = ..., newUserInherit: _Optional[bool] = ..., roleType: _Optional[str] = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., visibleBelow: bool = ..., newUserInherit: bool = ..., roleType: _Optional[str] = ...) -> None: ... class User(_message.Message): __slots__ = ("enterpriseUserId", "nodeId", "encryptedData", "keyType", "username", "status", "lock", "userId", "accountShareExpiration", "fullName", "jobTitle", "tfaEnabled", "transferAcceptanceStatus") @@ -720,7 +743,7 @@ class User(_message.Message): jobTitle: str tfaEnabled: bool transferAcceptanceStatus: TransferAcceptanceStatus - def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., username: _Optional[str] = ..., status: _Optional[str] = ..., lock: _Optional[int] = ..., userId: _Optional[int] = ..., accountShareExpiration: _Optional[int] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., tfaEnabled: _Optional[bool] = ..., transferAcceptanceStatus: _Optional[_Union[TransferAcceptanceStatus, str]] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[str] = ..., username: _Optional[str] = ..., status: _Optional[str] = ..., lock: _Optional[int] = ..., userId: _Optional[int] = ..., accountShareExpiration: _Optional[int] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., tfaEnabled: bool = ..., transferAcceptanceStatus: _Optional[_Union[TransferAcceptanceStatus, str]] = ...) -> None: ... class UserAlias(_message.Message): __slots__ = ("enterpriseUserId", "username") @@ -756,7 +779,7 @@ class ManagedNode(_message.Message): roleId: int managedNodeId: int cascadeNodeManagement: bool - def __init__(self, roleId: _Optional[int] = ..., managedNodeId: _Optional[int] = ..., cascadeNodeManagement: _Optional[bool] = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., managedNodeId: _Optional[int] = ..., cascadeNodeManagement: bool = ...) -> None: ... class UserManagedNode(_message.Message): __slots__ = ("nodeId", "cascadeNodeManagement", "privileges") @@ -766,7 +789,7 @@ class UserManagedNode(_message.Message): nodeId: int cascadeNodeManagement: bool privileges: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, nodeId: _Optional[int] = ..., cascadeNodeManagement: _Optional[bool] = ..., privileges: _Optional[_Iterable[str]] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., cascadeNodeManagement: bool = ..., privileges: _Optional[_Iterable[str]] = ...) -> None: ... class UserPrivilege(_message.Message): __slots__ = ("userManagedNodes", "enterpriseUserId", "encryptedData") @@ -834,7 +857,7 @@ class Team(_message.Message): restrictView: bool encryptedData: str encryptedTeamKey: str - def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., nodeId: _Optional[int] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ..., restrictView: _Optional[bool] = ..., encryptedData: _Optional[str] = ..., encryptedTeamKey: _Optional[str] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., name: _Optional[str] = ..., nodeId: _Optional[int] = ..., restrictEdit: bool = ..., restrictShare: bool = ..., restrictView: bool = ..., encryptedData: _Optional[str] = ..., encryptedTeamKey: _Optional[str] = ...) -> None: ... class TeamUser(_message.Message): __slots__ = ("teamUid", "enterpriseUserId", "userType") @@ -880,10 +903,10 @@ class MspInfo(_message.Message): managedCompanies: _containers.RepeatedCompositeFieldContainer[ManagedCompany] allowUnlimitedLicenses: bool addOns: _containers.RepeatedCompositeFieldContainer[LicenseAddOn] - def __init__(self, enterpriseId: _Optional[int] = ..., enterpriseName: _Optional[str] = ..., allocatedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., managedCompanies: _Optional[_Iterable[_Union[ManagedCompany, _Mapping]]] = ..., allowUnlimitedLicenses: _Optional[bool] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... + def __init__(self, enterpriseId: _Optional[int] = ..., enterpriseName: _Optional[str] = ..., allocatedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., managedCompanies: _Optional[_Iterable[_Union[ManagedCompany, _Mapping]]] = ..., allowUnlimitedLicenses: bool = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... class ManagedCompany(_message.Message): - __slots__ = ("mcEnterpriseId", "mcEnterpriseName", "mspNodeId", "numberOfSeats", "numberOfUsers", "productId", "isExpired", "treeKey", "tree_key_role", "filePlanType", "addOns") + __slots__ = ("mcEnterpriseId", "mcEnterpriseName", "mspNodeId", "numberOfSeats", "numberOfUsers", "productId", "isExpired", "treeKey", "tree_key_role", "filePlanType", "addOns", "treeKeyTypeId") MCENTERPRISEID_FIELD_NUMBER: _ClassVar[int] MCENTERPRISENAME_FIELD_NUMBER: _ClassVar[int] MSPNODEID_FIELD_NUMBER: _ClassVar[int] @@ -895,6 +918,7 @@ class ManagedCompany(_message.Message): TREE_KEY_ROLE_FIELD_NUMBER: _ClassVar[int] FILEPLANTYPE_FIELD_NUMBER: _ClassVar[int] ADDONS_FIELD_NUMBER: _ClassVar[int] + TREEKEYTYPEID_FIELD_NUMBER: _ClassVar[int] mcEnterpriseId: int mcEnterpriseName: str mspNodeId: int @@ -906,7 +930,8 @@ class ManagedCompany(_message.Message): tree_key_role: int filePlanType: str addOns: _containers.RepeatedCompositeFieldContainer[LicenseAddOn] - def __init__(self, mcEnterpriseId: _Optional[int] = ..., mcEnterpriseName: _Optional[str] = ..., mspNodeId: _Optional[int] = ..., numberOfSeats: _Optional[int] = ..., numberOfUsers: _Optional[int] = ..., productId: _Optional[str] = ..., isExpired: _Optional[bool] = ..., treeKey: _Optional[str] = ..., tree_key_role: _Optional[int] = ..., filePlanType: _Optional[str] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ...) -> None: ... + treeKeyTypeId: int + def __init__(self, mcEnterpriseId: _Optional[int] = ..., mcEnterpriseName: _Optional[str] = ..., mspNodeId: _Optional[int] = ..., numberOfSeats: _Optional[int] = ..., numberOfUsers: _Optional[int] = ..., productId: _Optional[str] = ..., isExpired: bool = ..., treeKey: _Optional[str] = ..., tree_key_role: _Optional[int] = ..., filePlanType: _Optional[str] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ..., treeKeyTypeId: _Optional[int] = ...) -> None: ... class MSPPool(_message.Message): __slots__ = ("productId", "seats", "availableSeats", "stash") @@ -929,7 +954,7 @@ class MSPContact(_message.Message): def __init__(self, enterpriseId: _Optional[int] = ..., enterpriseName: _Optional[str] = ...) -> None: ... class LicenseAddOn(_message.Message): - __slots__ = ("name", "enabled", "isTrial", "expiration", "created", "seats", "activationTime", "includedInProduct", "apiCallCount", "tierDescription", "seatsAllocated") + __slots__ = ("name", "enabled", "isTrial", "expiration", "created", "seats", "activationTime", "includedInProduct", "apiCallCount", "tierDescription", "seatsAllocated", "nhiTierAddOnId") NAME_FIELD_NUMBER: _ClassVar[int] ENABLED_FIELD_NUMBER: _ClassVar[int] ISTRIAL_FIELD_NUMBER: _ClassVar[int] @@ -941,6 +966,7 @@ class LicenseAddOn(_message.Message): APICALLCOUNT_FIELD_NUMBER: _ClassVar[int] TIERDESCRIPTION_FIELD_NUMBER: _ClassVar[int] SEATSALLOCATED_FIELD_NUMBER: _ClassVar[int] + NHITIERADDONID_FIELD_NUMBER: _ClassVar[int] name: str enabled: bool isTrial: bool @@ -952,7 +978,8 @@ class LicenseAddOn(_message.Message): apiCallCount: int tierDescription: str seatsAllocated: int - def __init__(self, name: _Optional[str] = ..., enabled: _Optional[bool] = ..., isTrial: _Optional[bool] = ..., expiration: _Optional[int] = ..., created: _Optional[int] = ..., seats: _Optional[int] = ..., activationTime: _Optional[int] = ..., includedInProduct: _Optional[bool] = ..., apiCallCount: _Optional[int] = ..., tierDescription: _Optional[str] = ..., seatsAllocated: _Optional[int] = ...) -> None: ... + nhiTierAddOnId: int + def __init__(self, name: _Optional[str] = ..., enabled: bool = ..., isTrial: bool = ..., expiration: _Optional[int] = ..., created: _Optional[int] = ..., seats: _Optional[int] = ..., activationTime: _Optional[int] = ..., includedInProduct: bool = ..., apiCallCount: _Optional[int] = ..., tierDescription: _Optional[str] = ..., seatsAllocated: _Optional[int] = ..., nhiTierAddOnId: _Optional[int] = ...) -> None: ... class MCDefault(_message.Message): __slots__ = ("mcProduct", "addOns", "filePlanType", "maxLicenses", "fixedMaxLicenses") @@ -966,7 +993,7 @@ class MCDefault(_message.Message): filePlanType: str maxLicenses: int fixedMaxLicenses: bool - def __init__(self, mcProduct: _Optional[str] = ..., addOns: _Optional[_Iterable[str]] = ..., filePlanType: _Optional[str] = ..., maxLicenses: _Optional[int] = ..., fixedMaxLicenses: _Optional[bool] = ...) -> None: ... + def __init__(self, mcProduct: _Optional[str] = ..., addOns: _Optional[_Iterable[str]] = ..., filePlanType: _Optional[str] = ..., maxLicenses: _Optional[int] = ..., fixedMaxLicenses: bool = ...) -> None: ... class MSPPermits(_message.Message): __slots__ = ("restricted", "maxAllowedLicenses", "allowedMcProducts", "allowedAddOns", "maxFilePlanType", "allowUnlimitedLicenses", "mcDefaults") @@ -984,7 +1011,7 @@ class MSPPermits(_message.Message): maxFilePlanType: str allowUnlimitedLicenses: bool mcDefaults: _containers.RepeatedCompositeFieldContainer[MCDefault] - def __init__(self, restricted: _Optional[bool] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: _Optional[bool] = ..., mcDefaults: _Optional[_Iterable[_Union[MCDefault, _Mapping]]] = ...) -> None: ... + def __init__(self, restricted: bool = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: bool = ..., mcDefaults: _Optional[_Iterable[_Union[MCDefault, _Mapping]]] = ...) -> None: ... class License(_message.Message): __slots__ = ("paid", "numberOfSeats", "expiration", "licenseKeyId", "productTypeId", "name", "enterpriseLicenseId", "seatsAllocated", "seatsPending", "tier", "filePlanTypeId", "maxBytes", "storageExpiration", "licenseStatus", "mspPool", "managedBy", "addOns", "nextBillingDate", "hasMSPLegacyLog", "mspPermits", "distributor") @@ -1030,7 +1057,7 @@ class License(_message.Message): hasMSPLegacyLog: bool mspPermits: MSPPermits distributor: bool - def __init__(self, paid: _Optional[bool] = ..., numberOfSeats: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseKeyId: _Optional[int] = ..., productTypeId: _Optional[int] = ..., name: _Optional[str] = ..., enterpriseLicenseId: _Optional[int] = ..., seatsAllocated: _Optional[int] = ..., seatsPending: _Optional[int] = ..., tier: _Optional[int] = ..., filePlanTypeId: _Optional[int] = ..., maxBytes: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., licenseStatus: _Optional[str] = ..., mspPool: _Optional[_Iterable[_Union[MSPPool, _Mapping]]] = ..., managedBy: _Optional[_Union[MSPContact, _Mapping]] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ..., nextBillingDate: _Optional[int] = ..., hasMSPLegacyLog: _Optional[bool] = ..., mspPermits: _Optional[_Union[MSPPermits, _Mapping]] = ..., distributor: _Optional[bool] = ...) -> None: ... + def __init__(self, paid: bool = ..., numberOfSeats: _Optional[int] = ..., expiration: _Optional[int] = ..., licenseKeyId: _Optional[int] = ..., productTypeId: _Optional[int] = ..., name: _Optional[str] = ..., enterpriseLicenseId: _Optional[int] = ..., seatsAllocated: _Optional[int] = ..., seatsPending: _Optional[int] = ..., tier: _Optional[int] = ..., filePlanTypeId: _Optional[int] = ..., maxBytes: _Optional[int] = ..., storageExpiration: _Optional[int] = ..., licenseStatus: _Optional[str] = ..., mspPool: _Optional[_Iterable[_Union[MSPPool, _Mapping]]] = ..., managedBy: _Optional[_Union[MSPContact, _Mapping]] = ..., addOns: _Optional[_Iterable[_Union[LicenseAddOn, _Mapping]]] = ..., nextBillingDate: _Optional[int] = ..., hasMSPLegacyLog: bool = ..., mspPermits: _Optional[_Union[MSPPermits, _Mapping]] = ..., distributor: bool = ...) -> None: ... class Bridge(_message.Message): __slots__ = ("bridgeId", "nodeId", "wanIpEnforcement", "lanIpEnforcement", "status") @@ -1060,7 +1087,7 @@ class Scim(_message.Message): lastSynced: int rolePrefix: str uniqueGroups: bool - def __init__(self, scimId: _Optional[int] = ..., nodeId: _Optional[int] = ..., status: _Optional[str] = ..., lastSynced: _Optional[int] = ..., rolePrefix: _Optional[str] = ..., uniqueGroups: _Optional[bool] = ...) -> None: ... + def __init__(self, scimId: _Optional[int] = ..., nodeId: _Optional[int] = ..., status: _Optional[str] = ..., lastSynced: _Optional[int] = ..., rolePrefix: _Optional[str] = ..., uniqueGroups: bool = ...) -> None: ... class EmailProvision(_message.Message): __slots__ = ("id", "nodeId", "domain", "method") @@ -1132,7 +1159,7 @@ class SsoService(_message.Message): inviteNewUsers: bool active: bool isCloud: bool - def __init__(self, ssoServiceProviderId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., sp_url: _Optional[str] = ..., inviteNewUsers: _Optional[bool] = ..., active: _Optional[bool] = ..., isCloud: _Optional[bool] = ...) -> None: ... + def __init__(self, ssoServiceProviderId: _Optional[int] = ..., nodeId: _Optional[int] = ..., name: _Optional[str] = ..., sp_url: _Optional[str] = ..., inviteNewUsers: bool = ..., active: bool = ..., isCloud: bool = ...) -> None: ... class ReportFilterUser(_message.Message): __slots__ = ("userId", "email") @@ -1178,7 +1205,7 @@ class EnterpriseData(_message.Message): entity: EnterpriseDataEntity delete: bool data: _containers.RepeatedScalarFieldContainer[bytes] - def __init__(self, entity: _Optional[_Union[EnterpriseDataEntity, str]] = ..., delete: _Optional[bool] = ..., data: _Optional[_Iterable[bytes]] = ...) -> None: ... + def __init__(self, entity: _Optional[_Union[EnterpriseDataEntity, str]] = ..., delete: bool = ..., data: _Optional[_Iterable[bytes]] = ...) -> None: ... class EnterpriseDataResponse(_message.Message): __slots__ = ("continuationToken", "hasMore", "cacheStatus", "data", "generalData") @@ -1192,7 +1219,7 @@ class EnterpriseDataResponse(_message.Message): cacheStatus: CacheStatus data: _containers.RepeatedCompositeFieldContainer[EnterpriseData] generalData: GeneralDataEntity - def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., data: _Optional[_Iterable[_Union[EnterpriseData, _Mapping]]] = ..., generalData: _Optional[_Union[GeneralDataEntity, _Mapping]] = ...) -> None: ... + def __init__(self, continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., cacheStatus: _Optional[_Union[CacheStatus, str]] = ..., data: _Optional[_Iterable[_Union[EnterpriseData, _Mapping]]] = ..., generalData: _Optional[_Union[GeneralDataEntity, _Mapping]] = ...) -> None: ... class BackupRequest(_message.Message): __slots__ = ("continuationToken",) @@ -1356,7 +1383,7 @@ class SharedRecordEvent(_message.Message): canEdit: bool canReshare: bool shareFrom: int - def __init__(self, recordUid: _Optional[bytes] = ..., userName: _Optional[str] = ..., canEdit: _Optional[bool] = ..., canReshare: _Optional[bool] = ..., shareFrom: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., userName: _Optional[str] = ..., canEdit: bool = ..., canReshare: bool = ..., shareFrom: _Optional[int] = ...) -> None: ... class SetRestrictVisibilityRequest(_message.Message): __slots__ = ("nodeId",) @@ -1382,7 +1409,7 @@ class UserAddRequest(_message.Message): jobTitle: str email: str suppressEmailInvite: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., email: _Optional[str] = ..., suppressEmailInvite: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., email: _Optional[str] = ..., suppressEmailInvite: bool = ...) -> None: ... class UserUpdateRequest(_message.Message): __slots__ = ("users",) @@ -1428,7 +1455,7 @@ class ComplianceRecordOwnersRequest(_message.Message): INCLUDENONSHARED_FIELD_NUMBER: _ClassVar[int] nodeIds: _containers.RepeatedScalarFieldContainer[int] includeNonShared: bool - def __init__(self, nodeIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ...) -> None: ... + def __init__(self, nodeIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ...) -> None: ... class ComplianceRecordOwnersResponse(_message.Message): __slots__ = ("recordOwners",) @@ -1442,7 +1469,7 @@ class RecordOwner(_message.Message): SHARED_FIELD_NUMBER: _ClassVar[int] enterpriseUserId: int shared: bool - def __init__(self, enterpriseUserId: _Optional[int] = ..., shared: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., shared: bool = ...) -> None: ... class PreliminaryComplianceDataRequest(_message.Message): __slots__ = ("enterpriseUserIds", "includeNonShared", "continuationToken", "includeTotalMatchingRecordsInFirstResponse") @@ -1454,7 +1481,7 @@ class PreliminaryComplianceDataRequest(_message.Message): includeNonShared: bool continuationToken: bytes includeTotalMatchingRecordsInFirstResponse: bool - def __init__(self, enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ..., continuationToken: _Optional[bytes] = ..., includeTotalMatchingRecordsInFirstResponse: _Optional[bool] = ...) -> None: ... + def __init__(self, enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ..., continuationToken: _Optional[bytes] = ..., includeTotalMatchingRecordsInFirstResponse: bool = ...) -> None: ... class PreliminaryComplianceDataResponse(_message.Message): __slots__ = ("auditUserData", "continuationToken", "hasMore", "totalMatchingRecords") @@ -1466,7 +1493,7 @@ class PreliminaryComplianceDataResponse(_message.Message): continuationToken: bytes hasMore: bool totalMatchingRecords: int - def __init__(self, auditUserData: _Optional[_Iterable[_Union[AuditUserData, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ..., hasMore: _Optional[bool] = ..., totalMatchingRecords: _Optional[int] = ...) -> None: ... + def __init__(self, auditUserData: _Optional[_Iterable[_Union[AuditUserData, _Mapping]]] = ..., continuationToken: _Optional[bytes] = ..., hasMore: bool = ..., totalMatchingRecords: _Optional[int] = ...) -> None: ... class AuditUserRecord(_message.Message): __slots__ = ("recordUid", "encryptedData", "shared") @@ -1476,7 +1503,7 @@ class AuditUserRecord(_message.Message): recordUid: bytes encryptedData: bytes shared: bool - def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., shared: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., shared: bool = ...) -> None: ... class AuditUserData(_message.Message): __slots__ = ("enterpriseUserId", "auditUserRecords", "status") @@ -1510,7 +1537,7 @@ class ComplianceReportRequest(_message.Message): complianceReportRun: ComplianceReportRun reportName: str saveReport: bool - def __init__(self, complianceReportRun: _Optional[_Union[ComplianceReportRun, _Mapping]] = ..., reportName: _Optional[str] = ..., saveReport: _Optional[bool] = ...) -> None: ... + def __init__(self, complianceReportRun: _Optional[_Union[ComplianceReportRun, _Mapping]] = ..., reportName: _Optional[str] = ..., saveReport: bool = ...) -> None: ... class ComplianceReportRun(_message.Message): __slots__ = ("reportCriteriaAndFilter", "users", "records") @@ -1548,7 +1575,7 @@ class ComplianceReportCriteria(_message.Message): jobTitles: _containers.RepeatedScalarFieldContainer[str] enterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] includeNonShared: bool - def __init__(self, jobTitles: _Optional[_Iterable[str]] = ..., enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: _Optional[bool] = ...) -> None: ... + def __init__(self, jobTitles: _Optional[_Iterable[str]] = ..., enterpriseUserIds: _Optional[_Iterable[int]] = ..., includeNonShared: bool = ...) -> None: ... class ComplianceReportFilter(_message.Message): __slots__ = ("recordTitles", "recordUids", "jobTitles", "urls", "recordTypes") @@ -1612,7 +1639,7 @@ class AuditRecord(_message.Message): inTrash: bool treeLeft: int treeRight: int - def __init__(self, recordUid: _Optional[bytes] = ..., auditData: _Optional[bytes] = ..., hasAttachments: _Optional[bool] = ..., inTrash: _Optional[bool] = ..., treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., auditData: _Optional[bytes] = ..., hasAttachments: bool = ..., inTrash: bool = ..., treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ...) -> None: ... class AuditRole(_message.Message): __slots__ = ("roleId", "encryptedData", "restrictShareOutsideEnterprise", "restrictShareAll", "restrictShareOfAttachments", "restrictMaskPasswordsWhileEditing", "roleNodeManagements") @@ -1630,7 +1657,7 @@ class AuditRole(_message.Message): restrictShareOfAttachments: bool restrictMaskPasswordsWhileEditing: bool roleNodeManagements: _containers.RepeatedCompositeFieldContainer[RoleNodeManagement] - def __init__(self, roleId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., restrictShareOutsideEnterprise: _Optional[bool] = ..., restrictShareAll: _Optional[bool] = ..., restrictShareOfAttachments: _Optional[bool] = ..., restrictMaskPasswordsWhileEditing: _Optional[bool] = ..., roleNodeManagements: _Optional[_Iterable[_Union[RoleNodeManagement, _Mapping]]] = ...) -> None: ... + def __init__(self, roleId: _Optional[int] = ..., encryptedData: _Optional[bytes] = ..., restrictShareOutsideEnterprise: bool = ..., restrictShareAll: bool = ..., restrictShareOfAttachments: bool = ..., restrictMaskPasswordsWhileEditing: bool = ..., roleNodeManagements: _Optional[_Iterable[_Union[RoleNodeManagement, _Mapping]]] = ...) -> None: ... class RoleNodeManagement(_message.Message): __slots__ = ("treeLeft", "treeRight", "cascade", "privileges") @@ -1642,7 +1669,7 @@ class RoleNodeManagement(_message.Message): treeRight: int cascade: bool privileges: int - def __init__(self, treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ..., cascade: _Optional[bool] = ..., privileges: _Optional[int] = ...) -> None: ... + def __init__(self, treeLeft: _Optional[int] = ..., treeRight: _Optional[int] = ..., cascade: bool = ..., privileges: _Optional[int] = ...) -> None: ... class UserProfile(_message.Message): __slots__ = ("enterpriseUserId", "fullName", "jobTitle", "email", "roleIds") @@ -1684,7 +1711,7 @@ class AuditTeam(_message.Message): teamName: str restrictEdit: bool restrictShare: bool - def __init__(self, teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., restrictEdit: _Optional[bool] = ..., restrictShare: _Optional[bool] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., teamName: _Optional[str] = ..., restrictEdit: bool = ..., restrictShare: bool = ...) -> None: ... class AuditTeamUser(_message.Message): __slots__ = ("teamUid", "enterpriseUserIds") @@ -1788,7 +1815,7 @@ class UserProfileExt(_message.Message): isShareAdminForRequestedObject: bool isShareAdminForSharedFolderOwner: bool hasAccessToObject: bool - def __init__(self, email: _Optional[str] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., isMSPMCAdmin: _Optional[bool] = ..., isInSharedFolder: _Optional[bool] = ..., isShareAdminForRequestedObject: _Optional[bool] = ..., isShareAdminForSharedFolderOwner: _Optional[bool] = ..., hasAccessToObject: _Optional[bool] = ...) -> None: ... + def __init__(self, email: _Optional[str] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., isMSPMCAdmin: bool = ..., isInSharedFolder: bool = ..., isShareAdminForRequestedObject: bool = ..., isShareAdminForSharedFolderOwner: bool = ..., hasAccessToObject: bool = ...) -> None: ... class GetSharingAdminsResponse(_message.Message): __slots__ = ("userProfileExts",) @@ -1852,7 +1879,7 @@ class TeamsEnterpriseUsersAddTeamResponse(_message.Message): message: str resultCode: str additionalInfo: str - def __init__(self, teamUid: _Optional[bytes] = ..., users: _Optional[_Iterable[_Union[TeamsEnterpriseUsersAddUserResponse, _Mapping]]] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., users: _Optional[_Iterable[_Union[TeamsEnterpriseUsersAddUserResponse, _Mapping]]] = ..., success: bool = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class TeamsEnterpriseUsersAddUserResponse(_message.Message): __slots__ = ("enterpriseUserId", "revision", "success", "message", "resultCode", "additionalInfo") @@ -1868,7 +1895,7 @@ class TeamsEnterpriseUsersAddUserResponse(_message.Message): message: str resultCode: str additionalInfo: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., revision: _Optional[int] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., revision: _Optional[int] = ..., success: bool = ..., message: _Optional[str] = ..., resultCode: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class TeamEnterpriseUserRemove(_message.Message): __slots__ = ("teamUid", "enterpriseUserId") @@ -1902,7 +1929,7 @@ class TeamEnterpriseUserRemoveResponse(_message.Message): resultCode: str message: str additionalInfo: str - def __init__(self, teamEnterpriseUserRemove: _Optional[_Union[TeamEnterpriseUserRemove, _Mapping]] = ..., success: _Optional[bool] = ..., resultCode: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, teamEnterpriseUserRemove: _Optional[_Union[TeamEnterpriseUserRemove, _Mapping]] = ..., success: bool = ..., resultCode: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class DomainAlias(_message.Message): __slots__ = ("domain", "alias", "status", "message") @@ -2022,7 +2049,7 @@ class EnterpriseUsersAdd(_message.Message): inviteeLocale: str move: bool roleId: int - def __init__(self, enterpriseUserId: _Optional[int] = ..., username: _Optional[str] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., suppressEmailInvite: _Optional[bool] = ..., inviteeLocale: _Optional[str] = ..., move: _Optional[bool] = ..., roleId: _Optional[int] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., username: _Optional[str] = ..., nodeId: _Optional[int] = ..., encryptedData: _Optional[str] = ..., keyType: _Optional[_Union[EncryptedKeyType, str]] = ..., fullName: _Optional[str] = ..., jobTitle: _Optional[str] = ..., suppressEmailInvite: bool = ..., inviteeLocale: _Optional[str] = ..., move: bool = ..., roleId: _Optional[int] = ...) -> None: ... class EnterpriseUsersAddResponse(_message.Message): __slots__ = ("results", "success", "code", "message", "additionalInfo") @@ -2036,7 +2063,7 @@ class EnterpriseUsersAddResponse(_message.Message): code: str message: str additionalInfo: str - def __init__(self, results: _Optional[_Iterable[_Union[EnterpriseUsersAddResult, _Mapping]]] = ..., success: _Optional[bool] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, results: _Optional[_Iterable[_Union[EnterpriseUsersAddResult, _Mapping]]] = ..., success: bool = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class EnterpriseUsersAddResult(_message.Message): __slots__ = ("enterpriseUserId", "success", "verificationCode", "code", "message", "additionalInfo") @@ -2052,7 +2079,7 @@ class EnterpriseUsersAddResult(_message.Message): code: str message: str additionalInfo: str - def __init__(self, enterpriseUserId: _Optional[int] = ..., success: _Optional[bool] = ..., verificationCode: _Optional[str] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[int] = ..., success: bool = ..., verificationCode: _Optional[str] = ..., code: _Optional[str] = ..., message: _Optional[str] = ..., additionalInfo: _Optional[str] = ...) -> None: ... class UpdateMSPPermitsRequest(_message.Message): __slots__ = ("mspEnterpriseId", "maxAllowedLicenses", "allowedMcProducts", "allowedAddOns", "maxFilePlanType", "allowUnlimitedLicenses") @@ -2068,7 +2095,7 @@ class UpdateMSPPermitsRequest(_message.Message): allowedAddOns: _containers.RepeatedScalarFieldContainer[str] maxFilePlanType: str allowUnlimitedLicenses: bool - def __init__(self, mspEnterpriseId: _Optional[int] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: _Optional[bool] = ...) -> None: ... + def __init__(self, mspEnterpriseId: _Optional[int] = ..., maxAllowedLicenses: _Optional[int] = ..., allowedMcProducts: _Optional[_Iterable[str]] = ..., allowedAddOns: _Optional[_Iterable[str]] = ..., maxFilePlanType: _Optional[str] = ..., allowUnlimitedLicenses: bool = ...) -> None: ... class DeleteEnterpriseUsersRequest(_message.Message): __slots__ = ("enterpriseUserIds",) @@ -2098,7 +2125,27 @@ class ClearSecurityDataRequest(_message.Message): enterpriseUserId: _containers.RepeatedScalarFieldContainer[int] allUsers: bool type: ClearSecurityDataType - def __init__(self, enterpriseUserId: _Optional[_Iterable[int]] = ..., allUsers: _Optional[bool] = ..., type: _Optional[_Union[ClearSecurityDataType, str]] = ...) -> None: ... + def __init__(self, enterpriseUserId: _Optional[_Iterable[int]] = ..., allUsers: bool = ..., type: _Optional[_Union[ClearSecurityDataType, str]] = ...) -> None: ... + +class ListDomainsResponse(_message.Message): + __slots__ = ("domain",) + DOMAIN_FIELD_NUMBER: _ClassVar[int] + domain: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, domain: _Optional[_Iterable[str]] = ...) -> None: ... + +class ReserveDomainRequest(_message.Message): + __slots__ = ("reserveDomainAction", "domain") + RESERVEDOMAINACTION_FIELD_NUMBER: _ClassVar[int] + DOMAIN_FIELD_NUMBER: _ClassVar[int] + reserveDomainAction: ReserveDomainAction + domain: str + def __init__(self, reserveDomainAction: _Optional[_Union[ReserveDomainAction, str]] = ..., domain: _Optional[str] = ...) -> None: ... + +class ReserveDomainResponse(_message.Message): + __slots__ = ("token",) + TOKEN_FIELD_NUMBER: _ClassVar[int] + token: str + def __init__(self, token: _Optional[str] = ...) -> None: ... class RolesByTeam(_message.Message): __slots__ = ("teamUid", "roleId") @@ -2118,7 +2165,7 @@ class LockUsersRequest(_message.Message): disableEnterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] unlockEnterpriseUserIds: _containers.RepeatedScalarFieldContainer[int] deleteIfPending: bool - def __init__(self, lockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., disableEnterpriseUserIds: _Optional[_Iterable[int]] = ..., unlockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., deleteIfPending: _Optional[bool] = ...) -> None: ... + def __init__(self, lockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., disableEnterpriseUserIds: _Optional[_Iterable[int]] = ..., unlockEnterpriseUserIds: _Optional[_Iterable[int]] = ..., deleteIfPending: bool = ...) -> None: ... class LockUsersResponse(_message.Message): __slots__ = ("response",) diff --git a/keepersdk-package/src/keepersdk/proto/folder_access_pb2.py b/keepersdk-package/src/keepersdk/proto/folder_access_pb2.py new file mode 100644 index 00000000..d5e7d196 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/folder_access_pb2.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: folder_access.proto +# Protobuf Python Version: 5.29.5 +"""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, + 29, + 5, + '', + 'folder_access.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import folder_pb2 as folder__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13\x66older_access.proto\x12\tfolder.v3\x1a\x0c\x66older.proto\x1a\x1cgoogle/api/annotations.proto\"\xa3\x01\n\x16GetFolderAccessRequest\x12\x11\n\tfolderUid\x18\x01 \x03(\x0c\x12<\n\x11\x63ontinuationToken\x18\x02 \x01(\x0b\x32\x1c.folder.v3.ContinuationTokenH\x00\x88\x01\x01\x12\x15\n\x08pageSize\x18\x03 \x01(\x05H\x01\x88\x01\x01\x42\x14\n\x12_continuationTokenB\x0b\n\t_pageSize\"\xbd\x01\n\x17GetFolderAccessResponse\x12=\n\x13\x66olderAccessResults\x18\x01 \x03(\x0b\x32 .folder.v3.GetFolderAccessResult\x12<\n\x11\x63ontinuationToken\x18\x02 \x01(\x0b\x32\x1c.folder.v3.ContinuationTokenH\x00\x88\x01\x01\x12\x0f\n\x07hasMore\x18\x03 \x01(\x08\x42\x14\n\x12_continuationToken\")\n\x11\x43ontinuationToken\x12\x14\n\x0clastModified\x18\x01 \x01(\x03\"\x93\x01\n\x15GetFolderAccessResult\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12+\n\taccessors\x18\x02 \x03(\x0b\x32\x18.Folder.FolderAccessData\x12\x30\n\x05\x65rror\x18\x03 \x01(\x0b\x32\x1c.folder.v3.FolderAccessErrorH\x00\x88\x01\x01\x42\x08\n\x06_error\"P\n\x11\x46olderAccessError\x12*\n\x06status\x18\x01 \x01(\x0e\x32\x1a.Folder.FolderModifyStatus\x12\x0f\n\x07message\x18\x02 \x01(\t2\x98\x01\n\rFolderService\x12\x86\x01\n\x0fGetFolderAccess\x12!.folder.v3.GetFolderAccessRequest\x1a\".folder.v3.GetFolderAccessResponse\",\x82\xd3\xe4\x93\x02&\"!/api/rest/vault/folders/v3/access:\x01*B*\n&com.keepersecurity.proto.api.folder.v3P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'folder_access_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.keepersecurity.proto.api.folder.v3P\001' + _globals['_FOLDERSERVICE'].methods_by_name['GetFolderAccess']._loaded_options = None + _globals['_FOLDERSERVICE'].methods_by_name['GetFolderAccess']._serialized_options = b'\202\323\344\223\002&\"!/api/rest/vault/folders/v3/access:\001*' + _globals['_GETFOLDERACCESSREQUEST']._serialized_start=79 + _globals['_GETFOLDERACCESSREQUEST']._serialized_end=242 + _globals['_GETFOLDERACCESSRESPONSE']._serialized_start=245 + _globals['_GETFOLDERACCESSRESPONSE']._serialized_end=434 + _globals['_CONTINUATIONTOKEN']._serialized_start=436 + _globals['_CONTINUATIONTOKEN']._serialized_end=477 + _globals['_GETFOLDERACCESSRESULT']._serialized_start=480 + _globals['_GETFOLDERACCESSRESULT']._serialized_end=627 + _globals['_FOLDERACCESSERROR']._serialized_start=629 + _globals['_FOLDERACCESSERROR']._serialized_end=709 + _globals['_FOLDERSERVICE']._serialized_start=712 + _globals['_FOLDERSERVICE']._serialized_end=864 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/folder_access_pb2.pyi b/keepersdk-package/src/keepersdk/proto/folder_access_pb2.pyi new file mode 100644 index 00000000..db1226e1 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/folder_access_pb2.pyi @@ -0,0 +1,52 @@ +from . import folder_pb2 as _folder_pb2 +from google.api import annotations_pb2 as _annotations_pb2 +from google.protobuf.internal import containers as _containers +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class GetFolderAccessRequest(_message.Message): + __slots__ = ("folderUid", "continuationToken", "pageSize") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] + PAGESIZE_FIELD_NUMBER: _ClassVar[int] + folderUid: _containers.RepeatedScalarFieldContainer[bytes] + continuationToken: ContinuationToken + pageSize: int + def __init__(self, folderUid: _Optional[_Iterable[bytes]] = ..., continuationToken: _Optional[_Union[ContinuationToken, _Mapping]] = ..., pageSize: _Optional[int] = ...) -> None: ... + +class GetFolderAccessResponse(_message.Message): + __slots__ = ("folderAccessResults", "continuationToken", "hasMore") + FOLDERACCESSRESULTS_FIELD_NUMBER: _ClassVar[int] + CONTINUATIONTOKEN_FIELD_NUMBER: _ClassVar[int] + HASMORE_FIELD_NUMBER: _ClassVar[int] + folderAccessResults: _containers.RepeatedCompositeFieldContainer[GetFolderAccessResult] + continuationToken: ContinuationToken + hasMore: bool + def __init__(self, folderAccessResults: _Optional[_Iterable[_Union[GetFolderAccessResult, _Mapping]]] = ..., continuationToken: _Optional[_Union[ContinuationToken, _Mapping]] = ..., hasMore: bool = ...) -> None: ... + +class ContinuationToken(_message.Message): + __slots__ = ("lastModified",) + LASTMODIFIED_FIELD_NUMBER: _ClassVar[int] + lastModified: int + def __init__(self, lastModified: _Optional[int] = ...) -> None: ... + +class GetFolderAccessResult(_message.Message): + __slots__ = ("folderUid", "accessors", "error") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + ACCESSORS_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + accessors: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderAccessData] + error: FolderAccessError + def __init__(self, folderUid: _Optional[bytes] = ..., accessors: _Optional[_Iterable[_Union[_folder_pb2.FolderAccessData, _Mapping]]] = ..., error: _Optional[_Union[FolderAccessError, _Mapping]] = ...) -> None: ... + +class FolderAccessError(_message.Message): + __slots__ = ("status", "message") + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + status: _folder_pb2.FolderModifyStatus + message: str + def __init__(self, status: _Optional[_Union[_folder_pb2.FolderModifyStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/folder_pb2.py b/keepersdk-package/src/keepersdk/proto/folder_pb2.py index aca26a46..466f3cdf 100644 --- a/keepersdk-package/src/keepersdk/proto/folder_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/folder_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: folder.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'folder.proto' ) @@ -23,9 +23,10 @@ from . import record_pb2 as record__pb2 +from . import tla_pb2 as tla__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x66older.proto\x12\x06\x46older\x1a\x0crecord.proto\"\\\n\x10\x45ncryptedDataKey\x12\x14\n\x0c\x65ncryptedKey\x18\x01 \x01(\x0c\x12\x32\n\x10\x65ncryptedKeyType\x18\x02 \x01(\x0e\x32\x18.Folder.EncryptedKeyType\"\x82\x01\n\x16SharedFolderRecordData\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x32\n\x10\x65ncryptedDataKey\x18\x04 \x03(\x0b\x32\x18.Folder.EncryptedDataKey\"\\\n\x1aSharedFolderRecordDataList\x12>\n\x16sharedFolderRecordData\x18\x01 \x03(\x0b\x32\x1e.Folder.SharedFolderRecordData\"_\n\x15SharedFolderRecordFix\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12 \n\x18\x65ncryptedRecordFolderKey\x18\x03 \x01(\x0c\"Y\n\x19SharedFolderRecordFixList\x12<\n\x15sharedFolderRecordFix\x18\x01 \x03(\x0b\x32\x1d.Folder.SharedFolderRecordFix\"\xa2\x02\n\rRecordRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12&\n\nrecordType\x18\x02 \x01(\x0e\x32\x12.Folder.RecordType\x12\x12\n\nrecordData\x18\x03 \x01(\x0c\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x04 \x01(\x0c\x12&\n\nfolderType\x18\x05 \x01(\x0e\x32\x12.Folder.FolderType\x12\x12\n\nhowLongAgo\x18\x06 \x01(\x03\x12\x11\n\tfolderUid\x18\x07 \x01(\x0c\x12 \n\x18\x65ncryptedRecordFolderKey\x18\x08 \x01(\x0c\x12\r\n\x05\x65xtra\x18\t \x01(\x0c\x12\x15\n\rnonSharedData\x18\n \x01(\x0c\x12\x0f\n\x07\x66ileIds\x18\x0b \x03(\x03\"E\n\x0eRecordResponse\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\"\x80\x01\n\x12SharedFolderFields\x12\x1b\n\x13\x65ncryptedFolderName\x18\x01 \x01(\x0c\x12\x13\n\x0bmanageUsers\x18\x02 \x01(\x08\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x04 \x01(\x08\x12\x10\n\x08\x63\x61nShare\x18\x05 \x01(\x08\"3\n\x18SharedFolderFolderFields\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\"\x8f\x02\n\rFolderRequest\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12&\n\nfolderType\x18\x02 \x01(\x0e\x32\x12.Folder.FolderType\x12\x17\n\x0fparentFolderUid\x18\x03 \x01(\x0c\x12\x12\n\nfolderData\x18\x04 \x01(\x0c\x12\x1a\n\x12\x65ncryptedFolderKey\x18\x05 \x01(\x0c\x12\x36\n\x12sharedFolderFields\x18\x06 \x01(\x0b\x32\x1a.Folder.SharedFolderFields\x12\x42\n\x18sharedFolderFolderFields\x18\x07 \x01(\x0b\x32 .Folder.SharedFolderFolderFields\"E\n\x0e\x46olderResponse\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\"w\n\x19ImportFolderRecordRequest\x12,\n\rfolderRequest\x18\x01 \x03(\x0b\x32\x15.Folder.FolderRequest\x12,\n\rrecordRequest\x18\x02 \x03(\x0b\x32\x15.Folder.RecordRequest\"|\n\x1aImportFolderRecordResponse\x12.\n\x0e\x66olderResponse\x18\x01 \x03(\x0b\x32\x16.Folder.FolderResponse\x12.\n\x0erecordResponse\x18\x02 \x03(\x0b\x32\x16.Folder.RecordResponse\"\xc9\x02\n\x18SharedFolderUpdateRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\x12(\n\x07\x63\x61nEdit\x18\x04 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12)\n\x08\x63\x61nShare\x18\x05 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x06 \x01(\x0c\x12\x10\n\x08revision\x18\x07 \x01(\x05\x12\x12\n\nexpiration\x18\x08 \x01(\x12\x12=\n\x15timerNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\n \x01(\x08\"\xcc\x02\n\x16SharedFolderUpdateUser\x12\x10\n\x08username\x18\x01 \x01(\t\x12,\n\x0bmanageUsers\x18\x02 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12.\n\rmanageRecords\x18\x03 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x1b\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x42\x02\x18\x01\x12\x12\n\nexpiration\x18\x05 \x01(\x12\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x36\n\x14typedSharedFolderKey\x18\x07 \x01(\x0b\x32\x18.Folder.EncryptedDataKey\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x99\x02\n\x16SharedFolderUpdateTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x13\n\x0bmanageUsers\x18\x02 \x01(\x08\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x1b\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x42\x02\x18\x01\x12\x12\n\nexpiration\x18\x05 \x01(\x12\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x36\n\x14typedSharedFolderKey\x18\x07 \x01(\x0b\x32\x18.Folder.EncryptedDataKey\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x8e\x07\n\x1bSharedFolderUpdateV3Request\x12,\n$sharedFolderUpdateOperation_dont_use\x18\x01 \x01(\x05\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12!\n\x19\x65ncryptedSharedFolderName\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\x12\x13\n\x0b\x66orceUpdate\x18\x05 \x01(\x08\x12\x13\n\x0b\x66romTeamUid\x18\x06 \x01(\x0c\x12\x33\n\x12\x64\x65\x66\x61ultManageUsers\x18\x07 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x35\n\x14\x64\x65\x66\x61ultManageRecords\x18\x08 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x64\x65\x66\x61ultCanEdit\x18\t \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x30\n\x0f\x64\x65\x66\x61ultCanShare\x18\n \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12?\n\x15sharedFolderAddRecord\x18\x0b \x03(\x0b\x32 .Folder.SharedFolderUpdateRecord\x12;\n\x13sharedFolderAddUser\x18\x0c \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateUser\x12;\n\x13sharedFolderAddTeam\x18\r \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateTeam\x12\x42\n\x18sharedFolderUpdateRecord\x18\x0e \x03(\x0b\x32 .Folder.SharedFolderUpdateRecord\x12>\n\x16sharedFolderUpdateUser\x18\x0f \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateUser\x12>\n\x16sharedFolderUpdateTeam\x18\x10 \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateTeam\x12 \n\x18sharedFolderRemoveRecord\x18\x11 \x03(\x0c\x12\x1e\n\x16sharedFolderRemoveUser\x18\x12 \x03(\t\x12\x1e\n\x16sharedFolderRemoveTeam\x18\x13 \x03(\x0c\x12\x19\n\x11sharedFolderOwner\x18\x14 \x01(\t\"c\n\x1dSharedFolderUpdateV3RequestV2\x12\x42\n\x15sharedFoldersUpdateV3\x18\x01 \x03(\x0b\x32#.Folder.SharedFolderUpdateV3Request\"C\n\x1eSharedFolderUpdateRecordStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\"@\n\x1cSharedFolderUpdateUserStatus\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"?\n\x1cSharedFolderUpdateTeamStatus\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\"\x88\x06\n\x1cSharedFolderUpdateV3Response\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12K\n\x1bsharedFolderAddRecordStatus\x18\x02 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12G\n\x19sharedFolderAddUserStatus\x18\x03 \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12G\n\x19sharedFolderAddTeamStatus\x18\x04 \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12N\n\x1esharedFolderUpdateRecordStatus\x18\x05 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12J\n\x1csharedFolderUpdateUserStatus\x18\x06 \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12J\n\x1csharedFolderUpdateTeamStatus\x18\x07 \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12N\n\x1esharedFolderRemoveRecordStatus\x18\x08 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12J\n\x1csharedFolderRemoveUserStatus\x18\t \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12J\n\x1csharedFolderRemoveTeamStatus\x18\n \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12\x17\n\x0fsharedFolderUid\x18\x0c \x01(\x0c\x12\x0e\n\x06status\x18\r \x01(\t\"m\n\x1eSharedFolderUpdateV3ResponseV2\x12K\n\x1dsharedFoldersUpdateV3Response\x18\x01 \x03(\x0b\x32$.Folder.SharedFolderUpdateV3Response\"\xfa\x01\n)GetDeletedSharedFoldersAndRecordsResponse\x12\x32\n\rsharedFolders\x18\x01 \x03(\x0b\x32\x1b.Folder.DeletedSharedFolder\x12>\n\x13sharedFolderRecords\x18\x02 \x03(\x0b\x32!.Folder.DeletedSharedFolderRecord\x12\x34\n\x11\x64\x65letedRecordData\x18\x03 \x03(\x0b\x32\x19.Folder.DeletedRecordData\x12#\n\tusernames\x18\x04 \x03(\x0b\x32\x10.Folder.Username\"\xd1\x01\n\x13\x44\x65letedSharedFolder\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\x17\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x12-\n\rfolderKeyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x13\n\x0b\x64\x61teDeleted\x18\x07 \x01(\x03\x12\x10\n\x08revision\x18\x08 \x01(\x03\"\x81\x01\n\x19\x44\x65letedSharedFolderRecord\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x17\n\x0fsharedRecordKey\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64\x61teDeleted\x18\x04 \x01(\x03\x12\x10\n\x08revision\x18\x05 \x01(\x03\"\x85\x01\n\x11\x44\x65letedRecordData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08ownerUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x1a\n\x12\x63lientModifiedTime\x18\x04 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\x12\x0f\n\x07version\x18\x06 \x01(\x05\"0\n\x08Username\x12\x12\n\naccountUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\"\x8a\x01\n,RestoreDeletedSharedFoldersAndRecordsRequest\x12,\n\x07\x66olders\x18\x01 \x03(\x0b\x32\x1b.Folder.RestoreSharedObject\x12,\n\x07records\x18\x02 \x03(\x0b\x32\x1b.Folder.RestoreSharedObject\"<\n\x13RestoreSharedObject\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c*\x1a\n\nRecordType\x12\x0c\n\x08password\x10\x00*^\n\nFolderType\x12\x12\n\x0e\x64\x65\x66\x61ult_folder\x10\x00\x12\x0f\n\x0buser_folder\x10\x01\x12\x11\n\rshared_folder\x10\x02\x12\x18\n\x14shared_folder_folder\x10\x03*\x96\x01\n\x10\x45ncryptedKeyType\x12\n\n\x06no_key\x10\x00\x12\x19\n\x15\x65ncrypted_by_data_key\x10\x01\x12\x1b\n\x17\x65ncrypted_by_public_key\x10\x02\x12\x1d\n\x19\x65ncrypted_by_data_key_gcm\x10\x03\x12\x1f\n\x1b\x65ncrypted_by_public_key_ecc\x10\x04*M\n\x0fSetBooleanValue\x12\x15\n\x11\x42OOLEAN_NO_CHANGE\x10\x00\x12\x10\n\x0c\x42OOLEAN_TRUE\x10\x01\x12\x11\n\rBOOLEAN_FALSE\x10\x02\x42\"\n\x18\x63om.keepersecurity.protoB\x06\x46olderb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x66older.proto\x12\x06\x46older\x1a\x0crecord.proto\x1a\ttla.proto\"\\\n\x10\x45ncryptedDataKey\x12\x14\n\x0c\x65ncryptedKey\x18\x01 \x01(\x0c\x12\x32\n\x10\x65ncryptedKeyType\x18\x02 \x01(\x0e\x32\x18.Folder.EncryptedKeyType\"\x82\x01\n\x16SharedFolderRecordData\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06userId\x18\x03 \x01(\x05\x12\x32\n\x10\x65ncryptedDataKey\x18\x04 \x03(\x0b\x32\x18.Folder.EncryptedDataKey\"\\\n\x1aSharedFolderRecordDataList\x12>\n\x16sharedFolderRecordData\x18\x01 \x03(\x0b\x32\x1e.Folder.SharedFolderRecordData\"_\n\x15SharedFolderRecordFix\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12 \n\x18\x65ncryptedRecordFolderKey\x18\x03 \x01(\x0c\"Y\n\x19SharedFolderRecordFixList\x12<\n\x15sharedFolderRecordFix\x18\x01 \x03(\x0b\x32\x1d.Folder.SharedFolderRecordFix\"\xa2\x02\n\rRecordRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12&\n\nrecordType\x18\x02 \x01(\x0e\x32\x12.Folder.RecordType\x12\x12\n\nrecordData\x18\x03 \x01(\x0c\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x04 \x01(\x0c\x12&\n\nfolderType\x18\x05 \x01(\x0e\x32\x12.Folder.FolderType\x12\x12\n\nhowLongAgo\x18\x06 \x01(\x03\x12\x11\n\tfolderUid\x18\x07 \x01(\x0c\x12 \n\x18\x65ncryptedRecordFolderKey\x18\x08 \x01(\x0c\x12\r\n\x05\x65xtra\x18\t \x01(\x0c\x12\x15\n\rnonSharedData\x18\n \x01(\x0c\x12\x0f\n\x07\x66ileIds\x18\x0b \x03(\x03\"E\n\x0eRecordResponse\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\"\x80\x01\n\x12SharedFolderFields\x12\x1b\n\x13\x65ncryptedFolderName\x18\x01 \x01(\x0c\x12\x13\n\x0bmanageUsers\x18\x02 \x01(\x08\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x04 \x01(\x08\x12\x10\n\x08\x63\x61nShare\x18\x05 \x01(\x08\"3\n\x18SharedFolderFolderFields\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\"\x8f\x02\n\rFolderRequest\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12&\n\nfolderType\x18\x02 \x01(\x0e\x32\x12.Folder.FolderType\x12\x17\n\x0fparentFolderUid\x18\x03 \x01(\x0c\x12\x12\n\nfolderData\x18\x04 \x01(\x0c\x12\x1a\n\x12\x65ncryptedFolderKey\x18\x05 \x01(\x0c\x12\x36\n\x12sharedFolderFields\x18\x06 \x01(\x0b\x32\x1a.Folder.SharedFolderFields\x12\x42\n\x18sharedFolderFolderFields\x18\x07 \x01(\x0b\x32 .Folder.SharedFolderFolderFields\"E\n\x0e\x46olderResponse\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0e\n\x06status\x18\x03 \x01(\t\"w\n\x19ImportFolderRecordRequest\x12,\n\rfolderRequest\x18\x01 \x03(\x0b\x32\x15.Folder.FolderRequest\x12,\n\rrecordRequest\x18\x02 \x03(\x0b\x32\x15.Folder.RecordRequest\"|\n\x1aImportFolderRecordResponse\x12.\n\x0e\x66olderResponse\x18\x01 \x03(\x0b\x32\x16.Folder.FolderResponse\x12.\n\x0erecordResponse\x18\x02 \x03(\x0b\x32\x16.Folder.RecordResponse\"\xc9\x02\n\x18SharedFolderUpdateRecord\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\x12(\n\x07\x63\x61nEdit\x18\x04 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12)\n\x08\x63\x61nShare\x18\x05 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x06 \x01(\x0c\x12\x10\n\x08revision\x18\x07 \x01(\x05\x12\x12\n\nexpiration\x18\x08 \x01(\x12\x12=\n\x15timerNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\n \x01(\x08\"\xcc\x02\n\x16SharedFolderUpdateUser\x12\x10\n\x08username\x18\x01 \x01(\t\x12,\n\x0bmanageUsers\x18\x02 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12.\n\rmanageRecords\x18\x03 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x1b\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x42\x02\x18\x01\x12\x12\n\nexpiration\x18\x05 \x01(\x12\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x36\n\x14typedSharedFolderKey\x18\x07 \x01(\x0b\x32\x18.Folder.EncryptedDataKey\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x99\x02\n\x16SharedFolderUpdateTeam\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x13\n\x0bmanageUsers\x18\x02 \x01(\x08\x12\x15\n\rmanageRecords\x18\x03 \x01(\x08\x12\x1b\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x42\x02\x18\x01\x12\x12\n\nexpiration\x18\x05 \x01(\x12\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x36\n\x14typedSharedFolderKey\x18\x07 \x01(\x0b\x32\x18.Folder.EncryptedDataKey\x12\x1a\n\x12rotateOnExpiration\x18\x08 \x01(\x08\"\x8e\x07\n\x1bSharedFolderUpdateV3Request\x12,\n$sharedFolderUpdateOperation_dont_use\x18\x01 \x01(\x05\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\x12!\n\x19\x65ncryptedSharedFolderName\x18\x03 \x01(\x0c\x12\x10\n\x08revision\x18\x04 \x01(\x03\x12\x13\n\x0b\x66orceUpdate\x18\x05 \x01(\x08\x12\x13\n\x0b\x66romTeamUid\x18\x06 \x01(\x0c\x12\x33\n\x12\x64\x65\x66\x61ultManageUsers\x18\x07 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x35\n\x14\x64\x65\x66\x61ultManageRecords\x18\x08 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x64\x65\x66\x61ultCanEdit\x18\t \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x30\n\x0f\x64\x65\x66\x61ultCanShare\x18\n \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12?\n\x15sharedFolderAddRecord\x18\x0b \x03(\x0b\x32 .Folder.SharedFolderUpdateRecord\x12;\n\x13sharedFolderAddUser\x18\x0c \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateUser\x12;\n\x13sharedFolderAddTeam\x18\r \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateTeam\x12\x42\n\x18sharedFolderUpdateRecord\x18\x0e \x03(\x0b\x32 .Folder.SharedFolderUpdateRecord\x12>\n\x16sharedFolderUpdateUser\x18\x0f \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateUser\x12>\n\x16sharedFolderUpdateTeam\x18\x10 \x03(\x0b\x32\x1e.Folder.SharedFolderUpdateTeam\x12 \n\x18sharedFolderRemoveRecord\x18\x11 \x03(\x0c\x12\x1e\n\x16sharedFolderRemoveUser\x18\x12 \x03(\t\x12\x1e\n\x16sharedFolderRemoveTeam\x18\x13 \x03(\x0c\x12\x19\n\x11sharedFolderOwner\x18\x14 \x01(\t\"c\n\x1dSharedFolderUpdateV3RequestV2\x12\x42\n\x15sharedFoldersUpdateV3\x18\x01 \x03(\x0b\x32#.Folder.SharedFolderUpdateV3Request\"C\n\x1eSharedFolderUpdateRecordStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\"@\n\x1cSharedFolderUpdateUserStatus\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"?\n\x1cSharedFolderUpdateTeamStatus\x12\x0f\n\x07teamUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\"\x88\x06\n\x1cSharedFolderUpdateV3Response\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12K\n\x1bsharedFolderAddRecordStatus\x18\x02 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12G\n\x19sharedFolderAddUserStatus\x18\x03 \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12G\n\x19sharedFolderAddTeamStatus\x18\x04 \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12N\n\x1esharedFolderUpdateRecordStatus\x18\x05 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12J\n\x1csharedFolderUpdateUserStatus\x18\x06 \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12J\n\x1csharedFolderUpdateTeamStatus\x18\x07 \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12N\n\x1esharedFolderRemoveRecordStatus\x18\x08 \x03(\x0b\x32&.Folder.SharedFolderUpdateRecordStatus\x12J\n\x1csharedFolderRemoveUserStatus\x18\t \x03(\x0b\x32$.Folder.SharedFolderUpdateUserStatus\x12J\n\x1csharedFolderRemoveTeamStatus\x18\n \x03(\x0b\x32$.Folder.SharedFolderUpdateTeamStatus\x12\x17\n\x0fsharedFolderUid\x18\x0c \x01(\x0c\x12\x0e\n\x06status\x18\r \x01(\t\"m\n\x1eSharedFolderUpdateV3ResponseV2\x12K\n\x1dsharedFoldersUpdateV3Response\x18\x01 \x03(\x0b\x32$.Folder.SharedFolderUpdateV3Response\"\xfa\x01\n)GetDeletedSharedFoldersAndRecordsResponse\x12\x32\n\rsharedFolders\x18\x01 \x03(\x0b\x32\x1b.Folder.DeletedSharedFolder\x12>\n\x13sharedFolderRecords\x18\x02 \x03(\x0b\x32!.Folder.DeletedSharedFolderRecord\x12\x34\n\x11\x64\x65letedRecordData\x18\x03 \x03(\x0b\x32\x19.Folder.DeletedRecordData\x12#\n\tusernames\x18\x04 \x03(\x0b\x32\x10.Folder.Username\"\xd1\x01\n\x13\x44\x65letedSharedFolder\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\x17\n\x0fsharedFolderKey\x18\x04 \x01(\x0c\x12-\n\rfolderKeyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x13\n\x0b\x64\x61teDeleted\x18\x07 \x01(\x03\x12\x10\n\x08revision\x18\x08 \x01(\x03\"\x81\x01\n\x19\x44\x65letedSharedFolderRecord\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x17\n\x0fsharedRecordKey\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64\x61teDeleted\x18\x04 \x01(\x03\x12\x10\n\x08revision\x18\x05 \x01(\x03\"\x85\x01\n\x11\x44\x65letedRecordData\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08ownerUid\x18\x02 \x01(\x0c\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x1a\n\x12\x63lientModifiedTime\x18\x04 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\x12\x0f\n\x07version\x18\x06 \x01(\x05\"0\n\x08Username\x12\x12\n\naccountUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\"\x8a\x01\n,RestoreDeletedSharedFoldersAndRecordsRequest\x12,\n\x07\x66olders\x18\x01 \x03(\x0b\x32\x1b.Folder.RestoreSharedObject\x12,\n\x07records\x18\x02 \x03(\x0b\x32\x1b.Folder.RestoreSharedObject\"<\n\x13RestoreSharedObject\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x12\n\nrecordUids\x18\x02 \x03(\x0c\"\x83\x02\n\nFolderData\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12%\n\x04type\x18\x04 \x01(\x0e\x32\x17.Folder.FolderUsageType\x12\x37\n\x16inheritUserPermissions\x18\x05 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x11\n\tfolderKey\x18\x06 \x01(\x0c\x12#\n\townerInfo\x18\x07 \x01(\x0b\x32\x10.Folder.UserInfo\x12\x13\n\x0b\x64\x61teCreated\x18\x08 \x01(\x03\x12\x14\n\x0clastModified\x18\t \x01(\x03\"z\n\tFolderKey\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x11\n\tfolderKey\x18\x03 \x01(\x0c\x12\x34\n\x0b\x65ncryptedBy\x18\x04 \x01(\x0e\x32\x1f.Folder.FolderKeyEncryptionType\":\n\x10\x46olderAddRequest\x12&\n\nfolderData\x18\x01 \x03(\x0b\x32\x12.Folder.FolderData\"d\n\x12\x46olderModifyResult\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.Folder.FolderModifyStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"I\n\x11\x46olderAddResponse\x12\x34\n\x10\x66olderAddResults\x18\x01 \x03(\x0b\x32\x1a.Folder.FolderModifyResult\"=\n\x13\x46olderUpdateRequest\x12&\n\nfolderData\x18\x01 \x03(\x0b\x32\x12.Folder.FolderData\"O\n\x14\x46olderUpdateResponse\x12\x37\n\x13\x66olderUpdateResults\x18\x01 \x03(\x0b\x32\x1a.Folder.FolderModifyResult\"\xc3\x02\n\x11\x46olderPermissions\x12\x0e\n\x06\x63\x61nAdd\x18\x01 \x01(\x08\x12\x11\n\tcanRemove\x18\x02 \x01(\x08\x12\x11\n\tcanDelete\x18\x03 \x01(\x08\x12\x15\n\rcanListAccess\x18\x04 \x01(\x08\x12\x17\n\x0f\x63\x61nUpdateAccess\x18\x05 \x01(\x08\x12\x1a\n\x12\x63\x61nChangeOwnership\x18\x06 \x01(\x08\x12\x16\n\x0e\x63\x61nEditRecords\x18\x07 \x01(\x08\x12\x16\n\x0e\x63\x61nViewRecords\x18\x08 \x01(\x08\x12\x18\n\x10\x63\x61nApproveAccess\x18\t \x01(\x08\x12\x18\n\x10\x63\x61nRequestAccess\x18\n \x01(\x08\x12\x18\n\x10\x63\x61nUpdateSetting\x18\x0b \x01(\x08\x12\x16\n\x0e\x63\x61nListRecords\x18\x0c \x01(\x08\x12\x16\n\x0e\x63\x61nListFolders\x18\r \x01(\x08\"\x83\x05\n\x0c\x43\x61pabilities\x12\'\n\x06\x63\x61nAdd\x18\x01 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12*\n\tcanRemove\x18\x02 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12*\n\tcanDelete\x18\x03 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12.\n\rcanListAccess\x18\x04 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x30\n\x0f\x63\x61nUpdateAccess\x18\x05 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x33\n\x12\x63\x61nChangeOwnership\x18\x06 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x63\x61nEditRecords\x18\x07 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x63\x61nViewRecords\x18\x08 \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x31\n\x10\x63\x61nApproveAccess\x18\t \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x31\n\x10\x63\x61nRequestAccess\x18\n \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12\x31\n\x10\x63\x61nUpdateSetting\x18\x0b \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x63\x61nListRecords\x18\x0c \x01(\x0e\x32\x17.Folder.SetBooleanValue\x12/\n\x0e\x63\x61nListFolders\x18\r \x01(\x0e\x32\x17.Folder.SetBooleanValue\"\xb8\x01\n\x19\x46olderRecordUpdateRequest\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12*\n\naddRecords\x18\x02 \x03(\x0b\x32\x16.Folder.RecordMetadata\x12-\n\rupdateRecords\x18\x03 \x03(\x0b\x32\x16.Folder.RecordMetadata\x12-\n\rremoveRecords\x18\x04 \x03(\x0b\x32\x16.Folder.RecordMetadata\"\xab\x01\n\x0eRecordMetadata\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x1a\n\x12\x65ncryptedRecordKey\x18\x02 \x01(\x0c\x12\x38\n\x16\x65ncryptedRecordKeyType\x18\x03 \x01(\x0e\x32\x18.Folder.EncryptedKeyType\x12\x30\n\rtlaProperties\x18\x05 \x01(\x0b\x32\x19.common.tla.TLAProperties\"\x93\x01\n\x0c\x46olderRecord\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12.\n\x0erecordMetadata\x18\x02 \x01(\x0b\x32\x16.Folder.RecordMetadata\x12@\n\x17\x66olderKeyEncryptionType\x18\x03 \x01(\x0e\x32\x1f.Folder.FolderKeyEncryptionType\"s\n\x1a\x46olderRecordUpdateResponse\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x42\n\x18\x66olderRecordUpdateResult\x18\x04 \x03(\x0b\x32 .Folder.FolderRecordUpdateResult\"j\n\x18\x46olderRecordUpdateResult\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.Folder.FolderModifyStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\"\x87\x03\n\x10\x46olderAccessData\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x15\n\raccessTypeUid\x18\x02 \x01(\x0c\x12&\n\naccessType\x18\x03 \x01(\x0e\x32\x12.Folder.AccessType\x12.\n\x0e\x61\x63\x63\x65ssRoleType\x18\x04 \x01(\x0e\x32\x16.Folder.AccessRoleType\x12+\n\tfolderKey\x18\x05 \x01(\x0b\x32\x18.Folder.EncryptedDataKey\x12\x11\n\tinherited\x18\x06 \x01(\x08\x12\x0e\n\x06hidden\x18\x07 \x01(\x08\x12.\n\x0bpermissions\x18\x08 \x01(\x0b\x32\x19.Folder.FolderPermissions\x12\x30\n\rtlaProperties\x18\t \x01(\x0b\x32\x19.common.tla.TLAProperties\x12\x13\n\x0b\x64\x61teCreated\x18\n \x01(\x03\x12\x14\n\x0clastModified\x18\x0b \x01(\x03\x12\x14\n\x0c\x64\x65niedAccess\x18\x0c \x01(\x08\"\\\n\rRevokedAccess\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x61\x63torUid\x18\x02 \x01(\x0c\x12&\n\naccessType\x18\x03 \x01(\x0e\x32\x12.Folder.AccessType\"#\n\rFolderRemoved\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\"\x93\x04\n\x10RecordAccessData\x12\x15\n\raccessTypeUid\x18\x01 \x01(\x0c\x12&\n\naccessType\x18\x02 \x01(\x0e\x32\x12.Folder.AccessType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12.\n\x0e\x61\x63\x63\x65ssRoleType\x18\x04 \x01(\x0e\x32\x16.Folder.AccessRoleType\x12\r\n\x05owner\x18\x05 \x01(\x08\x12\x11\n\tinherited\x18\x06 \x01(\x08\x12\x0e\n\x06hidden\x18\x07 \x01(\x08\x12\x14\n\x0c\x64\x65niedAccess\x18\x08 \x01(\x08\x12\x16\n\x0e\x63\x61n_view_title\x18\t \x01(\x08\x12\x10\n\x08\x63\x61n_edit\x18\n \x01(\x08\x12\x10\n\x08\x63\x61n_view\x18\x0b \x01(\x08\x12\x17\n\x0f\x63\x61n_list_access\x18\x0c \x01(\x08\x12\x19\n\x11\x63\x61n_update_access\x18\r \x01(\x08\x12\x12\n\ncan_delete\x18\x0e \x01(\x08\x12\x1c\n\x14\x63\x61n_change_ownership\x18\x0f \x01(\x08\x12\x1a\n\x12\x63\x61n_request_access\x18\x10 \x01(\x08\x12\x1a\n\x12\x63\x61n_approve_access\x18\x11 \x01(\x08\x12\x13\n\x0b\x64\x61teCreated\x18\x12 \x01(\x03\x12\x14\n\x0clastModified\x18\x13 \x01(\x03\x12\x30\n\rtlaProperties\x18\x14 \x01(\x0b\x32\x19.common.tla.TLAProperties\"\xb8\x01\n\nAccessData\x12\x15\n\raccessTypeUid\x18\x01 \x01(\x0c\x12.\n\x0e\x61\x63\x63\x65ssRoleType\x18\x02 \x01(\x0e\x32\x16.Folder.AccessRoleType\x12\x14\n\x0c\x64\x65niedAccess\x18\x03 \x01(\x08\x12\x11\n\tinherited\x18\x04 \x01(\x08\x12\x0e\n\x06hidden\x18\x05 \x01(\x08\x12*\n\x0c\x63\x61pabilities\x18\x06 \x01(\x0b\x32\x14.Folder.Capabilities\"\xb7\x01\n\x13\x46olderAccessRequest\x12\x32\n\x10\x66olderAccessAdds\x18\x01 \x03(\x0b\x32\x18.Folder.FolderAccessData\x12\x35\n\x13\x66olderAccessUpdates\x18\x02 \x03(\x0b\x32\x18.Folder.FolderAccessData\x12\x35\n\x13\x66olderAccessRemoves\x18\x03 \x03(\x0b\x32\x18.Folder.FolderAccessData\"\x9f\x01\n\x12\x46olderAccessResult\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x11\n\taccessUid\x18\x02 \x01(\x0c\x12&\n\naccessType\x18\x03 \x01(\x0e\x32\x12.Folder.AccessType\x12*\n\x06status\x18\x04 \x01(\x0e\x32\x1a.Folder.FolderModifyStatus\x12\x0f\n\x07message\x18\x05 \x01(\t\"O\n\x14\x46olderAccessResponse\x12\x37\n\x13\x66olderAccessResults\x18\x01 \x03(\x0b\x32\x1a.Folder.FolderAccessResult\"0\n\x08UserInfo\x12\x12\n\naccountUid\x18\x01 \x01(\x0c\x12\x10\n\x08username\x18\x02 \x01(\t\"M\n\nRecordData\x12\x1e\n\x04user\x18\x01 \x01(\x0b\x32\x10.Folder.UserInfo\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\"{\n\tRecordKey\x12\x10\n\x08user_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_uid\x18\x02 \x01(\x0c\x12\x12\n\nrecord_key\x18\x03 \x01(\x0c\x12\x34\n\x12\x65ncrypted_key_type\x18\x04 \x01(\x0e\x32\x18.Folder.EncryptedKeyType*\x1a\n\nRecordType\x12\x0c\n\x08password\x10\x00*^\n\nFolderType\x12\x12\n\x0e\x64\x65\x66\x61ult_folder\x10\x00\x12\x0f\n\x0buser_folder\x10\x01\x12\x11\n\rshared_folder\x10\x02\x12\x18\n\x14shared_folder_folder\x10\x03*\x96\x01\n\x10\x45ncryptedKeyType\x12\n\n\x06no_key\x10\x00\x12\x19\n\x15\x65ncrypted_by_data_key\x10\x01\x12\x1b\n\x17\x65ncrypted_by_public_key\x10\x02\x12\x1d\n\x19\x65ncrypted_by_data_key_gcm\x10\x03\x12\x1f\n\x1b\x65ncrypted_by_public_key_ecc\x10\x04*M\n\x0fSetBooleanValue\x12\x15\n\x11\x42OOLEAN_NO_CHANGE\x10\x00\x12\x10\n\x0c\x42OOLEAN_TRUE\x10\x01\x12\x11\n\rBOOLEAN_FALSE\x10\x02*R\n\x0f\x46olderUsageType\x12\x0e\n\nUT_UNKNOWN\x10\x00\x12\r\n\tUT_NORMAL\x10\x01\x12\x0f\n\x0bUT_WORKFLOW\x10\x02\x12\x0f\n\x0bUT_TRASHCAN\x10\x03*l\n\x17\x46olderKeyEncryptionType\x12\x19\n\x15\x45NCRYPTED_BY_USER_KEY\x10\x00\x12\x1b\n\x17\x45NCRYPTED_BY_PARENT_KEY\x10\x01\x12\x19\n\x15\x45NCRYPTED_BY_TEAM_KEY\x10\x02*T\n\x12\x46olderModifyStatus\x12\x0b\n\x07SUCCESS\x10\x00\x12\x0f\n\x0b\x42\x41\x44_REQUEST\x10\x01\x12\x11\n\rACCESS_DENIED\x10\x02\x12\r\n\tNOT_FOUND\x10\x03*\xa4\x02\n\x14\x46olderPermissionBits\x12\n\n\x06noBits\x10\x00\x12\n\n\x06\x63\x61nAdd\x10\x01\x12\r\n\tcanRemove\x10\x02\x12\r\n\tcanDelete\x10\x04\x12\x11\n\rcanListAccess\x10\x08\x12\x13\n\x0f\x63\x61nUpdateAccess\x10\x10\x12\x16\n\x12\x63\x61nChangeOwnership\x10 \x12\x12\n\x0e\x63\x61nEditRecords\x10@\x12\x13\n\x0e\x63\x61nViewRecords\x10\x80\x01\x12\x15\n\x10\x63\x61nApproveAccess\x10\x80\x02\x12\x15\n\x10\x63\x61nRequestAccess\x10\x80\x04\x12\x15\n\x10\x63\x61nUpdateSetting\x10\x80\x08\x12\x13\n\x0e\x63\x61nListRecords\x10\x80\x10\x12\x13\n\x0e\x63\x61nListFolders\x10\x80 *\x9b\x01\n\x0e\x41\x63\x63\x65ssRoleType\x12\r\n\tNAVIGATOR\x10\x00\x12\r\n\tREQUESTOR\x10\x01\x12\n\n\x06VIEWER\x10\x02\x12\x12\n\x0eSHARED_MANAGER\x10\x03\x12\x13\n\x0f\x43ONTENT_MANAGER\x10\x04\x12\x19\n\x15\x43ONTENT_SHARE_MANAGER\x10\x05\x12\x0b\n\x07MANAGER\x10\x06\x12\x0e\n\nUNRESOLVED\x10\x07*z\n\nAccessType\x12\x0e\n\nAT_UNKNOWN\x10\x00\x12\x0c\n\x08\x41T_OWNER\x10\x01\x12\x0b\n\x07\x41T_USER\x10\x02\x12\x0b\n\x07\x41T_TEAM\x10\x03\x12\x11\n\rAT_ENTERPRISE\x10\x04\x12\r\n\tAT_FOLDER\x10\x05\x12\x12\n\x0e\x41T_APPLICATION\x10\x06*:\n\nObjectType\x12\x0e\n\nOT_UNKNOWN\x10\x00\x12\r\n\tOT_RECORD\x10\x01\x12\r\n\tOT_FOLDER\x10\x02\x42\"\n\x18\x63om.keepersecurity.protoB\x06\x46olderb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -37,72 +38,136 @@ _globals['_SHAREDFOLDERUPDATEUSER'].fields_by_name['sharedFolderKey']._serialized_options = b'\030\001' _globals['_SHAREDFOLDERUPDATETEAM'].fields_by_name['sharedFolderKey']._loaded_options = None _globals['_SHAREDFOLDERUPDATETEAM'].fields_by_name['sharedFolderKey']._serialized_options = b'\030\001' - _globals['_RECORDTYPE']._serialized_start=5728 - _globals['_RECORDTYPE']._serialized_end=5754 - _globals['_FOLDERTYPE']._serialized_start=5756 - _globals['_FOLDERTYPE']._serialized_end=5850 - _globals['_ENCRYPTEDKEYTYPE']._serialized_start=5853 - _globals['_ENCRYPTEDKEYTYPE']._serialized_end=6003 - _globals['_SETBOOLEANVALUE']._serialized_start=6005 - _globals['_SETBOOLEANVALUE']._serialized_end=6082 - _globals['_ENCRYPTEDDATAKEY']._serialized_start=38 - _globals['_ENCRYPTEDDATAKEY']._serialized_end=130 - _globals['_SHAREDFOLDERRECORDDATA']._serialized_start=133 - _globals['_SHAREDFOLDERRECORDDATA']._serialized_end=263 - _globals['_SHAREDFOLDERRECORDDATALIST']._serialized_start=265 - _globals['_SHAREDFOLDERRECORDDATALIST']._serialized_end=357 - _globals['_SHAREDFOLDERRECORDFIX']._serialized_start=359 - _globals['_SHAREDFOLDERRECORDFIX']._serialized_end=454 - _globals['_SHAREDFOLDERRECORDFIXLIST']._serialized_start=456 - _globals['_SHAREDFOLDERRECORDFIXLIST']._serialized_end=545 - _globals['_RECORDREQUEST']._serialized_start=548 - _globals['_RECORDREQUEST']._serialized_end=838 - _globals['_RECORDRESPONSE']._serialized_start=840 - _globals['_RECORDRESPONSE']._serialized_end=909 - _globals['_SHAREDFOLDERFIELDS']._serialized_start=912 - _globals['_SHAREDFOLDERFIELDS']._serialized_end=1040 - _globals['_SHAREDFOLDERFOLDERFIELDS']._serialized_start=1042 - _globals['_SHAREDFOLDERFOLDERFIELDS']._serialized_end=1093 - _globals['_FOLDERREQUEST']._serialized_start=1096 - _globals['_FOLDERREQUEST']._serialized_end=1367 - _globals['_FOLDERRESPONSE']._serialized_start=1369 - _globals['_FOLDERRESPONSE']._serialized_end=1438 - _globals['_IMPORTFOLDERRECORDREQUEST']._serialized_start=1440 - _globals['_IMPORTFOLDERRECORDREQUEST']._serialized_end=1559 - _globals['_IMPORTFOLDERRECORDRESPONSE']._serialized_start=1561 - _globals['_IMPORTFOLDERRECORDRESPONSE']._serialized_end=1685 - _globals['_SHAREDFOLDERUPDATERECORD']._serialized_start=1688 - _globals['_SHAREDFOLDERUPDATERECORD']._serialized_end=2017 - _globals['_SHAREDFOLDERUPDATEUSER']._serialized_start=2020 - _globals['_SHAREDFOLDERUPDATEUSER']._serialized_end=2352 - _globals['_SHAREDFOLDERUPDATETEAM']._serialized_start=2355 - _globals['_SHAREDFOLDERUPDATETEAM']._serialized_end=2636 - _globals['_SHAREDFOLDERUPDATEV3REQUEST']._serialized_start=2639 - _globals['_SHAREDFOLDERUPDATEV3REQUEST']._serialized_end=3549 - _globals['_SHAREDFOLDERUPDATEV3REQUESTV2']._serialized_start=3551 - _globals['_SHAREDFOLDERUPDATEV3REQUESTV2']._serialized_end=3650 - _globals['_SHAREDFOLDERUPDATERECORDSTATUS']._serialized_start=3652 - _globals['_SHAREDFOLDERUPDATERECORDSTATUS']._serialized_end=3719 - _globals['_SHAREDFOLDERUPDATEUSERSTATUS']._serialized_start=3721 - _globals['_SHAREDFOLDERUPDATEUSERSTATUS']._serialized_end=3785 - _globals['_SHAREDFOLDERUPDATETEAMSTATUS']._serialized_start=3787 - _globals['_SHAREDFOLDERUPDATETEAMSTATUS']._serialized_end=3850 - _globals['_SHAREDFOLDERUPDATEV3RESPONSE']._serialized_start=3853 - _globals['_SHAREDFOLDERUPDATEV3RESPONSE']._serialized_end=4629 - _globals['_SHAREDFOLDERUPDATEV3RESPONSEV2']._serialized_start=4631 - _globals['_SHAREDFOLDERUPDATEV3RESPONSEV2']._serialized_end=4740 - _globals['_GETDELETEDSHAREDFOLDERSANDRECORDSRESPONSE']._serialized_start=4743 - _globals['_GETDELETEDSHAREDFOLDERSANDRECORDSRESPONSE']._serialized_end=4993 - _globals['_DELETEDSHAREDFOLDER']._serialized_start=4996 - _globals['_DELETEDSHAREDFOLDER']._serialized_end=5205 - _globals['_DELETEDSHAREDFOLDERRECORD']._serialized_start=5208 - _globals['_DELETEDSHAREDFOLDERRECORD']._serialized_end=5337 - _globals['_DELETEDRECORDDATA']._serialized_start=5340 - _globals['_DELETEDRECORDDATA']._serialized_end=5473 - _globals['_USERNAME']._serialized_start=5475 - _globals['_USERNAME']._serialized_end=5523 - _globals['_RESTOREDELETEDSHAREDFOLDERSANDRECORDSREQUEST']._serialized_start=5526 - _globals['_RESTOREDELETEDSHAREDFOLDERSANDRECORDSREQUEST']._serialized_end=5664 - _globals['_RESTORESHAREDOBJECT']._serialized_start=5666 - _globals['_RESTORESHAREDOBJECT']._serialized_end=5726 + _globals['_RECORDTYPE']._serialized_start=10143 + _globals['_RECORDTYPE']._serialized_end=10169 + _globals['_FOLDERTYPE']._serialized_start=10171 + _globals['_FOLDERTYPE']._serialized_end=10265 + _globals['_ENCRYPTEDKEYTYPE']._serialized_start=10268 + _globals['_ENCRYPTEDKEYTYPE']._serialized_end=10418 + _globals['_SETBOOLEANVALUE']._serialized_start=10420 + _globals['_SETBOOLEANVALUE']._serialized_end=10497 + _globals['_FOLDERUSAGETYPE']._serialized_start=10499 + _globals['_FOLDERUSAGETYPE']._serialized_end=10581 + _globals['_FOLDERKEYENCRYPTIONTYPE']._serialized_start=10583 + _globals['_FOLDERKEYENCRYPTIONTYPE']._serialized_end=10691 + _globals['_FOLDERMODIFYSTATUS']._serialized_start=10693 + _globals['_FOLDERMODIFYSTATUS']._serialized_end=10777 + _globals['_FOLDERPERMISSIONBITS']._serialized_start=10780 + _globals['_FOLDERPERMISSIONBITS']._serialized_end=11072 + _globals['_ACCESSROLETYPE']._serialized_start=11075 + _globals['_ACCESSROLETYPE']._serialized_end=11230 + _globals['_ACCESSTYPE']._serialized_start=11232 + _globals['_ACCESSTYPE']._serialized_end=11354 + _globals['_OBJECTTYPE']._serialized_start=11356 + _globals['_OBJECTTYPE']._serialized_end=11414 + _globals['_ENCRYPTEDDATAKEY']._serialized_start=49 + _globals['_ENCRYPTEDDATAKEY']._serialized_end=141 + _globals['_SHAREDFOLDERRECORDDATA']._serialized_start=144 + _globals['_SHAREDFOLDERRECORDDATA']._serialized_end=274 + _globals['_SHAREDFOLDERRECORDDATALIST']._serialized_start=276 + _globals['_SHAREDFOLDERRECORDDATALIST']._serialized_end=368 + _globals['_SHAREDFOLDERRECORDFIX']._serialized_start=370 + _globals['_SHAREDFOLDERRECORDFIX']._serialized_end=465 + _globals['_SHAREDFOLDERRECORDFIXLIST']._serialized_start=467 + _globals['_SHAREDFOLDERRECORDFIXLIST']._serialized_end=556 + _globals['_RECORDREQUEST']._serialized_start=559 + _globals['_RECORDREQUEST']._serialized_end=849 + _globals['_RECORDRESPONSE']._serialized_start=851 + _globals['_RECORDRESPONSE']._serialized_end=920 + _globals['_SHAREDFOLDERFIELDS']._serialized_start=923 + _globals['_SHAREDFOLDERFIELDS']._serialized_end=1051 + _globals['_SHAREDFOLDERFOLDERFIELDS']._serialized_start=1053 + _globals['_SHAREDFOLDERFOLDERFIELDS']._serialized_end=1104 + _globals['_FOLDERREQUEST']._serialized_start=1107 + _globals['_FOLDERREQUEST']._serialized_end=1378 + _globals['_FOLDERRESPONSE']._serialized_start=1380 + _globals['_FOLDERRESPONSE']._serialized_end=1449 + _globals['_IMPORTFOLDERRECORDREQUEST']._serialized_start=1451 + _globals['_IMPORTFOLDERRECORDREQUEST']._serialized_end=1570 + _globals['_IMPORTFOLDERRECORDRESPONSE']._serialized_start=1572 + _globals['_IMPORTFOLDERRECORDRESPONSE']._serialized_end=1696 + _globals['_SHAREDFOLDERUPDATERECORD']._serialized_start=1699 + _globals['_SHAREDFOLDERUPDATERECORD']._serialized_end=2028 + _globals['_SHAREDFOLDERUPDATEUSER']._serialized_start=2031 + _globals['_SHAREDFOLDERUPDATEUSER']._serialized_end=2363 + _globals['_SHAREDFOLDERUPDATETEAM']._serialized_start=2366 + _globals['_SHAREDFOLDERUPDATETEAM']._serialized_end=2647 + _globals['_SHAREDFOLDERUPDATEV3REQUEST']._serialized_start=2650 + _globals['_SHAREDFOLDERUPDATEV3REQUEST']._serialized_end=3560 + _globals['_SHAREDFOLDERUPDATEV3REQUESTV2']._serialized_start=3562 + _globals['_SHAREDFOLDERUPDATEV3REQUESTV2']._serialized_end=3661 + _globals['_SHAREDFOLDERUPDATERECORDSTATUS']._serialized_start=3663 + _globals['_SHAREDFOLDERUPDATERECORDSTATUS']._serialized_end=3730 + _globals['_SHAREDFOLDERUPDATEUSERSTATUS']._serialized_start=3732 + _globals['_SHAREDFOLDERUPDATEUSERSTATUS']._serialized_end=3796 + _globals['_SHAREDFOLDERUPDATETEAMSTATUS']._serialized_start=3798 + _globals['_SHAREDFOLDERUPDATETEAMSTATUS']._serialized_end=3861 + _globals['_SHAREDFOLDERUPDATEV3RESPONSE']._serialized_start=3864 + _globals['_SHAREDFOLDERUPDATEV3RESPONSE']._serialized_end=4640 + _globals['_SHAREDFOLDERUPDATEV3RESPONSEV2']._serialized_start=4642 + _globals['_SHAREDFOLDERUPDATEV3RESPONSEV2']._serialized_end=4751 + _globals['_GETDELETEDSHAREDFOLDERSANDRECORDSRESPONSE']._serialized_start=4754 + _globals['_GETDELETEDSHAREDFOLDERSANDRECORDSRESPONSE']._serialized_end=5004 + _globals['_DELETEDSHAREDFOLDER']._serialized_start=5007 + _globals['_DELETEDSHAREDFOLDER']._serialized_end=5216 + _globals['_DELETEDSHAREDFOLDERRECORD']._serialized_start=5219 + _globals['_DELETEDSHAREDFOLDERRECORD']._serialized_end=5348 + _globals['_DELETEDRECORDDATA']._serialized_start=5351 + _globals['_DELETEDRECORDDATA']._serialized_end=5484 + _globals['_USERNAME']._serialized_start=5486 + _globals['_USERNAME']._serialized_end=5534 + _globals['_RESTOREDELETEDSHAREDFOLDERSANDRECORDSREQUEST']._serialized_start=5537 + _globals['_RESTOREDELETEDSHAREDFOLDERSANDRECORDSREQUEST']._serialized_end=5675 + _globals['_RESTORESHAREDOBJECT']._serialized_start=5677 + _globals['_RESTORESHAREDOBJECT']._serialized_end=5737 + _globals['_FOLDERDATA']._serialized_start=5740 + _globals['_FOLDERDATA']._serialized_end=5999 + _globals['_FOLDERKEY']._serialized_start=6001 + _globals['_FOLDERKEY']._serialized_end=6123 + _globals['_FOLDERADDREQUEST']._serialized_start=6125 + _globals['_FOLDERADDREQUEST']._serialized_end=6183 + _globals['_FOLDERMODIFYRESULT']._serialized_start=6185 + _globals['_FOLDERMODIFYRESULT']._serialized_end=6285 + _globals['_FOLDERADDRESPONSE']._serialized_start=6287 + _globals['_FOLDERADDRESPONSE']._serialized_end=6360 + _globals['_FOLDERUPDATEREQUEST']._serialized_start=6362 + _globals['_FOLDERUPDATEREQUEST']._serialized_end=6423 + _globals['_FOLDERUPDATERESPONSE']._serialized_start=6425 + _globals['_FOLDERUPDATERESPONSE']._serialized_end=6504 + _globals['_FOLDERPERMISSIONS']._serialized_start=6507 + _globals['_FOLDERPERMISSIONS']._serialized_end=6830 + _globals['_CAPABILITIES']._serialized_start=6833 + _globals['_CAPABILITIES']._serialized_end=7476 + _globals['_FOLDERRECORDUPDATEREQUEST']._serialized_start=7479 + _globals['_FOLDERRECORDUPDATEREQUEST']._serialized_end=7663 + _globals['_RECORDMETADATA']._serialized_start=7666 + _globals['_RECORDMETADATA']._serialized_end=7837 + _globals['_FOLDERRECORD']._serialized_start=7840 + _globals['_FOLDERRECORD']._serialized_end=7987 + _globals['_FOLDERRECORDUPDATERESPONSE']._serialized_start=7989 + _globals['_FOLDERRECORDUPDATERESPONSE']._serialized_end=8104 + _globals['_FOLDERRECORDUPDATERESULT']._serialized_start=8106 + _globals['_FOLDERRECORDUPDATERESULT']._serialized_end=8212 + _globals['_FOLDERACCESSDATA']._serialized_start=8215 + _globals['_FOLDERACCESSDATA']._serialized_end=8606 + _globals['_REVOKEDACCESS']._serialized_start=8608 + _globals['_REVOKEDACCESS']._serialized_end=8700 + _globals['_FOLDERREMOVED']._serialized_start=8702 + _globals['_FOLDERREMOVED']._serialized_end=8737 + _globals['_RECORDACCESSDATA']._serialized_start=8740 + _globals['_RECORDACCESSDATA']._serialized_end=9271 + _globals['_ACCESSDATA']._serialized_start=9274 + _globals['_ACCESSDATA']._serialized_end=9458 + _globals['_FOLDERACCESSREQUEST']._serialized_start=9461 + _globals['_FOLDERACCESSREQUEST']._serialized_end=9644 + _globals['_FOLDERACCESSRESULT']._serialized_start=9647 + _globals['_FOLDERACCESSRESULT']._serialized_end=9806 + _globals['_FOLDERACCESSRESPONSE']._serialized_start=9808 + _globals['_FOLDERACCESSRESPONSE']._serialized_end=9887 + _globals['_USERINFO']._serialized_start=9889 + _globals['_USERINFO']._serialized_end=9937 + _globals['_RECORDDATA']._serialized_start=9939 + _globals['_RECORDDATA']._serialized_end=10016 + _globals['_RECORDKEY']._serialized_start=10018 + _globals['_RECORDKEY']._serialized_end=10141 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi b/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi index 1f4fb01d..57a85797 100644 --- a/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/folder_pb2.pyi @@ -1,10 +1,10 @@ import record_pb2 as _record_pb2 +import tla_pb2 as _tla_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 from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -32,6 +32,70 @@ class SetBooleanValue(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): BOOLEAN_NO_CHANGE: _ClassVar[SetBooleanValue] BOOLEAN_TRUE: _ClassVar[SetBooleanValue] BOOLEAN_FALSE: _ClassVar[SetBooleanValue] + +class FolderUsageType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + UT_UNKNOWN: _ClassVar[FolderUsageType] + UT_NORMAL: _ClassVar[FolderUsageType] + UT_WORKFLOW: _ClassVar[FolderUsageType] + UT_TRASHCAN: _ClassVar[FolderUsageType] + +class FolderKeyEncryptionType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ENCRYPTED_BY_USER_KEY: _ClassVar[FolderKeyEncryptionType] + ENCRYPTED_BY_PARENT_KEY: _ClassVar[FolderKeyEncryptionType] + ENCRYPTED_BY_TEAM_KEY: _ClassVar[FolderKeyEncryptionType] + +class FolderModifyStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + SUCCESS: _ClassVar[FolderModifyStatus] + BAD_REQUEST: _ClassVar[FolderModifyStatus] + ACCESS_DENIED: _ClassVar[FolderModifyStatus] + NOT_FOUND: _ClassVar[FolderModifyStatus] + +class FolderPermissionBits(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + noBits: _ClassVar[FolderPermissionBits] + canAdd: _ClassVar[FolderPermissionBits] + canRemove: _ClassVar[FolderPermissionBits] + canDelete: _ClassVar[FolderPermissionBits] + canListAccess: _ClassVar[FolderPermissionBits] + canUpdateAccess: _ClassVar[FolderPermissionBits] + canChangeOwnership: _ClassVar[FolderPermissionBits] + canEditRecords: _ClassVar[FolderPermissionBits] + canViewRecords: _ClassVar[FolderPermissionBits] + canApproveAccess: _ClassVar[FolderPermissionBits] + canRequestAccess: _ClassVar[FolderPermissionBits] + canUpdateSetting: _ClassVar[FolderPermissionBits] + canListRecords: _ClassVar[FolderPermissionBits] + canListFolders: _ClassVar[FolderPermissionBits] + +class AccessRoleType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + NAVIGATOR: _ClassVar[AccessRoleType] + REQUESTOR: _ClassVar[AccessRoleType] + VIEWER: _ClassVar[AccessRoleType] + SHARED_MANAGER: _ClassVar[AccessRoleType] + CONTENT_MANAGER: _ClassVar[AccessRoleType] + CONTENT_SHARE_MANAGER: _ClassVar[AccessRoleType] + MANAGER: _ClassVar[AccessRoleType] + UNRESOLVED: _ClassVar[AccessRoleType] + +class AccessType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + AT_UNKNOWN: _ClassVar[AccessType] + AT_OWNER: _ClassVar[AccessType] + AT_USER: _ClassVar[AccessType] + AT_TEAM: _ClassVar[AccessType] + AT_ENTERPRISE: _ClassVar[AccessType] + AT_FOLDER: _ClassVar[AccessType] + AT_APPLICATION: _ClassVar[AccessType] + +class ObjectType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + OT_UNKNOWN: _ClassVar[ObjectType] + OT_RECORD: _ClassVar[ObjectType] + OT_FOLDER: _ClassVar[ObjectType] password: RecordType default_folder: FolderType user_folder: FolderType @@ -45,6 +109,49 @@ encrypted_by_public_key_ecc: EncryptedKeyType BOOLEAN_NO_CHANGE: SetBooleanValue BOOLEAN_TRUE: SetBooleanValue BOOLEAN_FALSE: SetBooleanValue +UT_UNKNOWN: FolderUsageType +UT_NORMAL: FolderUsageType +UT_WORKFLOW: FolderUsageType +UT_TRASHCAN: FolderUsageType +ENCRYPTED_BY_USER_KEY: FolderKeyEncryptionType +ENCRYPTED_BY_PARENT_KEY: FolderKeyEncryptionType +ENCRYPTED_BY_TEAM_KEY: FolderKeyEncryptionType +SUCCESS: FolderModifyStatus +BAD_REQUEST: FolderModifyStatus +ACCESS_DENIED: FolderModifyStatus +NOT_FOUND: FolderModifyStatus +noBits: FolderPermissionBits +canAdd: FolderPermissionBits +canRemove: FolderPermissionBits +canDelete: FolderPermissionBits +canListAccess: FolderPermissionBits +canUpdateAccess: FolderPermissionBits +canChangeOwnership: FolderPermissionBits +canEditRecords: FolderPermissionBits +canViewRecords: FolderPermissionBits +canApproveAccess: FolderPermissionBits +canRequestAccess: FolderPermissionBits +canUpdateSetting: FolderPermissionBits +canListRecords: FolderPermissionBits +canListFolders: FolderPermissionBits +NAVIGATOR: AccessRoleType +REQUESTOR: AccessRoleType +VIEWER: AccessRoleType +SHARED_MANAGER: AccessRoleType +CONTENT_MANAGER: AccessRoleType +CONTENT_SHARE_MANAGER: AccessRoleType +MANAGER: AccessRoleType +UNRESOLVED: AccessRoleType +AT_UNKNOWN: AccessType +AT_OWNER: AccessType +AT_USER: AccessType +AT_TEAM: AccessType +AT_ENTERPRISE: AccessType +AT_FOLDER: AccessType +AT_APPLICATION: AccessType +OT_UNKNOWN: ObjectType +OT_RECORD: ObjectType +OT_FOLDER: ObjectType class EncryptedDataKey(_message.Message): __slots__ = ("encryptedKey", "encryptedKeyType") @@ -136,7 +243,7 @@ class SharedFolderFields(_message.Message): manageRecords: bool canEdit: bool canShare: bool - def __init__(self, encryptedFolderName: _Optional[bytes] = ..., manageUsers: _Optional[bool] = ..., manageRecords: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., canShare: _Optional[bool] = ...) -> None: ... + def __init__(self, encryptedFolderName: _Optional[bytes] = ..., manageUsers: bool = ..., manageRecords: bool = ..., canEdit: bool = ..., canShare: bool = ...) -> None: ... class SharedFolderFolderFields(_message.Message): __slots__ = ("sharedFolderUid",) @@ -210,7 +317,7 @@ class SharedFolderUpdateRecord(_message.Message): expiration: int timerNotificationType: _record_pb2.TimerNotificationType rotateOnExpiration: bool - def __init__(self, recordUid: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., canEdit: _Optional[_Union[SetBooleanValue, str]] = ..., canShare: _Optional[_Union[SetBooleanValue, str]] = ..., encryptedRecordKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., canEdit: _Optional[_Union[SetBooleanValue, str]] = ..., canShare: _Optional[_Union[SetBooleanValue, str]] = ..., encryptedRecordKey: _Optional[bytes] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderUpdateUser(_message.Message): __slots__ = ("username", "manageUsers", "manageRecords", "sharedFolderKey", "expiration", "timerNotificationType", "typedSharedFolderKey", "rotateOnExpiration") @@ -230,7 +337,7 @@ class SharedFolderUpdateUser(_message.Message): timerNotificationType: _record_pb2.TimerNotificationType typedSharedFolderKey: EncryptedDataKey rotateOnExpiration: bool - def __init__(self, username: _Optional[str] = ..., manageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., manageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., manageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., manageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderUpdateTeam(_message.Message): __slots__ = ("teamUid", "manageUsers", "manageRecords", "sharedFolderKey", "expiration", "timerNotificationType", "typedSharedFolderKey", "rotateOnExpiration") @@ -250,7 +357,7 @@ class SharedFolderUpdateTeam(_message.Message): timerNotificationType: _record_pb2.TimerNotificationType typedSharedFolderKey: EncryptedDataKey rotateOnExpiration: bool - def __init__(self, teamUid: _Optional[bytes] = ..., manageUsers: _Optional[bool] = ..., manageRecords: _Optional[bool] = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, teamUid: _Optional[bytes] = ..., manageUsers: bool = ..., manageRecords: bool = ..., sharedFolderKey: _Optional[bytes] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[_record_pb2.TimerNotificationType, str]] = ..., typedSharedFolderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderUpdateV3Request(_message.Message): __slots__ = ("sharedFolderUpdateOperation_dont_use", "sharedFolderUid", "encryptedSharedFolderName", "revision", "forceUpdate", "fromTeamUid", "defaultManageUsers", "defaultManageRecords", "defaultCanEdit", "defaultCanShare", "sharedFolderAddRecord", "sharedFolderAddUser", "sharedFolderAddTeam", "sharedFolderUpdateRecord", "sharedFolderUpdateUser", "sharedFolderUpdateTeam", "sharedFolderRemoveRecord", "sharedFolderRemoveUser", "sharedFolderRemoveTeam", "sharedFolderOwner") @@ -294,7 +401,7 @@ class SharedFolderUpdateV3Request(_message.Message): sharedFolderRemoveUser: _containers.RepeatedScalarFieldContainer[str] sharedFolderRemoveTeam: _containers.RepeatedScalarFieldContainer[bytes] sharedFolderOwner: str - def __init__(self, sharedFolderUpdateOperation_dont_use: _Optional[int] = ..., sharedFolderUid: _Optional[bytes] = ..., encryptedSharedFolderName: _Optional[bytes] = ..., revision: _Optional[int] = ..., forceUpdate: _Optional[bool] = ..., fromTeamUid: _Optional[bytes] = ..., defaultManageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., defaultManageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanEdit: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanShare: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderAddRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderAddUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderAddTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderUpdateRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderUpdateUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderUpdateTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderRemoveRecord: _Optional[_Iterable[bytes]] = ..., sharedFolderRemoveUser: _Optional[_Iterable[str]] = ..., sharedFolderRemoveTeam: _Optional[_Iterable[bytes]] = ..., sharedFolderOwner: _Optional[str] = ...) -> None: ... + def __init__(self, sharedFolderUpdateOperation_dont_use: _Optional[int] = ..., sharedFolderUid: _Optional[bytes] = ..., encryptedSharedFolderName: _Optional[bytes] = ..., revision: _Optional[int] = ..., forceUpdate: bool = ..., fromTeamUid: _Optional[bytes] = ..., defaultManageUsers: _Optional[_Union[SetBooleanValue, str]] = ..., defaultManageRecords: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanEdit: _Optional[_Union[SetBooleanValue, str]] = ..., defaultCanShare: _Optional[_Union[SetBooleanValue, str]] = ..., sharedFolderAddRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderAddUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderAddTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderUpdateRecord: _Optional[_Iterable[_Union[SharedFolderUpdateRecord, _Mapping]]] = ..., sharedFolderUpdateUser: _Optional[_Iterable[_Union[SharedFolderUpdateUser, _Mapping]]] = ..., sharedFolderUpdateTeam: _Optional[_Iterable[_Union[SharedFolderUpdateTeam, _Mapping]]] = ..., sharedFolderRemoveRecord: _Optional[_Iterable[bytes]] = ..., sharedFolderRemoveUser: _Optional[_Iterable[str]] = ..., sharedFolderRemoveTeam: _Optional[_Iterable[bytes]] = ..., sharedFolderOwner: _Optional[str] = ...) -> None: ... class SharedFolderUpdateV3RequestV2(_message.Message): __slots__ = ("sharedFoldersUpdateV3",) @@ -445,3 +552,347 @@ class RestoreSharedObject(_message.Message): folderUid: bytes recordUids: _containers.RepeatedScalarFieldContainer[bytes] def __init__(self, folderUid: _Optional[bytes] = ..., recordUids: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class FolderData(_message.Message): + __slots__ = ("folderUid", "parentUid", "data", "type", "inheritUserPermissions", "folderKey", "ownerInfo", "dateCreated", "lastModified") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + PARENTUID_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + TYPE_FIELD_NUMBER: _ClassVar[int] + INHERITUSERPERMISSIONS_FIELD_NUMBER: _ClassVar[int] + FOLDERKEY_FIELD_NUMBER: _ClassVar[int] + OWNERINFO_FIELD_NUMBER: _ClassVar[int] + DATECREATED_FIELD_NUMBER: _ClassVar[int] + LASTMODIFIED_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + parentUid: bytes + data: bytes + type: FolderUsageType + inheritUserPermissions: SetBooleanValue + folderKey: bytes + ownerInfo: UserInfo + dateCreated: int + lastModified: int + def __init__(self, folderUid: _Optional[bytes] = ..., parentUid: _Optional[bytes] = ..., data: _Optional[bytes] = ..., type: _Optional[_Union[FolderUsageType, str]] = ..., inheritUserPermissions: _Optional[_Union[SetBooleanValue, str]] = ..., folderKey: _Optional[bytes] = ..., ownerInfo: _Optional[_Union[UserInfo, _Mapping]] = ..., dateCreated: _Optional[int] = ..., lastModified: _Optional[int] = ...) -> None: ... + +class FolderKey(_message.Message): + __slots__ = ("folderUid", "parentUid", "folderKey", "encryptedBy") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + PARENTUID_FIELD_NUMBER: _ClassVar[int] + FOLDERKEY_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDBY_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + parentUid: bytes + folderKey: bytes + encryptedBy: FolderKeyEncryptionType + def __init__(self, folderUid: _Optional[bytes] = ..., parentUid: _Optional[bytes] = ..., folderKey: _Optional[bytes] = ..., encryptedBy: _Optional[_Union[FolderKeyEncryptionType, str]] = ...) -> None: ... + +class FolderAddRequest(_message.Message): + __slots__ = ("folderData",) + FOLDERDATA_FIELD_NUMBER: _ClassVar[int] + folderData: _containers.RepeatedCompositeFieldContainer[FolderData] + def __init__(self, folderData: _Optional[_Iterable[_Union[FolderData, _Mapping]]] = ...) -> None: ... + +class FolderModifyResult(_message.Message): + __slots__ = ("folderUid", "status", "message") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + status: FolderModifyStatus + message: str + def __init__(self, folderUid: _Optional[bytes] = ..., status: _Optional[_Union[FolderModifyStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + +class FolderAddResponse(_message.Message): + __slots__ = ("folderAddResults",) + FOLDERADDRESULTS_FIELD_NUMBER: _ClassVar[int] + folderAddResults: _containers.RepeatedCompositeFieldContainer[FolderModifyResult] + def __init__(self, folderAddResults: _Optional[_Iterable[_Union[FolderModifyResult, _Mapping]]] = ...) -> None: ... + +class FolderUpdateRequest(_message.Message): + __slots__ = ("folderData",) + FOLDERDATA_FIELD_NUMBER: _ClassVar[int] + folderData: _containers.RepeatedCompositeFieldContainer[FolderData] + def __init__(self, folderData: _Optional[_Iterable[_Union[FolderData, _Mapping]]] = ...) -> None: ... + +class FolderUpdateResponse(_message.Message): + __slots__ = ("folderUpdateResults",) + FOLDERUPDATERESULTS_FIELD_NUMBER: _ClassVar[int] + folderUpdateResults: _containers.RepeatedCompositeFieldContainer[FolderModifyResult] + def __init__(self, folderUpdateResults: _Optional[_Iterable[_Union[FolderModifyResult, _Mapping]]] = ...) -> None: ... + +class FolderPermissions(_message.Message): + __slots__ = ("canAdd", "canRemove", "canDelete", "canListAccess", "canUpdateAccess", "canChangeOwnership", "canEditRecords", "canViewRecords", "canApproveAccess", "canRequestAccess", "canUpdateSetting", "canListRecords", "canListFolders") + CANADD_FIELD_NUMBER: _ClassVar[int] + CANREMOVE_FIELD_NUMBER: _ClassVar[int] + CANDELETE_FIELD_NUMBER: _ClassVar[int] + CANLISTACCESS_FIELD_NUMBER: _ClassVar[int] + CANUPDATEACCESS_FIELD_NUMBER: _ClassVar[int] + CANCHANGEOWNERSHIP_FIELD_NUMBER: _ClassVar[int] + CANEDITRECORDS_FIELD_NUMBER: _ClassVar[int] + CANVIEWRECORDS_FIELD_NUMBER: _ClassVar[int] + CANAPPROVEACCESS_FIELD_NUMBER: _ClassVar[int] + CANREQUESTACCESS_FIELD_NUMBER: _ClassVar[int] + CANUPDATESETTING_FIELD_NUMBER: _ClassVar[int] + CANLISTRECORDS_FIELD_NUMBER: _ClassVar[int] + CANLISTFOLDERS_FIELD_NUMBER: _ClassVar[int] + canAdd: bool + canRemove: bool + canDelete: bool + canListAccess: bool + canUpdateAccess: bool + canChangeOwnership: bool + canEditRecords: bool + canViewRecords: bool + canApproveAccess: bool + canRequestAccess: bool + canUpdateSetting: bool + canListRecords: bool + canListFolders: bool + def __init__(self, canAdd: bool = ..., canRemove: bool = ..., canDelete: bool = ..., canListAccess: bool = ..., canUpdateAccess: bool = ..., canChangeOwnership: bool = ..., canEditRecords: bool = ..., canViewRecords: bool = ..., canApproveAccess: bool = ..., canRequestAccess: bool = ..., canUpdateSetting: bool = ..., canListRecords: bool = ..., canListFolders: bool = ...) -> None: ... + +class Capabilities(_message.Message): + __slots__ = ("canAdd", "canRemove", "canDelete", "canListAccess", "canUpdateAccess", "canChangeOwnership", "canEditRecords", "canViewRecords", "canApproveAccess", "canRequestAccess", "canUpdateSetting", "canListRecords", "canListFolders") + CANADD_FIELD_NUMBER: _ClassVar[int] + CANREMOVE_FIELD_NUMBER: _ClassVar[int] + CANDELETE_FIELD_NUMBER: _ClassVar[int] + CANLISTACCESS_FIELD_NUMBER: _ClassVar[int] + CANUPDATEACCESS_FIELD_NUMBER: _ClassVar[int] + CANCHANGEOWNERSHIP_FIELD_NUMBER: _ClassVar[int] + CANEDITRECORDS_FIELD_NUMBER: _ClassVar[int] + CANVIEWRECORDS_FIELD_NUMBER: _ClassVar[int] + CANAPPROVEACCESS_FIELD_NUMBER: _ClassVar[int] + CANREQUESTACCESS_FIELD_NUMBER: _ClassVar[int] + CANUPDATESETTING_FIELD_NUMBER: _ClassVar[int] + CANLISTRECORDS_FIELD_NUMBER: _ClassVar[int] + CANLISTFOLDERS_FIELD_NUMBER: _ClassVar[int] + canAdd: SetBooleanValue + canRemove: SetBooleanValue + canDelete: SetBooleanValue + canListAccess: SetBooleanValue + canUpdateAccess: SetBooleanValue + canChangeOwnership: SetBooleanValue + canEditRecords: SetBooleanValue + canViewRecords: SetBooleanValue + canApproveAccess: SetBooleanValue + canRequestAccess: SetBooleanValue + canUpdateSetting: SetBooleanValue + canListRecords: SetBooleanValue + canListFolders: SetBooleanValue + def __init__(self, canAdd: _Optional[_Union[SetBooleanValue, str]] = ..., canRemove: _Optional[_Union[SetBooleanValue, str]] = ..., canDelete: _Optional[_Union[SetBooleanValue, str]] = ..., canListAccess: _Optional[_Union[SetBooleanValue, str]] = ..., canUpdateAccess: _Optional[_Union[SetBooleanValue, str]] = ..., canChangeOwnership: _Optional[_Union[SetBooleanValue, str]] = ..., canEditRecords: _Optional[_Union[SetBooleanValue, str]] = ..., canViewRecords: _Optional[_Union[SetBooleanValue, str]] = ..., canApproveAccess: _Optional[_Union[SetBooleanValue, str]] = ..., canRequestAccess: _Optional[_Union[SetBooleanValue, str]] = ..., canUpdateSetting: _Optional[_Union[SetBooleanValue, str]] = ..., canListRecords: _Optional[_Union[SetBooleanValue, str]] = ..., canListFolders: _Optional[_Union[SetBooleanValue, str]] = ...) -> None: ... + +class FolderRecordUpdateRequest(_message.Message): + __slots__ = ("folderUid", "addRecords", "updateRecords", "removeRecords") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + ADDRECORDS_FIELD_NUMBER: _ClassVar[int] + UPDATERECORDS_FIELD_NUMBER: _ClassVar[int] + REMOVERECORDS_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + addRecords: _containers.RepeatedCompositeFieldContainer[RecordMetadata] + updateRecords: _containers.RepeatedCompositeFieldContainer[RecordMetadata] + removeRecords: _containers.RepeatedCompositeFieldContainer[RecordMetadata] + def __init__(self, folderUid: _Optional[bytes] = ..., addRecords: _Optional[_Iterable[_Union[RecordMetadata, _Mapping]]] = ..., updateRecords: _Optional[_Iterable[_Union[RecordMetadata, _Mapping]]] = ..., removeRecords: _Optional[_Iterable[_Union[RecordMetadata, _Mapping]]] = ...) -> None: ... + +class RecordMetadata(_message.Message): + __slots__ = ("recordUid", "encryptedRecordKey", "encryptedRecordKeyType", "tlaProperties") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDRECORDKEY_FIELD_NUMBER: _ClassVar[int] + ENCRYPTEDRECORDKEYTYPE_FIELD_NUMBER: _ClassVar[int] + TLAPROPERTIES_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + encryptedRecordKey: bytes + encryptedRecordKeyType: EncryptedKeyType + tlaProperties: _tla_pb2.TLAProperties + def __init__(self, recordUid: _Optional[bytes] = ..., encryptedRecordKey: _Optional[bytes] = ..., encryptedRecordKeyType: _Optional[_Union[EncryptedKeyType, str]] = ..., tlaProperties: _Optional[_Union[_tla_pb2.TLAProperties, _Mapping]] = ...) -> None: ... + +class FolderRecord(_message.Message): + __slots__ = ("folderUid", "recordMetadata", "folderKeyEncryptionType") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + RECORDMETADATA_FIELD_NUMBER: _ClassVar[int] + FOLDERKEYENCRYPTIONTYPE_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + recordMetadata: RecordMetadata + folderKeyEncryptionType: FolderKeyEncryptionType + def __init__(self, folderUid: _Optional[bytes] = ..., recordMetadata: _Optional[_Union[RecordMetadata, _Mapping]] = ..., folderKeyEncryptionType: _Optional[_Union[FolderKeyEncryptionType, str]] = ...) -> None: ... + +class FolderRecordUpdateResponse(_message.Message): + __slots__ = ("folderUid", "folderRecordUpdateResult") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + FOLDERRECORDUPDATERESULT_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + folderRecordUpdateResult: _containers.RepeatedCompositeFieldContainer[FolderRecordUpdateResult] + def __init__(self, folderUid: _Optional[bytes] = ..., folderRecordUpdateResult: _Optional[_Iterable[_Union[FolderRecordUpdateResult, _Mapping]]] = ...) -> None: ... + +class FolderRecordUpdateResult(_message.Message): + __slots__ = ("recordUid", "status", "message") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + status: FolderModifyStatus + message: str + def __init__(self, recordUid: _Optional[bytes] = ..., status: _Optional[_Union[FolderModifyStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + +class FolderAccessData(_message.Message): + __slots__ = ("folderUid", "accessTypeUid", "accessType", "accessRoleType", "folderKey", "inherited", "hidden", "permissions", "tlaProperties", "dateCreated", "lastModified", "deniedAccess") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + ACCESSTYPEUID_FIELD_NUMBER: _ClassVar[int] + ACCESSTYPE_FIELD_NUMBER: _ClassVar[int] + ACCESSROLETYPE_FIELD_NUMBER: _ClassVar[int] + FOLDERKEY_FIELD_NUMBER: _ClassVar[int] + INHERITED_FIELD_NUMBER: _ClassVar[int] + HIDDEN_FIELD_NUMBER: _ClassVar[int] + PERMISSIONS_FIELD_NUMBER: _ClassVar[int] + TLAPROPERTIES_FIELD_NUMBER: _ClassVar[int] + DATECREATED_FIELD_NUMBER: _ClassVar[int] + LASTMODIFIED_FIELD_NUMBER: _ClassVar[int] + DENIEDACCESS_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + accessTypeUid: bytes + accessType: AccessType + accessRoleType: AccessRoleType + folderKey: EncryptedDataKey + inherited: bool + hidden: bool + permissions: FolderPermissions + tlaProperties: _tla_pb2.TLAProperties + dateCreated: int + lastModified: int + deniedAccess: bool + def __init__(self, folderUid: _Optional[bytes] = ..., accessTypeUid: _Optional[bytes] = ..., accessType: _Optional[_Union[AccessType, str]] = ..., accessRoleType: _Optional[_Union[AccessRoleType, str]] = ..., folderKey: _Optional[_Union[EncryptedDataKey, _Mapping]] = ..., inherited: bool = ..., hidden: bool = ..., permissions: _Optional[_Union[FolderPermissions, _Mapping]] = ..., tlaProperties: _Optional[_Union[_tla_pb2.TLAProperties, _Mapping]] = ..., dateCreated: _Optional[int] = ..., lastModified: _Optional[int] = ..., deniedAccess: bool = ...) -> None: ... + +class RevokedAccess(_message.Message): + __slots__ = ("folderUid", "actorUid", "accessType") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + ACTORUID_FIELD_NUMBER: _ClassVar[int] + ACCESSTYPE_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + actorUid: bytes + accessType: AccessType + def __init__(self, folderUid: _Optional[bytes] = ..., actorUid: _Optional[bytes] = ..., accessType: _Optional[_Union[AccessType, str]] = ...) -> None: ... + +class FolderRemoved(_message.Message): + __slots__ = ("folder_uid",) + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + folder_uid: bytes + def __init__(self, folder_uid: _Optional[bytes] = ...) -> None: ... + +class RecordAccessData(_message.Message): + __slots__ = ("accessTypeUid", "accessType", "recordUid", "accessRoleType", "owner", "inherited", "hidden", "deniedAccess", "can_view_title", "can_edit", "can_view", "can_list_access", "can_update_access", "can_delete", "can_change_ownership", "can_request_access", "can_approve_access", "dateCreated", "lastModified", "tlaProperties") + ACCESSTYPEUID_FIELD_NUMBER: _ClassVar[int] + ACCESSTYPE_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] + ACCESSROLETYPE_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + INHERITED_FIELD_NUMBER: _ClassVar[int] + HIDDEN_FIELD_NUMBER: _ClassVar[int] + DENIEDACCESS_FIELD_NUMBER: _ClassVar[int] + CAN_VIEW_TITLE_FIELD_NUMBER: _ClassVar[int] + CAN_EDIT_FIELD_NUMBER: _ClassVar[int] + CAN_VIEW_FIELD_NUMBER: _ClassVar[int] + CAN_LIST_ACCESS_FIELD_NUMBER: _ClassVar[int] + CAN_UPDATE_ACCESS_FIELD_NUMBER: _ClassVar[int] + CAN_DELETE_FIELD_NUMBER: _ClassVar[int] + CAN_CHANGE_OWNERSHIP_FIELD_NUMBER: _ClassVar[int] + CAN_REQUEST_ACCESS_FIELD_NUMBER: _ClassVar[int] + CAN_APPROVE_ACCESS_FIELD_NUMBER: _ClassVar[int] + DATECREATED_FIELD_NUMBER: _ClassVar[int] + LASTMODIFIED_FIELD_NUMBER: _ClassVar[int] + TLAPROPERTIES_FIELD_NUMBER: _ClassVar[int] + accessTypeUid: bytes + accessType: AccessType + recordUid: bytes + accessRoleType: AccessRoleType + owner: bool + inherited: bool + hidden: bool + deniedAccess: bool + can_view_title: bool + can_edit: bool + can_view: bool + can_list_access: bool + can_update_access: bool + can_delete: bool + can_change_ownership: bool + can_request_access: bool + can_approve_access: bool + dateCreated: int + lastModified: int + tlaProperties: _tla_pb2.TLAProperties + def __init__(self, accessTypeUid: _Optional[bytes] = ..., accessType: _Optional[_Union[AccessType, str]] = ..., recordUid: _Optional[bytes] = ..., accessRoleType: _Optional[_Union[AccessRoleType, str]] = ..., owner: bool = ..., inherited: bool = ..., hidden: bool = ..., deniedAccess: bool = ..., can_view_title: bool = ..., can_edit: bool = ..., can_view: bool = ..., can_list_access: bool = ..., can_update_access: bool = ..., can_delete: bool = ..., can_change_ownership: bool = ..., can_request_access: bool = ..., can_approve_access: bool = ..., dateCreated: _Optional[int] = ..., lastModified: _Optional[int] = ..., tlaProperties: _Optional[_Union[_tla_pb2.TLAProperties, _Mapping]] = ...) -> None: ... + +class AccessData(_message.Message): + __slots__ = ("accessTypeUid", "accessRoleType", "deniedAccess", "inherited", "hidden", "capabilities") + ACCESSTYPEUID_FIELD_NUMBER: _ClassVar[int] + ACCESSROLETYPE_FIELD_NUMBER: _ClassVar[int] + DENIEDACCESS_FIELD_NUMBER: _ClassVar[int] + INHERITED_FIELD_NUMBER: _ClassVar[int] + HIDDEN_FIELD_NUMBER: _ClassVar[int] + CAPABILITIES_FIELD_NUMBER: _ClassVar[int] + accessTypeUid: bytes + accessRoleType: AccessRoleType + deniedAccess: bool + inherited: bool + hidden: bool + capabilities: Capabilities + def __init__(self, accessTypeUid: _Optional[bytes] = ..., accessRoleType: _Optional[_Union[AccessRoleType, str]] = ..., deniedAccess: bool = ..., inherited: bool = ..., hidden: bool = ..., capabilities: _Optional[_Union[Capabilities, _Mapping]] = ...) -> None: ... + +class FolderAccessRequest(_message.Message): + __slots__ = ("folderAccessAdds", "folderAccessUpdates", "folderAccessRemoves") + FOLDERACCESSADDS_FIELD_NUMBER: _ClassVar[int] + FOLDERACCESSUPDATES_FIELD_NUMBER: _ClassVar[int] + FOLDERACCESSREMOVES_FIELD_NUMBER: _ClassVar[int] + folderAccessAdds: _containers.RepeatedCompositeFieldContainer[FolderAccessData] + folderAccessUpdates: _containers.RepeatedCompositeFieldContainer[FolderAccessData] + folderAccessRemoves: _containers.RepeatedCompositeFieldContainer[FolderAccessData] + def __init__(self, folderAccessAdds: _Optional[_Iterable[_Union[FolderAccessData, _Mapping]]] = ..., folderAccessUpdates: _Optional[_Iterable[_Union[FolderAccessData, _Mapping]]] = ..., folderAccessRemoves: _Optional[_Iterable[_Union[FolderAccessData, _Mapping]]] = ...) -> None: ... + +class FolderAccessResult(_message.Message): + __slots__ = ("folderUid", "accessUid", "accessType", "status", "message") + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + ACCESSUID_FIELD_NUMBER: _ClassVar[int] + ACCESSTYPE_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + folderUid: bytes + accessUid: bytes + accessType: AccessType + status: FolderModifyStatus + message: str + def __init__(self, folderUid: _Optional[bytes] = ..., accessUid: _Optional[bytes] = ..., accessType: _Optional[_Union[AccessType, str]] = ..., status: _Optional[_Union[FolderModifyStatus, str]] = ..., message: _Optional[str] = ...) -> None: ... + +class FolderAccessResponse(_message.Message): + __slots__ = ("folderAccessResults",) + FOLDERACCESSRESULTS_FIELD_NUMBER: _ClassVar[int] + folderAccessResults: _containers.RepeatedCompositeFieldContainer[FolderAccessResult] + def __init__(self, folderAccessResults: _Optional[_Iterable[_Union[FolderAccessResult, _Mapping]]] = ...) -> None: ... + +class UserInfo(_message.Message): + __slots__ = ("accountUid", "username") + ACCOUNTUID_FIELD_NUMBER: _ClassVar[int] + USERNAME_FIELD_NUMBER: _ClassVar[int] + accountUid: bytes + username: str + def __init__(self, accountUid: _Optional[bytes] = ..., username: _Optional[str] = ...) -> None: ... + +class RecordData(_message.Message): + __slots__ = ("user", "data", "recordUid") + USER_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] + user: UserInfo + data: bytes + recordUid: bytes + def __init__(self, user: _Optional[_Union[UserInfo, _Mapping]] = ..., data: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ...) -> None: ... + +class RecordKey(_message.Message): + __slots__ = ("user_uid", "record_uid", "record_key", "encrypted_key_type") + USER_UID_FIELD_NUMBER: _ClassVar[int] + RECORD_UID_FIELD_NUMBER: _ClassVar[int] + RECORD_KEY_FIELD_NUMBER: _ClassVar[int] + ENCRYPTED_KEY_TYPE_FIELD_NUMBER: _ClassVar[int] + user_uid: bytes + record_uid: bytes + record_key: bytes + encrypted_key_type: EncryptedKeyType + def __init__(self, user_uid: _Optional[bytes] = ..., record_uid: _Optional[bytes] = ..., record_key: _Optional[bytes] = ..., encrypted_key_type: _Optional[_Union[EncryptedKeyType, str]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/pagination_pb2.py b/keepersdk-package/src/keepersdk/proto/pagination_pb2.py new file mode 100644 index 00000000..85b834b6 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/pagination_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: pagination.proto +# Protobuf Python Version: 5.29.5 +"""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, + 29, + 5, + '', + 'pagination.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10pagination.proto\x12\x11keeper.api.common\"A\n\x04Page\x12\x12\n\npageNumber\x18\x01 \x01(\x05\x12\x10\n\x08pageSize\x18\x02 \x01(\x05\x12\x13\n\x0b\x63ursorToken\x18\x03 \x01(\t\"j\n\x08PageInfo\x12\x12\n\npageNumber\x18\x01 \x01(\x05\x12\x10\n\x08pageSize\x18\x02 \x01(\x05\x12\x12\n\ntotalCount\x18\x03 \x01(\x05\x12\x0f\n\x07hasMore\x18\x04 \x01(\x08\x12\x13\n\x0b\x63ursorToken\x18\x05 \x01(\tB\'\n#com.keepersecurity.proto.api.commonP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'pagination_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n#com.keepersecurity.proto.api.commonP\001' + _globals['_PAGE']._serialized_start=39 + _globals['_PAGE']._serialized_end=104 + _globals['_PAGEINFO']._serialized_start=106 + _globals['_PAGEINFO']._serialized_end=212 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/pagination_pb2.pyi b/keepersdk-package/src/keepersdk/proto/pagination_pb2.pyi new file mode 100644 index 00000000..bb7a09bf --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/pagination_pb2.pyi @@ -0,0 +1,29 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class Page(_message.Message): + __slots__ = ("pageNumber", "pageSize", "cursorToken") + PAGENUMBER_FIELD_NUMBER: _ClassVar[int] + PAGESIZE_FIELD_NUMBER: _ClassVar[int] + CURSORTOKEN_FIELD_NUMBER: _ClassVar[int] + pageNumber: int + pageSize: int + cursorToken: str + def __init__(self, pageNumber: _Optional[int] = ..., pageSize: _Optional[int] = ..., cursorToken: _Optional[str] = ...) -> None: ... + +class PageInfo(_message.Message): + __slots__ = ("pageNumber", "pageSize", "totalCount", "hasMore", "cursorToken") + PAGENUMBER_FIELD_NUMBER: _ClassVar[int] + PAGESIZE_FIELD_NUMBER: _ClassVar[int] + TOTALCOUNT_FIELD_NUMBER: _ClassVar[int] + HASMORE_FIELD_NUMBER: _ClassVar[int] + CURSORTOKEN_FIELD_NUMBER: _ClassVar[int] + pageNumber: int + pageSize: int + totalCount: int + hasMore: bool + cursorToken: str + def __init__(self, pageNumber: _Optional[int] = ..., pageSize: _Optional[int] = ..., totalCount: _Optional[int] = ..., hasMore: bool = ..., cursorToken: _Optional[str] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/pam_pb2.py b/keepersdk-package/src/keepersdk/proto/pam_pb2.py index 2bc86c21..e0770d73 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pam_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pam.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'pam.proto' ) @@ -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\"\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\"\xd5\x01\n\x13\x43nappWebhookRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x10\n\x08provider\x18\x02 \x01(\t\x12\x10\n\x08\x63lientId\x18\x03 \x01(\t\x12\x14\n\x0c\x63lientSecret\x18\x04 \x01(\t\x12\x16\n\x0e\x61piEndpointUrl\x18\x05 \x01(\t\x12\x0f\n\x07\x61uthUrl\x18\x06 \x01(\t\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x07 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x08 \x01(\x0c\x12\x11\n\twebhookId\x18\t \x01(\t\"S\n\x14\x43nappWebhookResponse\x12\x11\n\twebhookId\x18\x01 \x01(\t\x12\x12\n\nwebhookUrl\x18\x02 \x01(\t\x12\x14\n\x0cwebhookToken\x18\x03 \x01(\t\"/\n\x19\x43nappDeleteWebhookRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\"\x80\x01\n\x1b\x43nappTestCredentialsRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x10\n\x08\x63lientId\x18\x02 \x01(\t\x12\x14\n\x0c\x63lientSecret\x18\x03 \x01(\t\x12\x16\n\x0e\x61piEndpointUrl\x18\x04 \x01(\t\x12\x0f\n\x07\x61uthUrl\x18\x05 \x01(\t\"M\n\x1c\x43nappTestCredentialsResponse\x12\r\n\x05valid\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\"`\n\x15\x43nappQueueListRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x0cstatusFilter\x18\x02 \x01(\x05\x12\r\n\x05limit\x18\x03 \x01(\x05\x12\x0e\n\x06offset\x18\x04 \x01(\x05\"\xbb\x01\n\x0e\x43nappQueueItem\x12\x14\n\x0c\x63nappQueueId\x18\x01 \x01(\t\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x1a\n\x12\x63nappQueueStatusId\x18\x04 \x01(\x05\x12\x12\n\nreceivedAt\x18\x05 \x01(\x03\x12\x12\n\nresolvedAt\x18\x06 \x01(\x03\x12\x11\n\trecordUid\x18\x07 \x01(\x0c\x12\x0f\n\x07payload\x18\x08 \x01(\x0c\"j\n\x16\x43nappQueueListResponse\x12\"\n\x05items\x18\x01 \x03(\x0b\x32\x13.PAM.CnappQueueItem\x12\r\n\x05total\x18\x02 \x01(\x05\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x03 \x01(\x0c\"\x8c\x02\n\x16\x43nappQueueItemResponse\x12\x14\n\x0c\x63nappQueueId\x18\x01 \x01(\t\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x1a\n\x12\x63nappQueueStatusId\x18\x04 \x01(\x05\x12\x12\n\nreceivedAt\x18\x05 \x01(\x03\x12\x12\n\nresolvedAt\x18\x06 \x01(\x03\x12\x11\n\trecordUid\x18\x07 \x01(\x0c\x12\x0f\n\x07payload\x18\x08 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\t \x01(\x0c\x12\x11\n\tnetworkId\x18\n \x01(\x0c\x12\x1d\n\x15\x65ncryptionRecordKeyId\x18\x0b \x01(\x0c\"E\n\x15\x43nappAssociateRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x19\n\x11\x65xecuteAfterSetup\x18\x02 \x01(\x08\"F\n\x16\x43nappAssociateResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x1c\n\x14remediationTriggered\x18\x02 \x01(\x08\".\n\x13\x43nappResolveRequest\x12\x17\n\x0fresolutionNotes\x18\x01 \x01(\t\"+\n\x15\x43nappRemediateRequest\x12\x12\n\nactionType\x18\x01 \x01(\t\"e\n\x16\x43nappRemediateResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\x12\x12\n\nactionType\x18\x02 \x01(\t\x12\x0e\n\x06result\x18\x03 \x01(\t\x12\x17\n\x0f\x65xecutionTimeMs\x18\x04 \x01(\x03\"$\n\x12\x43nappIgnoreRequest\x12\x0e\n\x06reason\x18\x01 \x01(\t\"\x8d\x01\n\x1b\x43nappDefaultBehaviorRequest\x12\x11\n\tnetworkId\x18\x01 \x01(\x0c\x12\x17\n\x0f\x63nappProviderId\x18\x02 \x01(\x05\x12\x12\n\ncontrolKey\x18\x03 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x04 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x05 \x01(\x08\">\n\x1c\x43nappDefaultBehaviorResponse\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05\".\n\x18\x43nappBehaviorListRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\"I\n\x19\x43nappBehaviorListResponse\x12,\n\x05items\x18\x01 \x03(\x0b\x32\x1d.PAM.CnappDefaultBehaviorItem\"\xa7\x01\n\x18\x43nappDefaultBehaviorItem\x12\n\n\x02id\x18\x01 \x01(\x05\x12\x11\n\tnetworkId\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63nappProviderId\x18\x03 \x01(\x05\x12\x12\n\ncontrolKey\x18\x04 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x05 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x06 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x07 \x01(\x08\"\x91\x01\n\x1a\x43nappBehaviorUpdateRequest\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05\x12\x12\n\ncontrolKey\x18\x02 \x01(\t\x12\x19\n\x11\x63nappActionTypeId\x18\x03 \x01(\x05\x12\x13\n\x0b\x61utoExecute\x18\x04 \x01(\x08\x12\x0f\n\x07\x65nabled\x18\x05 \x01(\x08\"<\n\x1a\x43nappBehaviorDeleteRequest\x12\x1e\n\x16\x63nappDefaultBehaviorId\x18\x01 \x01(\x05*\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*\\\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*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\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\"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\"\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\"\xc1\x01\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.PamUsageByUser\x12\x10\n\x08nhiCount\x18\x07 \x01(\x05\"D\n\x16NhiBulkMetricsResponse\x12*\n\tresponses\x18\x01 \x03(\x0b\x32\x17.PAM.NhiMetricsResponse*\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\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,18 +34,18 @@ 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=6405 - _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=6563 - _globals['_PAMOPERATIONTYPE']._serialized_start=6565 - _globals['_PAMOPERATIONTYPE']._serialized_end=6629 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=6631 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=6743 - _globals['_CONTROLLERMESSAGETYPE']._serialized_start=6745 - _globals['_CONTROLLERMESSAGETYPE']._serialized_end=6837 - _globals['_PAMRECORDINGTYPE']._serialized_start=6839 - _globals['_PAMRECORDINGTYPE']._serialized_end=6925 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=6927 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=7032 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=4700 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=4858 + _globals['_PAMOPERATIONTYPE']._serialized_start=4860 + _globals['_PAMOPERATIONTYPE']._serialized_end=4924 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=4926 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=5038 + _globals['_CONTROLLERMESSAGETYPE']._serialized_start=5041 + _globals['_CONTROLLERMESSAGETYPE']._serialized_end=5242 + _globals['_PAMRECORDINGTYPE']._serialized_start=5244 + _globals['_PAMRECORDINGTYPE']._serialized_end=5330 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=5332 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=5437 _globals['_PAMROTATIONSCHEDULE']._serialized_start=51 _globals['_PAMROTATIONSCHEDULE']._serialized_end=182 _globals['_PAMROTATIONSCHEDULESRESPONSE']._serialized_start=184 @@ -114,48 +114,12 @@ _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=3832 _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=3835 _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4087 - _globals['_CNAPPWEBHOOKREQUEST']._serialized_start=4090 - _globals['_CNAPPWEBHOOKREQUEST']._serialized_end=4303 - _globals['_CNAPPWEBHOOKRESPONSE']._serialized_start=4305 - _globals['_CNAPPWEBHOOKRESPONSE']._serialized_end=4388 - _globals['_CNAPPDELETEWEBHOOKREQUEST']._serialized_start=4390 - _globals['_CNAPPDELETEWEBHOOKREQUEST']._serialized_end=4437 - _globals['_CNAPPTESTCREDENTIALSREQUEST']._serialized_start=4440 - _globals['_CNAPPTESTCREDENTIALSREQUEST']._serialized_end=4568 - _globals['_CNAPPTESTCREDENTIALSRESPONSE']._serialized_start=4570 - _globals['_CNAPPTESTCREDENTIALSRESPONSE']._serialized_end=4647 - _globals['_CNAPPQUEUELISTREQUEST']._serialized_start=4649 - _globals['_CNAPPQUEUELISTREQUEST']._serialized_end=4745 - _globals['_CNAPPQUEUEITEM']._serialized_start=4748 - _globals['_CNAPPQUEUEITEM']._serialized_end=4935 - _globals['_CNAPPQUEUELISTRESPONSE']._serialized_start=4937 - _globals['_CNAPPQUEUELISTRESPONSE']._serialized_end=5043 - _globals['_CNAPPQUEUEITEMRESPONSE']._serialized_start=5046 - _globals['_CNAPPQUEUEITEMRESPONSE']._serialized_end=5314 - _globals['_CNAPPASSOCIATEREQUEST']._serialized_start=5316 - _globals['_CNAPPASSOCIATEREQUEST']._serialized_end=5385 - _globals['_CNAPPASSOCIATERESPONSE']._serialized_start=5387 - _globals['_CNAPPASSOCIATERESPONSE']._serialized_end=5457 - _globals['_CNAPPRESOLVEREQUEST']._serialized_start=5459 - _globals['_CNAPPRESOLVEREQUEST']._serialized_end=5505 - _globals['_CNAPPREMEDIATEREQUEST']._serialized_start=5507 - _globals['_CNAPPREMEDIATEREQUEST']._serialized_end=5550 - _globals['_CNAPPREMEDIATERESPONSE']._serialized_start=5552 - _globals['_CNAPPREMEDIATERESPONSE']._serialized_end=5653 - _globals['_CNAPPIGNOREREQUEST']._serialized_start=5655 - _globals['_CNAPPIGNOREREQUEST']._serialized_end=5691 - _globals['_CNAPPDEFAULTBEHAVIORREQUEST']._serialized_start=5694 - _globals['_CNAPPDEFAULTBEHAVIORREQUEST']._serialized_end=5835 - _globals['_CNAPPDEFAULTBEHAVIORRESPONSE']._serialized_start=5837 - _globals['_CNAPPDEFAULTBEHAVIORRESPONSE']._serialized_end=5899 - _globals['_CNAPPBEHAVIORLISTREQUEST']._serialized_start=5901 - _globals['_CNAPPBEHAVIORLISTREQUEST']._serialized_end=5947 - _globals['_CNAPPBEHAVIORLISTRESPONSE']._serialized_start=5949 - _globals['_CNAPPBEHAVIORLISTRESPONSE']._serialized_end=6022 - _globals['_CNAPPDEFAULTBEHAVIORITEM']._serialized_start=6025 - _globals['_CNAPPDEFAULTBEHAVIORITEM']._serialized_end=6192 - _globals['_CNAPPBEHAVIORUPDATEREQUEST']._serialized_start=6195 - _globals['_CNAPPBEHAVIORUPDATEREQUEST']._serialized_end=6340 - _globals['_CNAPPBEHAVIORDELETEREQUEST']._serialized_start=6342 - _globals['_CNAPPBEHAVIORDELETEREQUEST']._serialized_end=6402 + _globals['_NHIMETRICSREQUEST']._serialized_start=4089 + _globals['_NHIMETRICSREQUEST']._serialized_end=4144 + _globals['_PAMUSAGEBYUSER']._serialized_start=4147 + _globals['_PAMUSAGEBYUSER']._serialized_end=4431 + _globals['_NHIMETRICSRESPONSE']._serialized_start=4434 + _globals['_NHIMETRICSRESPONSE']._serialized_end=4627 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_start=4629 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_end=4697 # @@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 5c5acb4e..030a298e 100644 --- a/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/pam_pb2.pyi @@ -4,8 +4,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -43,6 +42,11 @@ class ControllerMessageType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): CMT_ROTATE: _ClassVar[ControllerMessageType] CMT_DISCOVERY: _ClassVar[ControllerMessageType] CMT_CONNECT: _ClassVar[ControllerMessageType] + CMT_ANALYZE_RECORDING: _ClassVar[ControllerMessageType] + CMT_WORKFLOW_ACCESS_ELEVATION: _ClassVar[ControllerMessageType] + CMT_USS: _ClassVar[ControllerMessageType] + CMT_INFO: _ClassVar[ControllerMessageType] + CMT_AUTOMATION: _ClassVar[ControllerMessageType] class PAMRecordingType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -81,6 +85,11 @@ CMT_GENERAL: ControllerMessageType CMT_ROTATE: ControllerMessageType CMT_DISCOVERY: ControllerMessageType CMT_CONNECT: ControllerMessageType +CMT_ANALYZE_RECORDING: ControllerMessageType +CMT_WORKFLOW_ACCESS_ELEVATION: ControllerMessageType +CMT_USS: ControllerMessageType +CMT_INFO: ControllerMessageType +CMT_AUTOMATION: ControllerMessageType PRT_SESSION: PAMRecordingType PRT_TYPESCRIPT: PAMRecordingType PRT_TIME: PAMRecordingType @@ -103,7 +112,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: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., scheduleData: _Optional[str] = ..., noSchedule: bool = ...) -> None: ... class PAMRotationSchedulesResponse(_message.Message): __slots__ = ("schedules",) @@ -303,7 +312,7 @@ 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: _Optional[bool] = ...) -> None: ... + 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: ... class PAMSetMaxInstanceCountRequest(_message.Message): __slots__ = ("controllerUid", "maxInstanceCount") @@ -407,7 +416,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: _Optional[bool] = ...) -> None: ... + def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... class PAMData(_message.Message): __slots__ = ("vertex", "content") @@ -465,250 +474,58 @@ class PAMUniversalSyncConfig(_message.Message): folders: _containers.RepeatedCompositeFieldContainer[PAMUniversalSyncFolder] syncIdentity: bytes vaultName: bytes - def __init__(self, networkUid: _Optional[bytes] = ..., enabled: _Optional[bool] = ..., dryRunEnabled: _Optional[bool] = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ...) -> None: ... - -class CnappWebhookRequest(_message.Message): - __slots__ = ("networkUid", "provider", "clientId", "clientSecret", "apiEndpointUrl", "authUrl", "encryptionRecordKeyId", "controllerUid", "webhookId") - NETWORKUID_FIELD_NUMBER: _ClassVar[int] - PROVIDER_FIELD_NUMBER: _ClassVar[int] - CLIENTID_FIELD_NUMBER: _ClassVar[int] - CLIENTSECRET_FIELD_NUMBER: _ClassVar[int] - APIENDPOINTURL_FIELD_NUMBER: _ClassVar[int] - AUTHURL_FIELD_NUMBER: _ClassVar[int] - ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] - CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] - WEBHOOKID_FIELD_NUMBER: _ClassVar[int] - networkUid: bytes - provider: str - clientId: str - clientSecret: str - apiEndpointUrl: str - authUrl: str - encryptionRecordKeyId: bytes - controllerUid: bytes - webhookId: str - def __init__(self, networkUid: _Optional[bytes] = ..., provider: _Optional[str] = ..., clientId: _Optional[str] = ..., clientSecret: _Optional[str] = ..., apiEndpointUrl: _Optional[str] = ..., authUrl: _Optional[str] = ..., encryptionRecordKeyId: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., webhookId: _Optional[str] = ...) -> None: ... - -class CnappWebhookResponse(_message.Message): - __slots__ = ("webhookId", "webhookUrl", "webhookToken") - WEBHOOKID_FIELD_NUMBER: _ClassVar[int] - WEBHOOKURL_FIELD_NUMBER: _ClassVar[int] - WEBHOOKTOKEN_FIELD_NUMBER: _ClassVar[int] - webhookId: str - webhookUrl: str - webhookToken: str - def __init__(self, webhookId: _Optional[str] = ..., webhookUrl: _Optional[str] = ..., webhookToken: _Optional[str] = ...) -> None: ... - -class CnappDeleteWebhookRequest(_message.Message): - __slots__ = ("networkUid",) - NETWORKUID_FIELD_NUMBER: _ClassVar[int] - networkUid: bytes - def __init__(self, networkUid: _Optional[bytes] = ...) -> None: ... - -class CnappTestCredentialsRequest(_message.Message): - __slots__ = ("provider", "clientId", "clientSecret", "apiEndpointUrl", "authUrl") - PROVIDER_FIELD_NUMBER: _ClassVar[int] - CLIENTID_FIELD_NUMBER: _ClassVar[int] - CLIENTSECRET_FIELD_NUMBER: _ClassVar[int] - APIENDPOINTURL_FIELD_NUMBER: _ClassVar[int] - AUTHURL_FIELD_NUMBER: _ClassVar[int] - provider: str - clientId: str - clientSecret: str - apiEndpointUrl: str - authUrl: str - def __init__(self, provider: _Optional[str] = ..., clientId: _Optional[str] = ..., clientSecret: _Optional[str] = ..., apiEndpointUrl: _Optional[str] = ..., authUrl: _Optional[str] = ...) -> None: ... - -class CnappTestCredentialsResponse(_message.Message): - __slots__ = ("valid", "error", "message") - VALID_FIELD_NUMBER: _ClassVar[int] - ERROR_FIELD_NUMBER: _ClassVar[int] - MESSAGE_FIELD_NUMBER: _ClassVar[int] - valid: bool - error: str - message: str - def __init__(self, valid: _Optional[bool] = ..., error: _Optional[str] = ..., message: _Optional[str] = ...) -> None: ... - -class CnappQueueListRequest(_message.Message): - __slots__ = ("networkUid", "statusFilter", "limit", "offset") - NETWORKUID_FIELD_NUMBER: _ClassVar[int] - STATUSFILTER_FIELD_NUMBER: _ClassVar[int] - LIMIT_FIELD_NUMBER: _ClassVar[int] - OFFSET_FIELD_NUMBER: _ClassVar[int] - networkUid: bytes - statusFilter: int - limit: int - offset: int - def __init__(self, networkUid: _Optional[bytes] = ..., statusFilter: _Optional[int] = ..., limit: _Optional[int] = ..., offset: _Optional[int] = ...) -> None: ... - -class CnappQueueItem(_message.Message): - __slots__ = ("cnappQueueId", "controlKey", "cnappProviderId", "cnappQueueStatusId", "receivedAt", "resolvedAt", "recordUid", "payload") - CNAPPQUEUEID_FIELD_NUMBER: _ClassVar[int] - CONTROLKEY_FIELD_NUMBER: _ClassVar[int] - CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] - CNAPPQUEUESTATUSID_FIELD_NUMBER: _ClassVar[int] - RECEIVEDAT_FIELD_NUMBER: _ClassVar[int] - RESOLVEDAT_FIELD_NUMBER: _ClassVar[int] - RECORDUID_FIELD_NUMBER: _ClassVar[int] - PAYLOAD_FIELD_NUMBER: _ClassVar[int] - cnappQueueId: str - controlKey: str - cnappProviderId: int - cnappQueueStatusId: int - receivedAt: int - resolvedAt: int - recordUid: bytes - payload: bytes - def __init__(self, cnappQueueId: _Optional[str] = ..., controlKey: _Optional[str] = ..., cnappProviderId: _Optional[int] = ..., cnappQueueStatusId: _Optional[int] = ..., receivedAt: _Optional[int] = ..., resolvedAt: _Optional[int] = ..., recordUid: _Optional[bytes] = ..., payload: _Optional[bytes] = ...) -> None: ... - -class CnappQueueListResponse(_message.Message): - __slots__ = ("items", "total", "encryptionRecordKeyId") - ITEMS_FIELD_NUMBER: _ClassVar[int] - TOTAL_FIELD_NUMBER: _ClassVar[int] - ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] - items: _containers.RepeatedCompositeFieldContainer[CnappQueueItem] - total: int - encryptionRecordKeyId: bytes - def __init__(self, items: _Optional[_Iterable[_Union[CnappQueueItem, _Mapping]]] = ..., total: _Optional[int] = ..., encryptionRecordKeyId: _Optional[bytes] = ...) -> None: ... - -class CnappQueueItemResponse(_message.Message): - __slots__ = ("cnappQueueId", "controlKey", "cnappProviderId", "cnappQueueStatusId", "receivedAt", "resolvedAt", "recordUid", "payload", "controllerUid", "networkId", "encryptionRecordKeyId") - CNAPPQUEUEID_FIELD_NUMBER: _ClassVar[int] - CONTROLKEY_FIELD_NUMBER: _ClassVar[int] - CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] - CNAPPQUEUESTATUSID_FIELD_NUMBER: _ClassVar[int] - RECEIVEDAT_FIELD_NUMBER: _ClassVar[int] - RESOLVEDAT_FIELD_NUMBER: _ClassVar[int] - RECORDUID_FIELD_NUMBER: _ClassVar[int] - PAYLOAD_FIELD_NUMBER: _ClassVar[int] - CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] - NETWORKID_FIELD_NUMBER: _ClassVar[int] - ENCRYPTIONRECORDKEYID_FIELD_NUMBER: _ClassVar[int] - cnappQueueId: str - controlKey: str - cnappProviderId: int - cnappQueueStatusId: int - receivedAt: int - resolvedAt: int - recordUid: bytes - payload: bytes - controllerUid: bytes - networkId: bytes - encryptionRecordKeyId: bytes - def __init__(self, cnappQueueId: _Optional[str] = ..., controlKey: _Optional[str] = ..., cnappProviderId: _Optional[int] = ..., cnappQueueStatusId: _Optional[int] = ..., receivedAt: _Optional[int] = ..., resolvedAt: _Optional[int] = ..., recordUid: _Optional[bytes] = ..., payload: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., networkId: _Optional[bytes] = ..., encryptionRecordKeyId: _Optional[bytes] = ...) -> None: ... - -class CnappAssociateRequest(_message.Message): - __slots__ = ("recordUid", "executeAfterSetup") - RECORDUID_FIELD_NUMBER: _ClassVar[int] - EXECUTEAFTERSETUP_FIELD_NUMBER: _ClassVar[int] - recordUid: bytes - executeAfterSetup: bool - def __init__(self, recordUid: _Optional[bytes] = ..., executeAfterSetup: _Optional[bool] = ...) -> None: ... - -class CnappAssociateResponse(_message.Message): - __slots__ = ("status", "remediationTriggered") - STATUS_FIELD_NUMBER: _ClassVar[int] - REMEDIATIONTRIGGERED_FIELD_NUMBER: _ClassVar[int] - status: str - remediationTriggered: bool - def __init__(self, status: _Optional[str] = ..., remediationTriggered: _Optional[bool] = ...) -> None: ... - -class CnappResolveRequest(_message.Message): - __slots__ = ("resolutionNotes",) - RESOLUTIONNOTES_FIELD_NUMBER: _ClassVar[int] - resolutionNotes: str - def __init__(self, resolutionNotes: _Optional[str] = ...) -> None: ... - -class CnappRemediateRequest(_message.Message): - __slots__ = ("actionType",) - ACTIONTYPE_FIELD_NUMBER: _ClassVar[int] - actionType: str - def __init__(self, actionType: _Optional[str] = ...) -> None: ... - -class CnappRemediateResponse(_message.Message): - __slots__ = ("status", "actionType", "result", "executionTimeMs") - STATUS_FIELD_NUMBER: _ClassVar[int] - ACTIONTYPE_FIELD_NUMBER: _ClassVar[int] - RESULT_FIELD_NUMBER: _ClassVar[int] - EXECUTIONTIMEMS_FIELD_NUMBER: _ClassVar[int] - status: str - actionType: str - result: str - executionTimeMs: int - def __init__(self, status: _Optional[str] = ..., actionType: _Optional[str] = ..., result: _Optional[str] = ..., executionTimeMs: _Optional[int] = ...) -> None: ... - -class CnappIgnoreRequest(_message.Message): - __slots__ = ("reason",) - REASON_FIELD_NUMBER: _ClassVar[int] - reason: str - def __init__(self, reason: _Optional[str] = ...) -> None: ... - -class CnappDefaultBehaviorRequest(_message.Message): - __slots__ = ("networkId", "cnappProviderId", "controlKey", "cnappActionTypeId", "autoExecute") - NETWORKID_FIELD_NUMBER: _ClassVar[int] - CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] - CONTROLKEY_FIELD_NUMBER: _ClassVar[int] - CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] - AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] - networkId: bytes - cnappProviderId: int - controlKey: str - cnappActionTypeId: int - autoExecute: bool - def __init__(self, networkId: _Optional[bytes] = ..., cnappProviderId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ...) -> None: ... - -class CnappDefaultBehaviorResponse(_message.Message): - __slots__ = ("cnappDefaultBehaviorId",) - CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] - cnappDefaultBehaviorId: int - def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ...) -> None: ... - -class CnappBehaviorListRequest(_message.Message): - __slots__ = ("networkUid",) - NETWORKUID_FIELD_NUMBER: _ClassVar[int] - networkUid: bytes - def __init__(self, networkUid: _Optional[bytes] = ...) -> None: ... - -class CnappBehaviorListResponse(_message.Message): - __slots__ = ("items",) - ITEMS_FIELD_NUMBER: _ClassVar[int] - items: _containers.RepeatedCompositeFieldContainer[CnappDefaultBehaviorItem] - def __init__(self, items: _Optional[_Iterable[_Union[CnappDefaultBehaviorItem, _Mapping]]] = ...) -> None: ... - -class CnappDefaultBehaviorItem(_message.Message): - __slots__ = ("id", "networkId", "cnappProviderId", "controlKey", "cnappActionTypeId", "autoExecute", "enabled") - ID_FIELD_NUMBER: _ClassVar[int] - NETWORKID_FIELD_NUMBER: _ClassVar[int] - CNAPPPROVIDERID_FIELD_NUMBER: _ClassVar[int] - CONTROLKEY_FIELD_NUMBER: _ClassVar[int] - CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] - AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] - ENABLED_FIELD_NUMBER: _ClassVar[int] - id: int - networkId: bytes - cnappProviderId: int - controlKey: str - cnappActionTypeId: int - autoExecute: bool - enabled: bool - def __init__(self, id: _Optional[int] = ..., networkId: _Optional[bytes] = ..., cnappProviderId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ..., enabled: _Optional[bool] = ...) -> None: ... - -class CnappBehaviorUpdateRequest(_message.Message): - __slots__ = ("cnappDefaultBehaviorId", "controlKey", "cnappActionTypeId", "autoExecute", "enabled") - CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] - CONTROLKEY_FIELD_NUMBER: _ClassVar[int] - CNAPPACTIONTYPEID_FIELD_NUMBER: _ClassVar[int] - AUTOEXECUTE_FIELD_NUMBER: _ClassVar[int] - ENABLED_FIELD_NUMBER: _ClassVar[int] - cnappDefaultBehaviorId: int - controlKey: str - cnappActionTypeId: int - autoExecute: bool - enabled: bool - def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ..., controlKey: _Optional[str] = ..., cnappActionTypeId: _Optional[int] = ..., autoExecute: _Optional[bool] = ..., enabled: _Optional[bool] = ...) -> None: ... - -class CnappBehaviorDeleteRequest(_message.Message): - __slots__ = ("cnappDefaultBehaviorId",) - CNAPPDEFAULTBEHAVIORID_FIELD_NUMBER: _ClassVar[int] - cnappDefaultBehaviorId: int - def __init__(self, cnappDefaultBehaviorId: _Optional[int] = ...) -> None: ... + def __init__(self, networkUid: _Optional[bytes] = ..., enabled: bool = ..., dryRunEnabled: bool = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ...) -> None: ... + +class NhiMetricsRequest(_message.Message): + __slots__ = ("startTime", "endTime") + STARTTIME_FIELD_NUMBER: _ClassVar[int] + ENDTIME_FIELD_NUMBER: _ClassVar[int] + startTime: int + endTime: int + def __init__(self, startTime: _Optional[int] = ..., endTime: _Optional[int] = ...) -> None: ... + +class PamUsageByUser(_message.Message): + __slots__ = ("userId", "recordRotationScheduledOk", "pamConnectionStarted", "pamTunnelStarted", "discoveryJobStarted", "recordRotationOnDemandOk", "pamSessionRecordingStarted", "pamRbiStarted", "pamSessionRbiRecordingStarted") + USERID_FIELD_NUMBER: _ClassVar[int] + RECORDROTATIONSCHEDULEDOK_FIELD_NUMBER: _ClassVar[int] + PAMCONNECTIONSTARTED_FIELD_NUMBER: _ClassVar[int] + PAMTUNNELSTARTED_FIELD_NUMBER: _ClassVar[int] + DISCOVERYJOBSTARTED_FIELD_NUMBER: _ClassVar[int] + RECORDROTATIONONDEMANDOK_FIELD_NUMBER: _ClassVar[int] + PAMSESSIONRECORDINGSTARTED_FIELD_NUMBER: _ClassVar[int] + PAMRBISTARTED_FIELD_NUMBER: _ClassVar[int] + PAMSESSIONRBIRECORDINGSTARTED_FIELD_NUMBER: _ClassVar[int] + userId: int + recordRotationScheduledOk: int + pamConnectionStarted: int + pamTunnelStarted: int + discoveryJobStarted: int + recordRotationOnDemandOk: int + pamSessionRecordingStarted: int + pamRbiStarted: int + pamSessionRbiRecordingStarted: int + def __init__(self, userId: _Optional[int] = ..., recordRotationScheduledOk: _Optional[int] = ..., pamConnectionStarted: _Optional[int] = ..., pamTunnelStarted: _Optional[int] = ..., discoveryJobStarted: _Optional[int] = ..., recordRotationOnDemandOk: _Optional[int] = ..., pamSessionRecordingStarted: _Optional[int] = ..., pamRbiStarted: _Optional[int] = ..., pamSessionRbiRecordingStarted: _Optional[int] = ...) -> None: ... + +class NhiMetricsResponse(_message.Message): + __slots__ = ("enterpriseId", "startTime", "endTime", "uniqueKsmDevices", "pamGatewayOnline", "pamUsageByUser", "nhiCount") + ENTERPRISEID_FIELD_NUMBER: _ClassVar[int] + STARTTIME_FIELD_NUMBER: _ClassVar[int] + ENDTIME_FIELD_NUMBER: _ClassVar[int] + UNIQUEKSMDEVICES_FIELD_NUMBER: _ClassVar[int] + PAMGATEWAYONLINE_FIELD_NUMBER: _ClassVar[int] + PAMUSAGEBYUSER_FIELD_NUMBER: _ClassVar[int] + NHICOUNT_FIELD_NUMBER: _ClassVar[int] + enterpriseId: int + startTime: int + endTime: int + uniqueKsmDevices: int + pamGatewayOnline: int + pamUsageByUser: _containers.RepeatedCompositeFieldContainer[PamUsageByUser] + nhiCount: int + def __init__(self, enterpriseId: _Optional[int] = ..., startTime: _Optional[int] = ..., endTime: _Optional[int] = ..., uniqueKsmDevices: _Optional[int] = ..., pamGatewayOnline: _Optional[int] = ..., pamUsageByUser: _Optional[_Iterable[_Union[PamUsageByUser, _Mapping]]] = ..., nhiCount: _Optional[int] = ...) -> None: ... + +class NhiBulkMetricsResponse(_message.Message): + __slots__ = ("responses",) + RESPONSES_FIELD_NUMBER: _ClassVar[int] + responses: _containers.RepeatedCompositeFieldContainer[NhiMetricsResponse] + def __init__(self, responses: _Optional[_Iterable[_Union[NhiMetricsResponse, _Mapping]]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py index 217859e5..e1d72a4d 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: pedm.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'pedm.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi b/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi index bb0f669a..52f82d9b 100644 --- a/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/pedm_pb2.pyi @@ -3,8 +3,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -52,7 +51,7 @@ class PedmStatus(_message.Message): key: _containers.RepeatedScalarFieldContainer[bytes] success: bool message: str - def __init__(self, key: _Optional[_Iterable[bytes]] = ..., success: _Optional[bool] = ..., message: _Optional[str] = ...) -> None: ... + def __init__(self, key: _Optional[_Iterable[bytes]] = ..., success: bool = ..., message: _Optional[str] = ...) -> None: ... class PedmStatusResponse(_message.Message): __slots__ = ("addStatus", "updateStatus", "removeStatus") @@ -140,7 +139,7 @@ class PolicyAdd(_message.Message): encryptedData: bytes encryptedKey: bytes disabled: bool - def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., disabled: _Optional[bool] = ...) -> None: ... + def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., disabled: bool = ...) -> None: ... class PolicyUpdate(_message.Message): __slots__ = ("policyUid", "plainData", "encryptedData", "disabled") @@ -260,7 +259,7 @@ class DeploymentNode(_message.Message): agentData: bytes created: int modified: int - def __init__(self, deploymentUid: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., aesKey: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., agentData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... + def __init__(self, deploymentUid: _Optional[bytes] = ..., disabled: bool = ..., aesKey: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., agentData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... class AgentNode(_message.Message): __slots__ = ("agentUid", "machineId", "deploymentUid", "ecPublicKey", "disabled", "encryptedData", "created", "modified") @@ -280,7 +279,7 @@ class AgentNode(_message.Message): encryptedData: bytes created: int modified: int - def __init__(self, agentUid: _Optional[bytes] = ..., machineId: _Optional[str] = ..., deploymentUid: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., encryptedData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... + def __init__(self, agentUid: _Optional[bytes] = ..., machineId: _Optional[str] = ..., deploymentUid: _Optional[bytes] = ..., ecPublicKey: _Optional[bytes] = ..., disabled: bool = ..., encryptedData: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ...) -> None: ... class PolicyNode(_message.Message): __slots__ = ("policyUid", "plainData", "encryptedData", "encryptedKey", "created", "modified", "disabled") @@ -298,7 +297,7 @@ class PolicyNode(_message.Message): created: int modified: int disabled: bool - def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ..., disabled: _Optional[bool] = ...) -> None: ... + def __init__(self, policyUid: _Optional[bytes] = ..., plainData: _Optional[bytes] = ..., encryptedData: _Optional[bytes] = ..., encryptedKey: _Optional[bytes] = ..., created: _Optional[int] = ..., modified: _Optional[int] = ..., disabled: bool = ...) -> None: ... class CollectionNode(_message.Message): __slots__ = ("collectionUid", "collectionType", "encryptedData", "created") @@ -418,7 +417,7 @@ class GetPedmDataResponse(_message.Message): collectionLink: _containers.RepeatedCompositeFieldContainer[CollectionLink] approvals: _containers.RepeatedCompositeFieldContainer[ApprovalNode] approvalStatus: _containers.RepeatedCompositeFieldContainer[ApprovalStatusNode] - def __init__(self, continuationToken: _Optional[bytes] = ..., resetCache: _Optional[bool] = ..., hasMore: _Optional[bool] = ..., removedDeployments: _Optional[_Iterable[bytes]] = ..., removedAgents: _Optional[_Iterable[bytes]] = ..., removedPolicies: _Optional[_Iterable[bytes]] = ..., removedCollection: _Optional[_Iterable[bytes]] = ..., removedCollectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., removedApprovals: _Optional[_Iterable[bytes]] = ..., deployments: _Optional[_Iterable[_Union[DeploymentNode, _Mapping]]] = ..., agents: _Optional[_Iterable[_Union[AgentNode, _Mapping]]] = ..., policies: _Optional[_Iterable[_Union[PolicyNode, _Mapping]]] = ..., collections: _Optional[_Iterable[_Union[CollectionNode, _Mapping]]] = ..., collectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., approvals: _Optional[_Iterable[_Union[ApprovalNode, _Mapping]]] = ..., approvalStatus: _Optional[_Iterable[_Union[ApprovalStatusNode, _Mapping]]] = ...) -> None: ... + def __init__(self, continuationToken: _Optional[bytes] = ..., resetCache: bool = ..., hasMore: bool = ..., removedDeployments: _Optional[_Iterable[bytes]] = ..., removedAgents: _Optional[_Iterable[bytes]] = ..., removedPolicies: _Optional[_Iterable[bytes]] = ..., removedCollection: _Optional[_Iterable[bytes]] = ..., removedCollectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., removedApprovals: _Optional[_Iterable[bytes]] = ..., deployments: _Optional[_Iterable[_Union[DeploymentNode, _Mapping]]] = ..., agents: _Optional[_Iterable[_Union[AgentNode, _Mapping]]] = ..., policies: _Optional[_Iterable[_Union[PolicyNode, _Mapping]]] = ..., collections: _Optional[_Iterable[_Union[CollectionNode, _Mapping]]] = ..., collectionLink: _Optional[_Iterable[_Union[CollectionLink, _Mapping]]] = ..., approvals: _Optional[_Iterable[_Union[ApprovalNode, _Mapping]]] = ..., approvalStatus: _Optional[_Iterable[_Union[ApprovalStatusNode, _Mapping]]] = ...) -> None: ... class PolicyAgentRequest(_message.Message): __slots__ = ("policyUid", "summaryOnly") @@ -426,7 +425,7 @@ class PolicyAgentRequest(_message.Message): SUMMARYONLY_FIELD_NUMBER: _ClassVar[int] policyUid: _containers.RepeatedScalarFieldContainer[bytes] summaryOnly: bool - def __init__(self, policyUid: _Optional[_Iterable[bytes]] = ..., summaryOnly: _Optional[bool] = ...) -> None: ... + def __init__(self, policyUid: _Optional[_Iterable[bytes]] = ..., summaryOnly: bool = ...) -> None: ... class PolicyAgentResponse(_message.Message): __slots__ = ("agentCount", "agentUid") @@ -466,7 +465,7 @@ class AuditCollectionResponse(_message.Message): values: _containers.RepeatedCompositeFieldContainer[AuditCollectionValue] hasMore: bool continuationToken: bytes - def __init__(self, values: _Optional[_Iterable[_Union[AuditCollectionValue, _Mapping]]] = ..., hasMore: _Optional[bool] = ..., continuationToken: _Optional[bytes] = ...) -> None: ... + def __init__(self, values: _Optional[_Iterable[_Union[AuditCollectionValue, _Mapping]]] = ..., hasMore: bool = ..., continuationToken: _Optional[bytes] = ...) -> None: ... class GetCollectionLinkRequest(_message.Message): __slots__ = ("collectionLink",) @@ -520,7 +519,7 @@ class GetAgentLastSeenRequest(_message.Message): AGENTUID_FIELD_NUMBER: _ClassVar[int] activeOnly: bool agentUid: _containers.RepeatedScalarFieldContainer[bytes] - def __init__(self, activeOnly: _Optional[bool] = ..., agentUid: _Optional[_Iterable[bytes]] = ...) -> None: ... + def __init__(self, activeOnly: bool = ..., agentUid: _Optional[_Iterable[bytes]] = ...) -> None: ... class AgentLastSeen(_message.Message): __slots__ = ("agentUid", "lastSeen") diff --git a/keepersdk-package/src/keepersdk/proto/push_pb2.py b/keepersdk-package/src/keepersdk/proto/push_pb2.py index 49214c23..c8a2f62b 100644 --- a/keepersdk-package/src/keepersdk/proto/push_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/push_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: push.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'push.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/push_pb2.pyi b/keepersdk-package/src/keepersdk/proto/push_pb2.pyi index 1e888c81..85bde767 100644 --- a/keepersdk-package/src/keepersdk/proto/push_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/push_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor diff --git a/keepersdk-package/src/keepersdk/proto/record_details_pb2.py b/keepersdk-package/src/keepersdk/proto/record_details_pb2.py new file mode 100644 index 00000000..39bd1e04 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_details_pb2.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: record_details.proto +# Protobuf Python Version: 5.29.5 +"""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, + 29, + 5, + '', + 'record_details.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 +from . import folder_pb2 as folder__pb2 +from . import record_pb2 as record__pb2 +from . import pagination_pb2 as pagination__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14record_details.proto\x12\x11record.v3.details\x1a\x1cgoogle/api/annotations.proto\x1a\x0c\x66older.proto\x1a\x0crecord.proto\x1a\x10pagination.proto\";\n\x11RecordDataRequest\x12\x12\n\nclientTime\x18\x01 \x01(\x03\x12\x12\n\nrecordUids\x18\x03 \x03(\x0c\"Q\n\x12RecordDataResponse\x12!\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x13.Records.RecordData\x12\x18\n\x10\x66orbiddenRecords\x18\x02 \x03(\x0c\"P\n\x13RecordAccessRequest\x12\x12\n\nrecordUids\x18\x03 \x03(\x0c\x12%\n\x04page\x18\x02 \x01(\x0b\x32\x17.keeper.api.common.Page\"\x98\x01\n\x14RecordAccessResponse\x12\x37\n\x0erecordAccesses\x18\x01 \x03(\x0b\x32\x1f.record.v3.details.RecordAccess\x12\x18\n\x10\x66orbiddenRecords\x18\x02 \x03(\x0c\x12-\n\x08pageInfo\x18\x03 \x01(\x0b\x32\x1b.keeper.api.common.PageInfo\"m\n\x1cRecordAccessorDetailsRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x61\x63\x63\x65ssorUid\x18\x02 \x01(\x0c\x12%\n\x04page\x18\x03 \x01(\x0b\x32\x17.keeper.api.common.Page\"\xb6\x01\n\x1dRecordAccessorDetailsResponse\x12\x32\n\x10recordAccessData\x18\x01 \x01(\x0b\x32\x18.Folder.RecordAccessData\x12\x32\n\x10\x66olderAccessData\x18\x02 \x03(\x0b\x32\x18.Folder.FolderAccessData\x12-\n\x08pageInfo\x18\x03 \x01(\x0b\x32\x1b.keeper.api.common.PageInfo\"m\n\x0cRecordAccess\x12&\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x18.Folder.RecordAccessData\x12\x35\n\x0c\x61\x63\x63\x65ssorInfo\x18\x02 \x01(\x0b\x32\x1f.record.v3.details.AccessorInfo\"\x1c\n\x0c\x41\x63\x63\x65ssorInfo\x12\x0c\n\x04name\x18\x01 \x01(\t2\x86\x04\n\x14RecordDetailsService\x12\x90\x01\n\rGetRecordData\x12$.record.v3.details.RecordDataRequest\x1a%.record.v3.details.RecordDataResponse\"2\x82\xd3\xe4\x93\x02,\"\'/api/rest/vault/records/v3/details/data:\x01*\x12\x9b\x01\n\x12GetRecordAccessors\x12&.record.v3.details.RecordAccessRequest\x1a\'.record.v3.details.RecordAccessResponse\"4\x82\xd3\xe4\x93\x02.\")/api/rest/vault/records/v3/details/access:\x01*\x12\xbc\x01\n\x18GetRecordAccessorDetails\x12/.record.v3.details.RecordAccessorDetailsRequest\x1a\x30.record.v3.details.RecordAccessorDetailsResponse\"=\x82\xd3\xe4\x93\x02\x37\"2/api/rest/vault/records/v3/details/access/accessor:\x01*B2\n.com.keepersecurity.proto.api.record.v3.detailsP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'record_details_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n.com.keepersecurity.proto.api.record.v3.detailsP\001' + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordData']._loaded_options = None + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordData']._serialized_options = b'\202\323\344\223\002,\"\'/api/rest/vault/records/v3/details/data:\001*' + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordAccessors']._loaded_options = None + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordAccessors']._serialized_options = b'\202\323\344\223\002.\")/api/rest/vault/records/v3/details/access:\001*' + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordAccessorDetails']._loaded_options = None + _globals['_RECORDDETAILSSERVICE'].methods_by_name['GetRecordAccessorDetails']._serialized_options = b'\202\323\344\223\0027\"2/api/rest/vault/records/v3/details/access/accessor:\001*' + _globals['_RECORDDATAREQUEST']._serialized_start=119 + _globals['_RECORDDATAREQUEST']._serialized_end=178 + _globals['_RECORDDATARESPONSE']._serialized_start=180 + _globals['_RECORDDATARESPONSE']._serialized_end=261 + _globals['_RECORDACCESSREQUEST']._serialized_start=263 + _globals['_RECORDACCESSREQUEST']._serialized_end=343 + _globals['_RECORDACCESSRESPONSE']._serialized_start=346 + _globals['_RECORDACCESSRESPONSE']._serialized_end=498 + _globals['_RECORDACCESSORDETAILSREQUEST']._serialized_start=500 + _globals['_RECORDACCESSORDETAILSREQUEST']._serialized_end=609 + _globals['_RECORDACCESSORDETAILSRESPONSE']._serialized_start=612 + _globals['_RECORDACCESSORDETAILSRESPONSE']._serialized_end=794 + _globals['_RECORDACCESS']._serialized_start=796 + _globals['_RECORDACCESS']._serialized_end=905 + _globals['_ACCESSORINFO']._serialized_start=907 + _globals['_ACCESSORINFO']._serialized_end=935 + _globals['_RECORDDETAILSSERVICE']._serialized_start=938 + _globals['_RECORDDETAILSSERVICE']._serialized_end=1456 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/record_details_pb2.pyi b/keepersdk-package/src/keepersdk/proto/record_details_pb2.pyi new file mode 100644 index 00000000..dcab92bf --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_details_pb2.pyi @@ -0,0 +1,78 @@ +from google.api import annotations_pb2 as _annotations_pb2 +import folder_pb2 as _folder_pb2 +import record_pb2 as _record_pb2 +import pagination_pb2 as _pagination_pb2 +from google.protobuf.internal import containers as _containers +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class RecordDataRequest(_message.Message): + __slots__ = ("clientTime", "recordUids") + CLIENTTIME_FIELD_NUMBER: _ClassVar[int] + RECORDUIDS_FIELD_NUMBER: _ClassVar[int] + clientTime: int + recordUids: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, clientTime: _Optional[int] = ..., recordUids: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class RecordDataResponse(_message.Message): + __slots__ = ("data", "forbiddenRecords") + DATA_FIELD_NUMBER: _ClassVar[int] + FORBIDDENRECORDS_FIELD_NUMBER: _ClassVar[int] + data: _containers.RepeatedCompositeFieldContainer[_record_pb2.RecordData] + forbiddenRecords: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, data: _Optional[_Iterable[_Union[_record_pb2.RecordData, _Mapping]]] = ..., forbiddenRecords: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class RecordAccessRequest(_message.Message): + __slots__ = ("recordUids", "page") + RECORDUIDS_FIELD_NUMBER: _ClassVar[int] + PAGE_FIELD_NUMBER: _ClassVar[int] + recordUids: _containers.RepeatedScalarFieldContainer[bytes] + page: _pagination_pb2.Page + def __init__(self, recordUids: _Optional[_Iterable[bytes]] = ..., page: _Optional[_Union[_pagination_pb2.Page, _Mapping]] = ...) -> None: ... + +class RecordAccessResponse(_message.Message): + __slots__ = ("recordAccesses", "forbiddenRecords", "pageInfo") + RECORDACCESSES_FIELD_NUMBER: _ClassVar[int] + FORBIDDENRECORDS_FIELD_NUMBER: _ClassVar[int] + PAGEINFO_FIELD_NUMBER: _ClassVar[int] + recordAccesses: _containers.RepeatedCompositeFieldContainer[RecordAccess] + forbiddenRecords: _containers.RepeatedScalarFieldContainer[bytes] + pageInfo: _pagination_pb2.PageInfo + def __init__(self, recordAccesses: _Optional[_Iterable[_Union[RecordAccess, _Mapping]]] = ..., forbiddenRecords: _Optional[_Iterable[bytes]] = ..., pageInfo: _Optional[_Union[_pagination_pb2.PageInfo, _Mapping]] = ...) -> None: ... + +class RecordAccessorDetailsRequest(_message.Message): + __slots__ = ("recordUid", "accessorUid", "page") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + ACCESSORUID_FIELD_NUMBER: _ClassVar[int] + PAGE_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + accessorUid: bytes + page: _pagination_pb2.Page + def __init__(self, recordUid: _Optional[bytes] = ..., accessorUid: _Optional[bytes] = ..., page: _Optional[_Union[_pagination_pb2.Page, _Mapping]] = ...) -> None: ... + +class RecordAccessorDetailsResponse(_message.Message): + __slots__ = ("recordAccessData", "folderAccessData", "pageInfo") + RECORDACCESSDATA_FIELD_NUMBER: _ClassVar[int] + FOLDERACCESSDATA_FIELD_NUMBER: _ClassVar[int] + PAGEINFO_FIELD_NUMBER: _ClassVar[int] + recordAccessData: _folder_pb2.RecordAccessData + folderAccessData: _containers.RepeatedCompositeFieldContainer[_folder_pb2.FolderAccessData] + pageInfo: _pagination_pb2.PageInfo + def __init__(self, recordAccessData: _Optional[_Union[_folder_pb2.RecordAccessData, _Mapping]] = ..., folderAccessData: _Optional[_Iterable[_Union[_folder_pb2.FolderAccessData, _Mapping]]] = ..., pageInfo: _Optional[_Union[_pagination_pb2.PageInfo, _Mapping]] = ...) -> None: ... + +class RecordAccess(_message.Message): + __slots__ = ("data", "accessorInfo") + DATA_FIELD_NUMBER: _ClassVar[int] + ACCESSORINFO_FIELD_NUMBER: _ClassVar[int] + data: _folder_pb2.RecordAccessData + accessorInfo: AccessorInfo + def __init__(self, data: _Optional[_Union[_folder_pb2.RecordAccessData, _Mapping]] = ..., accessorInfo: _Optional[_Union[AccessorInfo, _Mapping]] = ...) -> None: ... + +class AccessorInfo(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: str + def __init__(self, name: _Optional[str] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.py b/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.py new file mode 100644 index 00000000..55cbe6d3 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: record_endpoints.proto +# Protobuf Python Version: 5.29.5 +"""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, + 29, + 5, + '', + 'record_endpoints.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import record_pb2 as record__pb2 +from . import folder_pb2 as folder__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16record_endpoints.proto\x12\trecord.v3\x1a\x0crecord.proto\x1a\x0c\x66older.proto\"\x83\x01\n\x11RecordsAddRequest\x12%\n\x07records\x18\x01 \x03(\x0b\x32\x14.record.v3.RecordAdd\x12\x12\n\nclientTime\x18\x02 \x01(\x03\x12\x33\n\x13securityDataKeyType\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\xa8\x03\n\tRecordAdd\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12/\n\rrecordKeyType\x18\x03 \x01(\x0e\x32\x18.Folder.EncryptedKeyType\x12=\n\x14recordKeyEncryptedBy\x18\x04 \x01(\x0e\x32\x1f.Folder.FolderKeyEncryptionType\x12\x1a\n\x12\x63lientModifiedTime\x18\x05 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x15\n\rnonSharedData\x18\x07 \x01(\x0c\x12\x11\n\tfolderUid\x18\x08 \x01(\x0c\x12(\n\x0brecordLinks\x18\t \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\n \x01(\x0b\x32\x14.Records.RecordAudit\x12+\n\x0csecurityData\x18\x0b \x01(\x0b\x32\x15.Records.SecurityData\x12\x35\n\x11securityScoreData\x18\x0c \x01(\x0b\x32\x1a.Records.SecurityScoreDataB*\n&com.keepersecurity.proto.api.record.v3P\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'record_endpoints_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n&com.keepersecurity.proto.api.record.v3P\001' + _globals['_RECORDSADDREQUEST']._serialized_start=66 + _globals['_RECORDSADDREQUEST']._serialized_end=197 + _globals['_RECORDADD']._serialized_start=200 + _globals['_RECORDADD']._serialized_end=624 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.pyi b/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.pyi new file mode 100644 index 00000000..d60f1357 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_endpoints_pb2.pyi @@ -0,0 +1,46 @@ +from . import record_pb2 as _record_pb2 +from . import folder_pb2 as _folder_pb2 +from google.protobuf.internal import containers as _containers +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class RecordsAddRequest(_message.Message): + __slots__ = ("records", "clientTime", "securityDataKeyType") + RECORDS_FIELD_NUMBER: _ClassVar[int] + CLIENTTIME_FIELD_NUMBER: _ClassVar[int] + SECURITYDATAKEYTYPE_FIELD_NUMBER: _ClassVar[int] + records: _containers.RepeatedCompositeFieldContainer[RecordAdd] + clientTime: int + securityDataKeyType: _record_pb2.RecordKeyType + def __init__(self, records: _Optional[_Iterable[_Union[RecordAdd, _Mapping]]] = ..., clientTime: _Optional[int] = ..., securityDataKeyType: _Optional[_Union[_record_pb2.RecordKeyType, str]] = ...) -> None: ... + +class RecordAdd(_message.Message): + __slots__ = ("recordUid", "recordKey", "recordKeyType", "recordKeyEncryptedBy", "clientModifiedTime", "data", "nonSharedData", "folderUid", "recordLinks", "audit", "securityData", "securityScoreData") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + RECORDKEY_FIELD_NUMBER: _ClassVar[int] + RECORDKEYTYPE_FIELD_NUMBER: _ClassVar[int] + RECORDKEYENCRYPTEDBY_FIELD_NUMBER: _ClassVar[int] + CLIENTMODIFIEDTIME_FIELD_NUMBER: _ClassVar[int] + DATA_FIELD_NUMBER: _ClassVar[int] + NONSHAREDDATA_FIELD_NUMBER: _ClassVar[int] + FOLDERUID_FIELD_NUMBER: _ClassVar[int] + RECORDLINKS_FIELD_NUMBER: _ClassVar[int] + AUDIT_FIELD_NUMBER: _ClassVar[int] + SECURITYDATA_FIELD_NUMBER: _ClassVar[int] + SECURITYSCOREDATA_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + recordKey: bytes + recordKeyType: _folder_pb2.EncryptedKeyType + recordKeyEncryptedBy: _folder_pb2.FolderKeyEncryptionType + clientModifiedTime: int + data: bytes + nonSharedData: bytes + folderUid: bytes + recordLinks: _containers.RepeatedCompositeFieldContainer[_record_pb2.RecordLink] + audit: _record_pb2.RecordAudit + securityData: _record_pb2.SecurityData + securityScoreData: _record_pb2.SecurityScoreData + def __init__(self, recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., recordKeyType: _Optional[_Union[_folder_pb2.EncryptedKeyType, str]] = ..., recordKeyEncryptedBy: _Optional[_Union[_folder_pb2.FolderKeyEncryptionType, str]] = ..., clientModifiedTime: _Optional[int] = ..., data: _Optional[bytes] = ..., nonSharedData: _Optional[bytes] = ..., folderUid: _Optional[bytes] = ..., recordLinks: _Optional[_Iterable[_Union[_record_pb2.RecordLink, _Mapping]]] = ..., audit: _Optional[_Union[_record_pb2.RecordAudit, _Mapping]] = ..., securityData: _Optional[_Union[_record_pb2.SecurityData, _Mapping]] = ..., securityScoreData: _Optional[_Union[_record_pb2.SecurityScoreData, _Mapping]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/record_pb2.py b/keepersdk-package/src/keepersdk/proto/record_pb2.py index 8537d816..a07fc5a6 100644 --- a/keepersdk-package/src/keepersdk/proto/record_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/record_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: record.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'record.proto' ) @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crecord.proto\x12\x07Records\"\\\n\nRecordType\x12\x14\n\x0crecordTypeId\x18\x01 \x01(\x05\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\'\n\x05scope\x18\x03 \x01(\x0e\x32\x18.Records.RecordTypeScope\"U\n\x12RecordTypesRequest\x12\x10\n\x08standard\x18\x01 \x01(\x08\x12\x0c\n\x04user\x18\x02 \x01(\x08\x12\x12\n\nenterprise\x18\x03 \x01(\x08\x12\x0b\n\x03pam\x18\x04 \x01(\x08\"\x9c\x01\n\x13RecordTypesResponse\x12(\n\x0brecordTypes\x18\x01 \x03(\x0b\x32\x13.Records.RecordType\x12\x17\n\x0fstandardCounter\x18\x02 \x01(\x05\x12\x13\n\x0buserCounter\x18\x03 \x01(\x05\x12\x19\n\x11\x65nterpriseCounter\x18\x04 \x01(\x05\x12\x12\n\npamCounter\x18\x05 \x01(\x05\"A\n\x18RecordTypeModifyResponse\x12\x14\n\x0crecordTypeId\x18\x01 \x01(\x05\x12\x0f\n\x07\x63ounter\x18\x02 \x01(\x05\"=\n\x11RecordsGetRequest\x12\x13\n\x0brecord_uids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\xd1\x01\n\x06Record\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12/\n\x0frecord_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x05 \x01(\x0c\x12\x0f\n\x07version\x18\x06 \x01(\x05\x12\x1c\n\x14\x63lient_modified_time\x18\x07 \x01(\x03\x12\x10\n\x08revision\x18\x08 \x01(\x03\x12\x10\n\x08\x66ile_ids\x18\t \x03(\x0c\"M\n\x0f\x46olderRecordKey\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_uid\x18\x02 \x01(\x0c\x12\x12\n\nrecord_key\x18\x03 \x01(\x0c\"a\n\x06\x46older\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nfolder_key\x18\x02 \x01(\x0c\x12/\n\x0f\x66older_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\x95\x01\n\x04Team\x12\x10\n\x08team_uid\x18\x01 \x01(\x0c\x12\x10\n\x08team_key\x18\x02 \x01(\x0c\x12\x18\n\x10team_private_key\x18\x03 \x01(\x0c\x12-\n\rteam_key_type\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12 \n\x07\x66olders\x18\x05 \x03(\x0b\x32\x0f.Records.Folder\"\xac\x01\n\x12RecordsGetResponse\x12 \n\x07records\x18\x01 \x03(\x0b\x32\x0f.Records.Record\x12\x34\n\x12\x66older_record_keys\x18\x02 \x03(\x0b\x32\x18.Records.FolderRecordKey\x12 \n\x07\x66olders\x18\x03 \x03(\x0b\x32\x0f.Records.Folder\x12\x1c\n\x05teams\x18\x04 \x03(\x0b\x32\r.Records.Team\"4\n\nRecordLink\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\",\n\x0bRecordAudit\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1c\n\x0cSecurityData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"!\n\x11SecurityScoreData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x84\x03\n\tRecordAdd\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12.\n\x0b\x66older_type\x18\x06 \x01(\x0e\x32\x19.Records.RecordFolderType\x12\x12\n\nfolder_uid\x18\x07 \x01(\x0c\x12\x12\n\nfolder_key\x18\x08 \x01(\x0c\x12)\n\x0crecord_links\x18\t \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\n \x01(\x0b\x32\x14.Records.RecordAudit\x12+\n\x0csecurityData\x18\x0b \x01(\x0b\x32\x15.Records.SecurityData\x12\x35\n\x11securityScoreData\x18\x0c \x01(\x0b\x32\x1a.Records.SecurityScoreData\"\x85\x01\n\x11RecordsAddRequest\x12#\n\x07records\x18\x01 \x03(\x0b\x32\x12.Records.RecordAdd\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\x12\x36\n\x16security_data_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\xce\x02\n\x0cRecordUpdate\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x02 \x01(\x03\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12-\n\x10record_links_add\x18\x06 \x03(\x0b\x32\x13.Records.RecordLink\x12\x1b\n\x13record_links_remove\x18\x07 \x03(\x0c\x12#\n\x05\x61udit\x18\x08 \x01(\x0b\x32\x14.Records.RecordAudit\x12+\n\x0csecurityData\x18\t \x01(\x0b\x32\x15.Records.SecurityData\x12\x35\n\x11securityScoreData\x18\n \x01(\x0b\x32\x1a.Records.SecurityScoreData\"\x8b\x01\n\x14RecordsUpdateRequest\x12&\n\x07records\x18\x01 \x03(\x0b\x32\x15.Records.RecordUpdate\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\x12\x36\n\x16security_data_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\x8e\x01\n\x17RecordFileForConversion\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x66ile_file_id\x18\x02 \x01(\t\x12\x15\n\rthumb_file_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x12\n\nrecord_key\x18\x05 \x01(\x0c\x12\x10\n\x08link_key\x18\x06 \x01(\x0c\"J\n\x19RecordFolderForConversion\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x19\n\x11record_folder_key\x18\x02 \x01(\x0c\"\x92\x02\n\x11RecordConvertToV3\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x02 \x01(\x03\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12#\n\x05\x61udit\x18\x06 \x01(\x0b\x32\x14.Records.RecordAudit\x12\x35\n\x0brecord_file\x18\x07 \x03(\x0b\x32 .Records.RecordFileForConversion\x12\x36\n\nfolder_key\x18\x08 \x03(\x0b\x32\".Records.RecordFolderForConversion\"]\n\x19RecordsConvertToV3Request\x12+\n\x07records\x18\x01 \x03(\x0b\x32\x1a.Records.RecordConvertToV3\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\'\n\x14RecordsRemoveRequest\x12\x0f\n\x07records\x18\x01 \x03(\x0c\">\n\x0cRecordRevert\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1a\n\x12revert_to_revision\x18\x02 \x01(\x03\">\n\x14RecordsRevertRequest\x12&\n\x07records\x18\x01 \x03(\x0b\x32\x15.Records.RecordRevert\"c\n\x0fRecordLinkError\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.Records.RecordModifyResult\x12\x0f\n\x07message\x18\x03 \x01(\t\"\x95\x01\n\x12RecordModifyStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.Records.RecordModifyResult\x12\x0f\n\x07message\x18\x03 \x01(\t\x12-\n\x0blink_errors\x18\x04 \x03(\x0b\x32\x18.Records.RecordLinkError\"W\n\x15RecordsModifyResponse\x12,\n\x07records\x18\x01 \x03(\x0b\x32\x1b.Records.RecordModifyStatus\x12\x10\n\x08revision\x18\x02 \x01(\x03\"Y\n\x12RecordAddAuditData\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x0f\n\x07version\x18\x04 \x01(\x05\"C\n\x13\x41\x64\x64\x41uditDataRequest\x12,\n\x07records\x18\x01 \x03(\x0b\x32\x1b.Records.RecordAddAuditData\"t\n\x04\x46ile\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x10\n\x08\x66ileSize\x18\x04 \x01(\x03\x12\x11\n\tthumbSize\x18\x05 \x01(\x05\x12\x11\n\tis_script\x18\x06 \x01(\x08\"D\n\x0f\x46ilesAddRequest\x12\x1c\n\x05\x66iles\x18\x01 \x03(\x0b\x32\r.Records.File\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\xa7\x01\n\rFileAddStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12&\n\x06status\x18\x02 \x01(\x0e\x32\x16.Records.FileAddResult\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x12\n\nparameters\x18\x04 \x01(\t\x12\x1c\n\x14thumbnail_parameters\x18\x05 \x01(\t\x12\x1b\n\x13success_status_code\x18\x06 \x01(\x05\"K\n\x10\x46ilesAddResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Records.FileAddStatus\x12\x10\n\x08revision\x18\x02 \x01(\x03\"f\n\x0f\x46ilesGetRequest\x12\x13\n\x0brecord_uids\x18\x01 \x03(\x0c\x12\x16\n\x0e\x66or_thumbnails\x18\x02 \x01(\x08\x12&\n\x1e\x65mergency_access_account_owner\x18\x03 \x01(\t\"\xa2\x01\n\rFileGetStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12&\n\x06status\x18\x02 \x01(\x0e\x32\x16.Records.FileGetResult\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x1b\n\x13success_status_code\x18\x04 \x01(\x05\x12+\n\x0b\x66ileKeyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\"9\n\x10\x46ilesGetResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Records.FileGetStatus\"\x8d\x01\n\x15\x41pplicationAddRequest\x12\x0f\n\x07\x61pp_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"\x88\x01\n\"GetRecordDataWithAccessInfoRequest\x12\x12\n\nclientTime\x18\x01 \x01(\x03\x12\x11\n\trecordUid\x18\x02 \x03(\x0c\x12;\n\x14recordDetailsInclude\x18\x03 \x01(\x0e\x32\x1d.Records.RecordDetailsInclude\"\x86\x02\n\x0eUserPermission\x12\x10\n\x08username\x18\x01 \x01(\t\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x12\n\nshareAdmin\x18\x03 \x01(\x08\x12\x10\n\x08sharable\x18\x04 \x01(\x08\x12\x10\n\x08\x65\x64itable\x18\x05 \x01(\x08\x12\x18\n\x10\x61waitingApproval\x18\x06 \x01(\x08\x12\x12\n\nexpiration\x18\x07 \x01(\x03\x12\x12\n\naccountUid\x18\x08 \x01(\x0c\x12=\n\x15timerNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\n \x01(\x08\"\xd8\x01\n\x16SharedFolderPermission\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x12\n\nresharable\x18\x02 \x01(\x08\x12\x10\n\x08\x65\x64itable\x18\x03 \x01(\x08\x12\x10\n\x08revision\x18\x04 \x01(\x03\x12\x12\n\nexpiration\x18\x05 \x01(\x03\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x07 \x01(\x08\"\xe8\x02\n\nRecordData\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x0e\n\x06shared\x18\x03 \x01(\x08\x12\x1b\n\x13\x65ncryptedRecordData\x18\x04 \x01(\t\x12\x1a\n\x12\x65ncryptedExtraData\x18\x05 \x01(\t\x12\x1a\n\x12\x63lientModifiedTime\x18\x06 \x01(\x03\x12\x15\n\rnonSharedData\x18\x07 \x01(\t\x12-\n\x10linkedRecordData\x18\x08 \x03(\x0b\x32\x13.Records.RecordData\x12\x0e\n\x06\x66ileId\x18\t \x03(\x0c\x12\x10\n\x08\x66ileSize\x18\n \x01(\x03\x12\x15\n\rthumbnailSize\x18\x0b \x01(\x03\x12-\n\rrecordKeyType\x18\x0c \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x11\n\trecordKey\x18\r \x01(\x0c\x12\x11\n\trecordUid\x18\x0e \x01(\x0c\"\xc8\x01\n\x18RecordDataWithAccessInfo\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\'\n\nrecordData\x18\x02 \x01(\x0b\x32\x13.Records.RecordData\x12/\n\x0euserPermission\x18\x03 \x03(\x0b\x32\x17.Records.UserPermission\x12?\n\x16sharedFolderPermission\x18\x04 \x03(\x0b\x32\x1f.Records.SharedFolderPermission\"\x89\x01\n#GetRecordDataWithAccessInfoResponse\x12\x43\n\x18recordDataWithAccessInfo\x18\x01 \x03(\x0b\x32!.Records.RecordDataWithAccessInfo\x12\x1d\n\x15noPermissionRecordUid\x18\x02 \x03(\x0c\"j\n\x12IsObjectShareAdmin\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07isAdmin\x18\x02 \x01(\x08\x12\x36\n\nobjectType\x18\x03 \x01(\x0e\x32\".Records.CheckShareAdminObjectType\"H\n\rAmIShareAdmin\x12\x37\n\x12isObjectShareAdmin\x18\x01 \x03(\x0b\x32\x1b.Records.IsObjectShareAdmin\"\xbc\x01\n\x18RecordShareUpdateRequest\x12.\n\x0f\x61\x64\x64SharedRecord\x18\x01 \x03(\x0b\x32\x15.Records.SharedRecord\x12\x31\n\x12updateSharedRecord\x18\x02 \x03(\x0b\x32\x15.Records.SharedRecord\x12\x31\n\x12removeSharedRecord\x18\x03 \x03(\x0b\x32\x15.Records.SharedRecord\x12\n\n\x02pt\x18\x04 \x01(\t\"\xc4\x02\n\x0cSharedRecord\x12\x12\n\ntoUsername\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x04 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x05 \x01(\x0c\x12\x10\n\x08\x65\x64itable\x18\x06 \x01(\x08\x12\x11\n\tshareable\x18\x07 \x01(\x08\x12\x10\n\x08transfer\x18\x08 \x01(\x08\x12\x11\n\tuseEccKey\x18\t \x01(\x08\x12\x17\n\x0fremoveVaultData\x18\n \x01(\x08\x12\x12\n\nexpiration\x18\x0b \x01(\x03\x12=\n\x15timerNotificationType\x18\x0c \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\r \x01(\x08\"\xd5\x01\n\x19RecordShareUpdateResponse\x12:\n\x15\x61\x64\x64SharedRecordStatus\x18\x01 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\x12=\n\x18updateSharedRecordStatus\x18\x02 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\x12=\n\x18removeSharedRecordStatus\x18\x03 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\"Z\n\x12SharedRecordStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x10\n\x08username\x18\x04 \x01(\t\"G\n\x1bGetRecordPermissionsRequest\x12\x12\n\nrecordUids\x18\x01 \x03(\x0c\x12\x14\n\x0cisShareAdmin\x18\x02 \x01(\x08\"T\n\x1cGetRecordPermissionsResponse\x12\x34\n\x11recordPermissions\x18\x01 \x03(\x0b\x32\x19.Records.RecordPermission\"l\n\x10RecordPermission\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x03 \x01(\x08\x12\x10\n\x08\x63\x61nShare\x18\x04 \x01(\x08\x12\x13\n\x0b\x63\x61nTransfer\x18\x05 \x01(\x08\"h\n\x16GetShareObjectsRequest\x12\x11\n\tstartWith\x18\x01 \x01(\t\x12\x10\n\x08\x63ontains\x18\x02 \x01(\t\x12\x10\n\x08\x66iltered\x18\x03 \x01(\x08\x12\x17\n\x0fsharedFolderUid\x18\x04 \x01(\x0c\"\xe7\x02\n\x17GetShareObjectsResponse\x12.\n\x12shareRelationships\x18\x01 \x03(\x0b\x32\x12.Records.ShareUser\x12,\n\x10shareFamilyUsers\x18\x02 \x03(\x0b\x32\x12.Records.ShareUser\x12\x30\n\x14shareEnterpriseUsers\x18\x03 \x03(\x0b\x32\x12.Records.ShareUser\x12&\n\nshareTeams\x18\x04 \x03(\x0b\x32\x12.Records.ShareTeam\x12(\n\x0cshareMCTeams\x18\x05 \x03(\x0b\x32\x12.Records.ShareTeam\x12\x32\n\x16shareMCEnterpriseUsers\x18\x06 \x03(\x0b\x32\x12.Records.ShareUser\x12\x36\n\x14shareEnterpriseNames\x18\x07 \x03(\x0b\x32\x18.Records.ShareEnterprise\"\xa5\x01\n\tShareUser\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08\x66ullname\x18\x02 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x03 \x01(\x05\x12$\n\x06status\x18\x04 \x01(\x0e\x32\x14.Records.ShareStatus\x12\x14\n\x0cisShareAdmin\x18\x05 \x01(\x08\x12\"\n\x1aisAdminOfSharedFolderOwner\x18\x06 \x01(\x08\"D\n\tShareTeam\x12\x10\n\x08teamname\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\"?\n\x0fShareEnterprise\x12\x16\n\x0e\x65nterprisename\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\"S\n\x1fRecordsOnwershipTransferRequest\x12\x30\n\x0ftransferRecords\x18\x01 \x03(\x0b\x32\x17.Records.TransferRecord\"[\n\x0eTransferRecord\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x11\n\tuseEccKey\x18\x04 \x01(\x08\"_\n RecordsOnwershipTransferResponse\x12;\n\x14transferRecordStatus\x18\x01 \x03(\x0b\x32\x1d.Records.TransferRecordStatus\"\\\n\x14TransferRecordStatus\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\"y\n\x15RecordsUnshareRequest\x12\x34\n\rsharedFolders\x18\x01 \x03(\x0b\x32\x1d.Records.RecordsUnshareFolder\x12*\n\x05users\x18\x02 \x03(\x0b\x32\x1b.Records.RecordsUnshareUser\"\x86\x01\n\x16RecordsUnshareResponse\x12:\n\rsharedFolders\x18\x01 \x03(\x0b\x32#.Records.RecordsUnshareFolderStatus\x12\x30\n\x05users\x18\x02 \x03(\x0b\x32!.Records.RecordsUnshareUserStatus\"B\n\x14RecordsUnshareFolder\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\";\n\x12RecordsUnshareUser\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\"H\n\x1aRecordsUnshareFolderStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\"A\n\x18RecordsUnshareUserStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\"[\n\x1aTimedAccessCallbackPayload\x12=\n\x15timeLimitedAccessType\x18\x01 \x01(\x0e\x32\x1e.Records.TimeLimitedAccessType\"\xfd\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=\n\x15timeLimitedAccessType\x18\x05 \x01(\x0e\x32\x1e.Records.TimeLimitedAccessType\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12=\n\x15timerNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\"7\n\x17TimeLimitedAccessStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xe3\x01\n\x19TimeLimitedAccessResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12:\n\x10userAccessStatus\x18\x02 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus\x12:\n\x10teamAccessStatus\x18\x03 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus\x12<\n\x12recordAccessStatus\x18\x04 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus*h\n\x0fRecordTypeScope\x12\x0f\n\x0bRT_STANDARD\x10\x00\x12\x0b\n\x07RT_USER\x10\x01\x12\x11\n\rRT_ENTERPRISE\x10\x02\x12\n\n\x06RT_PAM\x10\x03\x12\x18\n\x14RT_PAM_CONFIGURATION\x10\x04*\xd1\x01\n\rRecordKeyType\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\x12\x1d\n\x19\x45NCRYPTED_BY_ROOT_KEY_CBC\x10\x05\x12\x1d\n\x19\x45NCRYPTED_BY_ROOT_KEY_GCM\x10\x06*P\n\x10RecordFolderType\x12\x0f\n\x0buser_folder\x10\x00\x12\x11\n\rshared_folder\x10\x01\x12\x18\n\x14shared_folder_folder\x10\x02*\xec\x02\n\x12RecordModifyResult\x12\x0e\n\nRS_SUCCESS\x10\x00\x12\x12\n\x0eRS_OUT_OF_SYNC\x10\x01\x12\x14\n\x10RS_ACCESS_DENIED\x10\x02\x12\x13\n\x0fRS_SHARE_DENIED\x10\x03\x12\x14\n\x10RS_RECORD_EXISTS\x10\x04\x12\x1e\n\x1aRS_OLD_RECORD_VERSION_TYPE\x10\x05\x12\x1e\n\x1aRS_NEW_RECORD_VERSION_TYPE\x10\x06\x12\x16\n\x12RS_FILES_NOT_MATCH\x10\x07\x12\x1b\n\x17RS_RECORD_NOT_SHAREABLE\x10\x08\x12\x1f\n\x1bRS_ATTACHMENT_NOT_SHAREABLE\x10\t\x12\x19\n\x15RS_FILE_LIMIT_REACHED\x10\n\x12\x1a\n\x16RS_SIZE_EXCEEDED_LIMIT\x10\x0b\x12$\n RS_ONLY_OWNER_CAN_MODIFY_SCRIPTS\x10\x0c*-\n\rFileAddResult\x12\x0e\n\nFA_SUCCESS\x10\x00\x12\x0c\n\x08\x46\x41_ERROR\x10\x01*C\n\rFileGetResult\x12\x0e\n\nFG_SUCCESS\x10\x00\x12\x0c\n\x08\x46G_ERROR\x10\x01\x12\x14\n\x10\x46G_ACCESS_DENIED\x10\x02*J\n\x14RecordDetailsInclude\x12\x13\n\x0f\x44\x41TA_PLUS_SHARE\x10\x00\x12\r\n\tDATA_ONLY\x10\x01\x12\x0e\n\nSHARE_ONLY\x10\x02*b\n\x19\x43heckShareAdminObjectType\x12\x19\n\x15\x43HECK_SA_INVALID_TYPE\x10\x00\x12\x12\n\x0e\x43HECK_SA_ON_SF\x10\x01\x12\x16\n\x12\x43HECK_SA_ON_RECORD\x10\x02*1\n\x0bShareStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\t\n\x05\x42LOCK\x10\x01\x12\x0b\n\x07INVITED\x10\x02*:\n\x15RecordTransactionType\x12\x0f\n\x0bRTT_GENERAL\x10\x00\x12\x10\n\x0cRTT_ROTATION\x10\x01*\xe6\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\x12\x1f\n\x1bUSER_ACCESS_TO_SHAREDFOLDER\x10\x04\x12\x1f\n\x1bTEAM_ACCESS_TO_SHAREDFOLDER\x10\x05*\\\n\x15TimerNotificationType\x12\x14\n\x10NOTIFICATION_OFF\x10\x00\x12\x10\n\x0cNOTIFY_OWNER\x10\x01\x12\x1b\n\x17NOTIFY_PRIVILEGED_USERS\x10\x02\x42#\n\x18\x63om.keepersecurity.protoB\x07Recordsb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crecord.proto\x12\x07Records\"\\\n\nRecordType\x12\x14\n\x0crecordTypeId\x18\x01 \x01(\x05\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\'\n\x05scope\x18\x03 \x01(\x0e\x32\x18.Records.RecordTypeScope\"U\n\x12RecordTypesRequest\x12\x10\n\x08standard\x18\x01 \x01(\x08\x12\x0c\n\x04user\x18\x02 \x01(\x08\x12\x12\n\nenterprise\x18\x03 \x01(\x08\x12\x0b\n\x03pam\x18\x04 \x01(\x08\"\x9c\x01\n\x13RecordTypesResponse\x12(\n\x0brecordTypes\x18\x01 \x03(\x0b\x32\x13.Records.RecordType\x12\x17\n\x0fstandardCounter\x18\x02 \x01(\x05\x12\x13\n\x0buserCounter\x18\x03 \x01(\x05\x12\x19\n\x11\x65nterpriseCounter\x18\x04 \x01(\x05\x12\x12\n\npamCounter\x18\x05 \x01(\x05\"A\n\x18RecordTypeModifyResponse\x12\x14\n\x0crecordTypeId\x18\x01 \x01(\x05\x12\x0f\n\x07\x63ounter\x18\x02 \x01(\x05\"=\n\x11RecordsGetRequest\x12\x13\n\x0brecord_uids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\xd1\x01\n\x06Record\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12/\n\x0frecord_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\r\n\x05\x65xtra\x18\x05 \x01(\x0c\x12\x0f\n\x07version\x18\x06 \x01(\x05\x12\x1c\n\x14\x63lient_modified_time\x18\x07 \x01(\x03\x12\x10\n\x08revision\x18\x08 \x01(\x03\x12\x10\n\x08\x66ile_ids\x18\t \x03(\x0c\"M\n\x0f\x46olderRecordKey\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_uid\x18\x02 \x01(\x0c\x12\x12\n\nrecord_key\x18\x03 \x01(\x0c\"a\n\x06\x46older\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nfolder_key\x18\x02 \x01(\x0c\x12/\n\x0f\x66older_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\x95\x01\n\x04Team\x12\x10\n\x08team_uid\x18\x01 \x01(\x0c\x12\x10\n\x08team_key\x18\x02 \x01(\x0c\x12\x18\n\x10team_private_key\x18\x03 \x01(\x0c\x12-\n\rteam_key_type\x18\x04 \x01(\x0e\x32\x16.Records.RecordKeyType\x12 \n\x07\x66olders\x18\x05 \x03(\x0b\x32\x0f.Records.Folder\"\xac\x01\n\x12RecordsGetResponse\x12 \n\x07records\x18\x01 \x03(\x0b\x32\x0f.Records.Record\x12\x34\n\x12\x66older_record_keys\x18\x02 \x03(\x0b\x32\x18.Records.FolderRecordKey\x12 \n\x07\x66olders\x18\x03 \x03(\x0b\x32\x0f.Records.Folder\x12\x1c\n\x05teams\x18\x04 \x03(\x0b\x32\r.Records.Team\"4\n\nRecordLink\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\",\n\x0bRecordAudit\x12\x0f\n\x07version\x18\x01 \x01(\x05\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\x1c\n\x0cSecurityData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"!\n\x11SecurityScoreData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"\x84\x03\n\tRecordAdd\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12.\n\x0b\x66older_type\x18\x06 \x01(\x0e\x32\x19.Records.RecordFolderType\x12\x12\n\nfolder_uid\x18\x07 \x01(\x0c\x12\x12\n\nfolder_key\x18\x08 \x01(\x0c\x12)\n\x0crecord_links\x18\t \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\n \x01(\x0b\x32\x14.Records.RecordAudit\x12+\n\x0csecurityData\x18\x0b \x01(\x0b\x32\x15.Records.SecurityData\x12\x35\n\x11securityScoreData\x18\x0c \x01(\x0b\x32\x1a.Records.SecurityScoreData\"\x85\x01\n\x11RecordsAddRequest\x12#\n\x07records\x18\x01 \x03(\x0b\x32\x12.Records.RecordAdd\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\x12\x36\n\x16security_data_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\xce\x02\n\x0cRecordUpdate\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x02 \x01(\x03\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12-\n\x10record_links_add\x18\x06 \x03(\x0b\x32\x13.Records.RecordLink\x12\x1b\n\x13record_links_remove\x18\x07 \x03(\x0c\x12#\n\x05\x61udit\x18\x08 \x01(\x0b\x32\x14.Records.RecordAudit\x12+\n\x0csecurityData\x18\t \x01(\x0b\x32\x15.Records.SecurityData\x12\x35\n\x11securityScoreData\x18\n \x01(\x0b\x32\x1a.Records.SecurityScoreData\"\x8b\x01\n\x14RecordsUpdateRequest\x12&\n\x07records\x18\x01 \x03(\x0b\x32\x15.Records.RecordUpdate\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\x12\x36\n\x16security_data_key_type\x18\x03 \x01(\x0e\x32\x16.Records.RecordKeyType\"\x8e\x01\n\x17RecordFileForConversion\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x14\n\x0c\x66ile_file_id\x18\x02 \x01(\t\x12\x15\n\rthumb_file_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x12\n\nrecord_key\x18\x05 \x01(\x0c\x12\x10\n\x08link_key\x18\x06 \x01(\x0c\"J\n\x19RecordFolderForConversion\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x19\n\x11record_folder_key\x18\x02 \x01(\x0c\"\x92\x02\n\x11RecordConvertToV3\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x02 \x01(\x03\x12\x10\n\x08revision\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x17\n\x0fnon_shared_data\x18\x05 \x01(\x0c\x12#\n\x05\x61udit\x18\x06 \x01(\x0b\x32\x14.Records.RecordAudit\x12\x35\n\x0brecord_file\x18\x07 \x03(\x0b\x32 .Records.RecordFileForConversion\x12\x36\n\nfolder_key\x18\x08 \x03(\x0b\x32\".Records.RecordFolderForConversion\"]\n\x19RecordsConvertToV3Request\x12+\n\x07records\x18\x01 \x03(\x0b\x32\x1a.Records.RecordConvertToV3\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\'\n\x14RecordsRemoveRequest\x12\x0f\n\x07records\x18\x01 \x03(\x0c\">\n\x0cRecordRevert\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x1a\n\x12revert_to_revision\x18\x02 \x01(\x03\">\n\x14RecordsRevertRequest\x12&\n\x07records\x18\x01 \x03(\x0b\x32\x15.Records.RecordRevert\"c\n\x0fRecordLinkError\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.Records.RecordModifyResult\x12\x0f\n\x07message\x18\x03 \x01(\t\"\x95\x01\n\x12RecordModifyStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12+\n\x06status\x18\x02 \x01(\x0e\x32\x1b.Records.RecordModifyResult\x12\x0f\n\x07message\x18\x03 \x01(\t\x12-\n\x0blink_errors\x18\x04 \x03(\x0b\x32\x18.Records.RecordLinkError\"W\n\x15RecordsModifyResponse\x12,\n\x07records\x18\x01 \x03(\x0b\x32\x1b.Records.RecordModifyStatus\x12\x10\n\x08revision\x18\x02 \x01(\x03\"Y\n\x12RecordAddAuditData\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x10\n\x08revision\x18\x02 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x0f\n\x07version\x18\x04 \x01(\x05\"C\n\x13\x41\x64\x64\x41uditDataRequest\x12,\n\x07records\x18\x01 \x03(\x0b\x32\x1b.Records.RecordAddAuditData\"t\n\x04\x46ile\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x10\n\x08\x66ileSize\x18\x04 \x01(\x03\x12\x11\n\tthumbSize\x18\x05 \x01(\x05\x12\x11\n\tis_script\x18\x06 \x01(\x08\"D\n\x0f\x46ilesAddRequest\x12\x1c\n\x05\x66iles\x18\x01 \x03(\x0b\x32\r.Records.File\x12\x13\n\x0b\x63lient_time\x18\x02 \x01(\x03\"\xa7\x01\n\rFileAddStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12&\n\x06status\x18\x02 \x01(\x0e\x32\x16.Records.FileAddResult\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x12\n\nparameters\x18\x04 \x01(\t\x12\x1c\n\x14thumbnail_parameters\x18\x05 \x01(\t\x12\x1b\n\x13success_status_code\x18\x06 \x01(\x05\"K\n\x10\x46ilesAddResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Records.FileAddStatus\x12\x10\n\x08revision\x18\x02 \x01(\x03\"f\n\x0f\x46ilesGetRequest\x12\x13\n\x0brecord_uids\x18\x01 \x03(\x0c\x12\x16\n\x0e\x66or_thumbnails\x18\x02 \x01(\x08\x12&\n\x1e\x65mergency_access_account_owner\x18\x03 \x01(\t\"\xa2\x01\n\rFileGetStatus\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12&\n\x06status\x18\x02 \x01(\x0e\x32\x16.Records.FileGetResult\x12\x0b\n\x03url\x18\x03 \x01(\t\x12\x1b\n\x13success_status_code\x18\x04 \x01(\x05\x12+\n\x0b\x66ileKeyType\x18\x05 \x01(\x0e\x32\x16.Records.RecordKeyType\"9\n\x10\x46ilesGetResponse\x12%\n\x05\x66iles\x18\x01 \x03(\x0b\x32\x16.Records.FileGetStatus\"\x8d\x01\n\x15\x41pplicationAddRequest\x12\x0f\n\x07\x61pp_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_key\x18\x02 \x01(\x0c\x12\x1c\n\x14\x63lient_modified_time\x18\x03 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"\x88\x01\n\"GetRecordDataWithAccessInfoRequest\x12\x12\n\nclientTime\x18\x01 \x01(\x03\x12\x11\n\trecordUid\x18\x02 \x03(\x0c\x12;\n\x14recordDetailsInclude\x18\x03 \x01(\x0e\x32\x1d.Records.RecordDetailsInclude\"\x86\x02\n\x0eUserPermission\x12\x10\n\x08username\x18\x01 \x01(\t\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x12\n\nshareAdmin\x18\x03 \x01(\x08\x12\x10\n\x08sharable\x18\x04 \x01(\x08\x12\x10\n\x08\x65\x64itable\x18\x05 \x01(\x08\x12\x18\n\x10\x61waitingApproval\x18\x06 \x01(\x08\x12\x12\n\nexpiration\x18\x07 \x01(\x03\x12\x12\n\naccountUid\x18\x08 \x01(\x0c\x12=\n\x15timerNotificationType\x18\t \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\n \x01(\x08\"\xd8\x01\n\x16SharedFolderPermission\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12\x12\n\nresharable\x18\x02 \x01(\x08\x12\x10\n\x08\x65\x64itable\x18\x03 \x01(\x08\x12\x10\n\x08revision\x18\x04 \x01(\x03\x12\x12\n\nexpiration\x18\x05 \x01(\x03\x12=\n\x15timerNotificationType\x18\x06 \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x07 \x01(\x08\"\xe8\x02\n\nRecordData\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x0e\n\x06shared\x18\x03 \x01(\x08\x12\x1b\n\x13\x65ncryptedRecordData\x18\x04 \x01(\t\x12\x1a\n\x12\x65ncryptedExtraData\x18\x05 \x01(\t\x12\x1a\n\x12\x63lientModifiedTime\x18\x06 \x01(\x03\x12\x15\n\rnonSharedData\x18\x07 \x01(\t\x12-\n\x10linkedRecordData\x18\x08 \x03(\x0b\x32\x13.Records.RecordData\x12\x0e\n\x06\x66ileId\x18\t \x03(\x0c\x12\x10\n\x08\x66ileSize\x18\n \x01(\x03\x12\x15\n\rthumbnailSize\x18\x0b \x01(\x03\x12-\n\rrecordKeyType\x18\x0c \x01(\x0e\x32\x16.Records.RecordKeyType\x12\x11\n\trecordKey\x18\r \x01(\x0c\x12\x11\n\trecordUid\x18\x0e \x01(\x0c\"\xc8\x01\n\x18RecordDataWithAccessInfo\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\'\n\nrecordData\x18\x02 \x01(\x0b\x32\x13.Records.RecordData\x12/\n\x0euserPermission\x18\x03 \x03(\x0b\x32\x17.Records.UserPermission\x12?\n\x16sharedFolderPermission\x18\x04 \x03(\x0b\x32\x1f.Records.SharedFolderPermission\"\x89\x01\n#GetRecordDataWithAccessInfoResponse\x12\x43\n\x18recordDataWithAccessInfo\x18\x01 \x03(\x0b\x32!.Records.RecordDataWithAccessInfo\x12\x1d\n\x15noPermissionRecordUid\x18\x02 \x03(\x0c\"j\n\x12IsObjectShareAdmin\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07isAdmin\x18\x02 \x01(\x08\x12\x36\n\nobjectType\x18\x03 \x01(\x0e\x32\".Records.CheckShareAdminObjectType\"H\n\rAmIShareAdmin\x12\x37\n\x12isObjectShareAdmin\x18\x01 \x03(\x0b\x32\x1b.Records.IsObjectShareAdmin\"\xbc\x01\n\x18RecordShareUpdateRequest\x12.\n\x0f\x61\x64\x64SharedRecord\x18\x01 \x03(\x0b\x32\x15.Records.SharedRecord\x12\x31\n\x12updateSharedRecord\x18\x02 \x03(\x0b\x32\x15.Records.SharedRecord\x12\x31\n\x12removeSharedRecord\x18\x03 \x03(\x0b\x32\x15.Records.SharedRecord\x12\n\n\x02pt\x18\x04 \x01(\t\"\xc4\x02\n\x0cSharedRecord\x12\x12\n\ntoUsername\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x04 \x01(\x0c\x12\x0f\n\x07teamUid\x18\x05 \x01(\x0c\x12\x10\n\x08\x65\x64itable\x18\x06 \x01(\x08\x12\x11\n\tshareable\x18\x07 \x01(\x08\x12\x10\n\x08transfer\x18\x08 \x01(\x08\x12\x11\n\tuseEccKey\x18\t \x01(\x08\x12\x17\n\x0fremoveVaultData\x18\n \x01(\x08\x12\x12\n\nexpiration\x18\x0b \x01(\x03\x12=\n\x15timerNotificationType\x18\x0c \x01(\x0e\x32\x1e.Records.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\r \x01(\x08\"\xd5\x01\n\x19RecordShareUpdateResponse\x12:\n\x15\x61\x64\x64SharedRecordStatus\x18\x01 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\x12=\n\x18updateSharedRecordStatus\x18\x02 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\x12=\n\x18removeSharedRecordStatus\x18\x03 \x03(\x0b\x32\x1b.Records.SharedRecordStatus\"Z\n\x12SharedRecordStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x0e\n\x06status\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x10\n\x08username\x18\x04 \x01(\t\"G\n\x1bGetRecordPermissionsRequest\x12\x12\n\nrecordUids\x18\x01 \x03(\x0c\x12\x14\n\x0cisShareAdmin\x18\x02 \x01(\x08\"T\n\x1cGetRecordPermissionsResponse\x12\x34\n\x11recordPermissions\x18\x01 \x03(\x0b\x32\x19.Records.RecordPermission\"l\n\x10RecordPermission\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\r\n\x05owner\x18\x02 \x01(\x08\x12\x0f\n\x07\x63\x61nEdit\x18\x03 \x01(\x08\x12\x10\n\x08\x63\x61nShare\x18\x04 \x01(\x08\x12\x13\n\x0b\x63\x61nTransfer\x18\x05 \x01(\x08\"h\n\x16GetShareObjectsRequest\x12\x11\n\tstartWith\x18\x01 \x01(\t\x12\x10\n\x08\x63ontains\x18\x02 \x01(\t\x12\x10\n\x08\x66iltered\x18\x03 \x01(\x08\x12\x17\n\x0fsharedFolderUid\x18\x04 \x01(\x0c\"\xe7\x02\n\x17GetShareObjectsResponse\x12.\n\x12shareRelationships\x18\x01 \x03(\x0b\x32\x12.Records.ShareUser\x12,\n\x10shareFamilyUsers\x18\x02 \x03(\x0b\x32\x12.Records.ShareUser\x12\x30\n\x14shareEnterpriseUsers\x18\x03 \x03(\x0b\x32\x12.Records.ShareUser\x12&\n\nshareTeams\x18\x04 \x03(\x0b\x32\x12.Records.ShareTeam\x12(\n\x0cshareMCTeams\x18\x05 \x03(\x0b\x32\x12.Records.ShareTeam\x12\x32\n\x16shareMCEnterpriseUsers\x18\x06 \x03(\x0b\x32\x12.Records.ShareUser\x12\x36\n\x14shareEnterpriseNames\x18\x07 \x03(\x0b\x32\x18.Records.ShareEnterprise\"\xbd\x01\n\tShareUser\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08\x66ullname\x18\x02 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x03 \x01(\x05\x12$\n\x06status\x18\x04 \x01(\x0e\x32\x14.Records.ShareStatus\x12\x14\n\x0cisShareAdmin\x18\x05 \x01(\x08\x12\"\n\x1aisAdminOfSharedFolderOwner\x18\x06 \x01(\x08\x12\x16\n\x0euserAccountUid\x18\x07 \x01(\x0c\"D\n\tShareTeam\x12\x10\n\x08teamname\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\x12\x0f\n\x07teamUid\x18\x03 \x01(\x0c\"?\n\x0fShareEnterprise\x12\x16\n\x0e\x65nterprisename\x18\x01 \x01(\t\x12\x14\n\x0c\x65nterpriseId\x18\x02 \x01(\x05\"S\n\x1fRecordsOnwershipTransferRequest\x12\x30\n\x0ftransferRecords\x18\x01 \x03(\x0b\x32\x17.Records.TransferRecord\"[\n\x0eTransferRecord\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x11\n\trecordKey\x18\x03 \x01(\x0c\x12\x11\n\tuseEccKey\x18\x04 \x01(\x08\"_\n RecordsOnwershipTransferResponse\x12;\n\x14transferRecordStatus\x18\x01 \x03(\x0b\x32\x1d.Records.TransferRecordStatus\"\\\n\x14TransferRecordStatus\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x0f\n\x07message\x18\x04 \x01(\t\"y\n\x15RecordsUnshareRequest\x12\x34\n\rsharedFolders\x18\x01 \x03(\x0b\x32\x1d.Records.RecordsUnshareFolder\x12*\n\x05users\x18\x02 \x03(\x0b\x32\x1b.Records.RecordsUnshareUser\"\x86\x01\n\x16RecordsUnshareResponse\x12:\n\rsharedFolders\x18\x01 \x03(\x0b\x32#.Records.RecordsUnshareFolderStatus\x12\x30\n\x05users\x18\x02 \x03(\x0b\x32!.Records.RecordsUnshareUserStatus\"B\n\x14RecordsUnshareFolder\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\";\n\x12RecordsUnshareUser\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\"H\n\x1aRecordsUnshareFolderStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\x0fsharedFolderUid\x18\x02 \x01(\x0c\"A\n\x18RecordsUnshareUserStatus\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x12\n\naccountUid\x18\x02 \x01(\x0c\"[\n\x1aTimedAccessCallbackPayload\x12=\n\x15timeLimitedAccessType\x18\x01 \x01(\x0e\x32\x1e.Records.TimeLimitedAccessType\"\xfd\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=\n\x15timeLimitedAccessType\x18\x05 \x01(\x0e\x32\x1e.Records.TimeLimitedAccessType\x12\x12\n\nexpiration\x18\x06 \x01(\x03\x12=\n\x15timerNotificationType\x18\x07 \x01(\x0e\x32\x1e.Records.TimerNotificationType\"7\n\x17TimeLimitedAccessStatus\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xe3\x01\n\x19TimeLimitedAccessResponse\x12\x10\n\x08revision\x18\x01 \x01(\x03\x12:\n\x10userAccessStatus\x18\x02 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus\x12:\n\x10teamAccessStatus\x18\x03 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus\x12<\n\x12recordAccessStatus\x18\x04 \x03(\x0b\x32 .Records.TimeLimitedAccessStatus*h\n\x0fRecordTypeScope\x12\x0f\n\x0bRT_STANDARD\x10\x00\x12\x0b\n\x07RT_USER\x10\x01\x12\x11\n\rRT_ENTERPRISE\x10\x02\x12\n\n\x06RT_PAM\x10\x03\x12\x18\n\x14RT_PAM_CONFIGURATION\x10\x04*\xd1\x01\n\rRecordKeyType\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\x12\x1d\n\x19\x45NCRYPTED_BY_ROOT_KEY_CBC\x10\x05\x12\x1d\n\x19\x45NCRYPTED_BY_ROOT_KEY_GCM\x10\x06*P\n\x10RecordFolderType\x12\x0f\n\x0buser_folder\x10\x00\x12\x11\n\rshared_folder\x10\x01\x12\x18\n\x14shared_folder_folder\x10\x02*\xec\x02\n\x12RecordModifyResult\x12\x0e\n\nRS_SUCCESS\x10\x00\x12\x12\n\x0eRS_OUT_OF_SYNC\x10\x01\x12\x14\n\x10RS_ACCESS_DENIED\x10\x02\x12\x13\n\x0fRS_SHARE_DENIED\x10\x03\x12\x14\n\x10RS_RECORD_EXISTS\x10\x04\x12\x1e\n\x1aRS_OLD_RECORD_VERSION_TYPE\x10\x05\x12\x1e\n\x1aRS_NEW_RECORD_VERSION_TYPE\x10\x06\x12\x16\n\x12RS_FILES_NOT_MATCH\x10\x07\x12\x1b\n\x17RS_RECORD_NOT_SHAREABLE\x10\x08\x12\x1f\n\x1bRS_ATTACHMENT_NOT_SHAREABLE\x10\t\x12\x19\n\x15RS_FILE_LIMIT_REACHED\x10\n\x12\x1a\n\x16RS_SIZE_EXCEEDED_LIMIT\x10\x0b\x12$\n RS_ONLY_OWNER_CAN_MODIFY_SCRIPTS\x10\x0c*-\n\rFileAddResult\x12\x0e\n\nFA_SUCCESS\x10\x00\x12\x0c\n\x08\x46\x41_ERROR\x10\x01*C\n\rFileGetResult\x12\x0e\n\nFG_SUCCESS\x10\x00\x12\x0c\n\x08\x46G_ERROR\x10\x01\x12\x14\n\x10\x46G_ACCESS_DENIED\x10\x02*J\n\x14RecordDetailsInclude\x12\x13\n\x0f\x44\x41TA_PLUS_SHARE\x10\x00\x12\r\n\tDATA_ONLY\x10\x01\x12\x0e\n\nSHARE_ONLY\x10\x02*b\n\x19\x43heckShareAdminObjectType\x12\x19\n\x15\x43HECK_SA_INVALID_TYPE\x10\x00\x12\x12\n\x0e\x43HECK_SA_ON_SF\x10\x01\x12\x16\n\x12\x43HECK_SA_ON_RECORD\x10\x02*1\n\x0bShareStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\t\n\x05\x42LOCK\x10\x01\x12\x0b\n\x07INVITED\x10\x02*:\n\x15RecordTransactionType\x12\x0f\n\x0bRTT_GENERAL\x10\x00\x12\x10\n\x0cRTT_ROTATION\x10\x01*\xdc\x02\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\x12\x1f\n\x1bUSER_ACCESS_TO_SHAREDFOLDER\x10\x04\x12\x1f\n\x1bTEAM_ACCESS_TO_SHAREDFOLDER\x10\x05\x12\x1b\n\x17RECORD_ACCESS_TO_FOLDER\x10\x06\x12\x19\n\x15USER_ACCESS_TO_FOLDER\x10\x07\x12\x19\n\x15TEAM_ACCESS_TO_FOLDER\x10\x08\x12!\n\x1dUSER_OR_TEAM_ACCESS_TO_FOLDER\x10\t*\\\n\x15TimerNotificationType\x12\x14\n\x10NOTIFICATION_OFF\x10\x00\x12\x10\n\x0cNOTIFY_OWNER\x10\x01\x12\x1b\n\x17NOTIFY_PRIVILEGED_USERS\x10\x02\x42#\n\x18\x63om.keepersecurity.protoB\x07Recordsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -32,30 +32,30 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\007Records' - _globals['_RECORDTYPESCOPE']._serialized_start=9466 - _globals['_RECORDTYPESCOPE']._serialized_end=9570 - _globals['_RECORDKEYTYPE']._serialized_start=9573 - _globals['_RECORDKEYTYPE']._serialized_end=9782 - _globals['_RECORDFOLDERTYPE']._serialized_start=9784 - _globals['_RECORDFOLDERTYPE']._serialized_end=9864 - _globals['_RECORDMODIFYRESULT']._serialized_start=9867 - _globals['_RECORDMODIFYRESULT']._serialized_end=10231 - _globals['_FILEADDRESULT']._serialized_start=10233 - _globals['_FILEADDRESULT']._serialized_end=10278 - _globals['_FILEGETRESULT']._serialized_start=10280 - _globals['_FILEGETRESULT']._serialized_end=10347 - _globals['_RECORDDETAILSINCLUDE']._serialized_start=10349 - _globals['_RECORDDETAILSINCLUDE']._serialized_end=10423 - _globals['_CHECKSHAREADMINOBJECTTYPE']._serialized_start=10425 - _globals['_CHECKSHAREADMINOBJECTTYPE']._serialized_end=10523 - _globals['_SHARESTATUS']._serialized_start=10525 - _globals['_SHARESTATUS']._serialized_end=10574 - _globals['_RECORDTRANSACTIONTYPE']._serialized_start=10576 - _globals['_RECORDTRANSACTIONTYPE']._serialized_end=10634 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=10637 - _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=10867 - _globals['_TIMERNOTIFICATIONTYPE']._serialized_start=10869 - _globals['_TIMERNOTIFICATIONTYPE']._serialized_end=10961 + _globals['_RECORDTYPESCOPE']._serialized_start=9490 + _globals['_RECORDTYPESCOPE']._serialized_end=9594 + _globals['_RECORDKEYTYPE']._serialized_start=9597 + _globals['_RECORDKEYTYPE']._serialized_end=9806 + _globals['_RECORDFOLDERTYPE']._serialized_start=9808 + _globals['_RECORDFOLDERTYPE']._serialized_end=9888 + _globals['_RECORDMODIFYRESULT']._serialized_start=9891 + _globals['_RECORDMODIFYRESULT']._serialized_end=10255 + _globals['_FILEADDRESULT']._serialized_start=10257 + _globals['_FILEADDRESULT']._serialized_end=10302 + _globals['_FILEGETRESULT']._serialized_start=10304 + _globals['_FILEGETRESULT']._serialized_end=10371 + _globals['_RECORDDETAILSINCLUDE']._serialized_start=10373 + _globals['_RECORDDETAILSINCLUDE']._serialized_end=10447 + _globals['_CHECKSHAREADMINOBJECTTYPE']._serialized_start=10449 + _globals['_CHECKSHAREADMINOBJECTTYPE']._serialized_end=10547 + _globals['_SHARESTATUS']._serialized_start=10549 + _globals['_SHARESTATUS']._serialized_end=10598 + _globals['_RECORDTRANSACTIONTYPE']._serialized_start=10600 + _globals['_RECORDTRANSACTIONTYPE']._serialized_end=10658 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_start=10661 + _globals['_TIMELIMITEDACCESSTYPE']._serialized_end=11009 + _globals['_TIMERNOTIFICATIONTYPE']._serialized_start=11011 + _globals['_TIMERNOTIFICATIONTYPE']._serialized_end=11103 _globals['_RECORDTYPE']._serialized_start=25 _globals['_RECORDTYPE']._serialized_end=117 _globals['_RECORDTYPESREQUEST']._serialized_start=119 @@ -167,37 +167,37 @@ _globals['_GETSHAREOBJECTSRESPONSE']._serialized_start=7267 _globals['_GETSHAREOBJECTSRESPONSE']._serialized_end=7626 _globals['_SHAREUSER']._serialized_start=7629 - _globals['_SHAREUSER']._serialized_end=7794 - _globals['_SHARETEAM']._serialized_start=7796 - _globals['_SHARETEAM']._serialized_end=7864 - _globals['_SHAREENTERPRISE']._serialized_start=7866 - _globals['_SHAREENTERPRISE']._serialized_end=7929 - _globals['_RECORDSONWERSHIPTRANSFERREQUEST']._serialized_start=7931 - _globals['_RECORDSONWERSHIPTRANSFERREQUEST']._serialized_end=8014 - _globals['_TRANSFERRECORD']._serialized_start=8016 - _globals['_TRANSFERRECORD']._serialized_end=8107 - _globals['_RECORDSONWERSHIPTRANSFERRESPONSE']._serialized_start=8109 - _globals['_RECORDSONWERSHIPTRANSFERRESPONSE']._serialized_end=8204 - _globals['_TRANSFERRECORDSTATUS']._serialized_start=8206 - _globals['_TRANSFERRECORDSTATUS']._serialized_end=8298 - _globals['_RECORDSUNSHAREREQUEST']._serialized_start=8300 - _globals['_RECORDSUNSHAREREQUEST']._serialized_end=8421 - _globals['_RECORDSUNSHARERESPONSE']._serialized_start=8424 - _globals['_RECORDSUNSHARERESPONSE']._serialized_end=8558 - _globals['_RECORDSUNSHAREFOLDER']._serialized_start=8560 - _globals['_RECORDSUNSHAREFOLDER']._serialized_end=8626 - _globals['_RECORDSUNSHAREUSER']._serialized_start=8628 - _globals['_RECORDSUNSHAREUSER']._serialized_end=8687 - _globals['_RECORDSUNSHAREFOLDERSTATUS']._serialized_start=8689 - _globals['_RECORDSUNSHAREFOLDERSTATUS']._serialized_end=8761 - _globals['_RECORDSUNSHAREUSERSTATUS']._serialized_start=8763 - _globals['_RECORDSUNSHAREUSERSTATUS']._serialized_end=8828 - _globals['_TIMEDACCESSCALLBACKPAYLOAD']._serialized_start=8830 - _globals['_TIMEDACCESSCALLBACKPAYLOAD']._serialized_end=8921 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=8924 - _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=9177 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=9179 - _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=9234 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=9237 - _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=9464 + _globals['_SHAREUSER']._serialized_end=7818 + _globals['_SHARETEAM']._serialized_start=7820 + _globals['_SHARETEAM']._serialized_end=7888 + _globals['_SHAREENTERPRISE']._serialized_start=7890 + _globals['_SHAREENTERPRISE']._serialized_end=7953 + _globals['_RECORDSONWERSHIPTRANSFERREQUEST']._serialized_start=7955 + _globals['_RECORDSONWERSHIPTRANSFERREQUEST']._serialized_end=8038 + _globals['_TRANSFERRECORD']._serialized_start=8040 + _globals['_TRANSFERRECORD']._serialized_end=8131 + _globals['_RECORDSONWERSHIPTRANSFERRESPONSE']._serialized_start=8133 + _globals['_RECORDSONWERSHIPTRANSFERRESPONSE']._serialized_end=8228 + _globals['_TRANSFERRECORDSTATUS']._serialized_start=8230 + _globals['_TRANSFERRECORDSTATUS']._serialized_end=8322 + _globals['_RECORDSUNSHAREREQUEST']._serialized_start=8324 + _globals['_RECORDSUNSHAREREQUEST']._serialized_end=8445 + _globals['_RECORDSUNSHARERESPONSE']._serialized_start=8448 + _globals['_RECORDSUNSHARERESPONSE']._serialized_end=8582 + _globals['_RECORDSUNSHAREFOLDER']._serialized_start=8584 + _globals['_RECORDSUNSHAREFOLDER']._serialized_end=8650 + _globals['_RECORDSUNSHAREUSER']._serialized_start=8652 + _globals['_RECORDSUNSHAREUSER']._serialized_end=8711 + _globals['_RECORDSUNSHAREFOLDERSTATUS']._serialized_start=8713 + _globals['_RECORDSUNSHAREFOLDERSTATUS']._serialized_end=8785 + _globals['_RECORDSUNSHAREUSERSTATUS']._serialized_start=8787 + _globals['_RECORDSUNSHAREUSERSTATUS']._serialized_end=8852 + _globals['_TIMEDACCESSCALLBACKPAYLOAD']._serialized_start=8854 + _globals['_TIMEDACCESSCALLBACKPAYLOAD']._serialized_end=8945 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_start=8948 + _globals['_TIMELIMITEDACCESSREQUEST']._serialized_end=9201 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_start=9203 + _globals['_TIMELIMITEDACCESSSTATUS']._serialized_end=9258 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_start=9261 + _globals['_TIMELIMITEDACCESSRESPONSE']._serialized_end=9488 # @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/record_pb2.pyi b/keepersdk-package/src/keepersdk/proto/record_pb2.pyi index bac4c4a9..19b58230 100644 --- a/keepersdk-package/src/keepersdk/proto/record_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/record_pb2.pyi @@ -2,8 +2,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -89,6 +88,10 @@ class TimeLimitedAccessType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): RECORD_ACCESS_TO_SHAREDFOLDER: _ClassVar[TimeLimitedAccessType] USER_ACCESS_TO_SHAREDFOLDER: _ClassVar[TimeLimitedAccessType] TEAM_ACCESS_TO_SHAREDFOLDER: _ClassVar[TimeLimitedAccessType] + RECORD_ACCESS_TO_FOLDER: _ClassVar[TimeLimitedAccessType] + USER_ACCESS_TO_FOLDER: _ClassVar[TimeLimitedAccessType] + TEAM_ACCESS_TO_FOLDER: _ClassVar[TimeLimitedAccessType] + USER_OR_TEAM_ACCESS_TO_FOLDER: _ClassVar[TimeLimitedAccessType] class TimerNotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -145,6 +148,10 @@ USER_OR_TEAM_ACCESS_TO_SHAREDFOLDER: TimeLimitedAccessType RECORD_ACCESS_TO_SHAREDFOLDER: TimeLimitedAccessType USER_ACCESS_TO_SHAREDFOLDER: TimeLimitedAccessType TEAM_ACCESS_TO_SHAREDFOLDER: TimeLimitedAccessType +RECORD_ACCESS_TO_FOLDER: TimeLimitedAccessType +USER_ACCESS_TO_FOLDER: TimeLimitedAccessType +TEAM_ACCESS_TO_FOLDER: TimeLimitedAccessType +USER_OR_TEAM_ACCESS_TO_FOLDER: TimeLimitedAccessType NOTIFICATION_OFF: TimerNotificationType NOTIFY_OWNER: TimerNotificationType NOTIFY_PRIVILEGED_USERS: TimerNotificationType @@ -169,7 +176,7 @@ class RecordTypesRequest(_message.Message): user: bool enterprise: bool pam: bool - def __init__(self, standard: _Optional[bool] = ..., user: _Optional[bool] = ..., enterprise: _Optional[bool] = ..., pam: _Optional[bool] = ...) -> None: ... + def __init__(self, standard: bool = ..., user: bool = ..., enterprise: bool = ..., pam: bool = ...) -> None: ... class RecordTypesResponse(_message.Message): __slots__ = ("recordTypes", "standardCounter", "userCounter", "enterpriseCounter", "pamCounter") @@ -503,7 +510,7 @@ class File(_message.Message): fileSize: int thumbSize: int is_script: bool - def __init__(self, record_uid: _Optional[bytes] = ..., record_key: _Optional[bytes] = ..., data: _Optional[bytes] = ..., fileSize: _Optional[int] = ..., thumbSize: _Optional[int] = ..., is_script: _Optional[bool] = ...) -> None: ... + def __init__(self, record_uid: _Optional[bytes] = ..., record_key: _Optional[bytes] = ..., data: _Optional[bytes] = ..., fileSize: _Optional[int] = ..., thumbSize: _Optional[int] = ..., is_script: bool = ...) -> None: ... class FilesAddRequest(_message.Message): __slots__ = ("files", "client_time") @@ -545,7 +552,7 @@ class FilesGetRequest(_message.Message): record_uids: _containers.RepeatedScalarFieldContainer[bytes] for_thumbnails: bool emergency_access_account_owner: str - def __init__(self, record_uids: _Optional[_Iterable[bytes]] = ..., for_thumbnails: _Optional[bool] = ..., emergency_access_account_owner: _Optional[str] = ...) -> None: ... + def __init__(self, record_uids: _Optional[_Iterable[bytes]] = ..., for_thumbnails: bool = ..., emergency_access_account_owner: _Optional[str] = ...) -> None: ... class FileGetStatus(_message.Message): __slots__ = ("record_uid", "status", "url", "success_status_code", "fileKeyType") @@ -613,7 +620,7 @@ class UserPermission(_message.Message): accountUid: bytes timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, username: _Optional[str] = ..., owner: _Optional[bool] = ..., shareAdmin: _Optional[bool] = ..., sharable: _Optional[bool] = ..., editable: _Optional[bool] = ..., awaitingApproval: _Optional[bool] = ..., expiration: _Optional[int] = ..., accountUid: _Optional[bytes] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., owner: bool = ..., shareAdmin: bool = ..., sharable: bool = ..., editable: bool = ..., awaitingApproval: bool = ..., expiration: _Optional[int] = ..., accountUid: _Optional[bytes] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class SharedFolderPermission(_message.Message): __slots__ = ("sharedFolderUid", "resharable", "editable", "revision", "expiration", "timerNotificationType", "rotateOnExpiration") @@ -631,7 +638,7 @@ class SharedFolderPermission(_message.Message): expiration: int timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, sharedFolderUid: _Optional[bytes] = ..., resharable: _Optional[bool] = ..., editable: _Optional[bool] = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., resharable: bool = ..., editable: bool = ..., revision: _Optional[int] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class RecordData(_message.Message): __slots__ = ("revision", "version", "shared", "encryptedRecordData", "encryptedExtraData", "clientModifiedTime", "nonSharedData", "linkedRecordData", "fileId", "fileSize", "thumbnailSize", "recordKeyType", "recordKey", "recordUid") @@ -663,7 +670,7 @@ class RecordData(_message.Message): recordKeyType: RecordKeyType recordKey: bytes recordUid: bytes - def __init__(self, revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: _Optional[bool] = ..., encryptedRecordData: _Optional[str] = ..., encryptedExtraData: _Optional[str] = ..., clientModifiedTime: _Optional[int] = ..., nonSharedData: _Optional[str] = ..., linkedRecordData: _Optional[_Iterable[_Union[RecordData, _Mapping]]] = ..., fileId: _Optional[_Iterable[bytes]] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ..., recordKeyType: _Optional[_Union[RecordKeyType, str]] = ..., recordKey: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ...) -> None: ... + def __init__(self, revision: _Optional[int] = ..., version: _Optional[int] = ..., shared: bool = ..., encryptedRecordData: _Optional[str] = ..., encryptedExtraData: _Optional[str] = ..., clientModifiedTime: _Optional[int] = ..., nonSharedData: _Optional[str] = ..., linkedRecordData: _Optional[_Iterable[_Union[RecordData, _Mapping]]] = ..., fileId: _Optional[_Iterable[bytes]] = ..., fileSize: _Optional[int] = ..., thumbnailSize: _Optional[int] = ..., recordKeyType: _Optional[_Union[RecordKeyType, str]] = ..., recordKey: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ...) -> None: ... class RecordDataWithAccessInfo(_message.Message): __slots__ = ("recordUid", "recordData", "userPermission", "sharedFolderPermission") @@ -693,7 +700,7 @@ class IsObjectShareAdmin(_message.Message): uid: bytes isAdmin: bool objectType: CheckShareAdminObjectType - def __init__(self, uid: _Optional[bytes] = ..., isAdmin: _Optional[bool] = ..., objectType: _Optional[_Union[CheckShareAdminObjectType, str]] = ...) -> None: ... + def __init__(self, uid: _Optional[bytes] = ..., isAdmin: bool = ..., objectType: _Optional[_Union[CheckShareAdminObjectType, str]] = ...) -> None: ... class AmIShareAdmin(_message.Message): __slots__ = ("isObjectShareAdmin",) @@ -741,7 +748,7 @@ class SharedRecord(_message.Message): expiration: int timerNotificationType: TimerNotificationType rotateOnExpiration: bool - def __init__(self, toUsername: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., editable: _Optional[bool] = ..., shareable: _Optional[bool] = ..., transfer: _Optional[bool] = ..., useEccKey: _Optional[bool] = ..., removeVaultData: _Optional[bool] = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: _Optional[bool] = ...) -> None: ... + def __init__(self, toUsername: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., sharedFolderUid: _Optional[bytes] = ..., teamUid: _Optional[bytes] = ..., editable: bool = ..., shareable: bool = ..., transfer: bool = ..., useEccKey: bool = ..., removeVaultData: bool = ..., expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... class RecordShareUpdateResponse(_message.Message): __slots__ = ("addSharedRecordStatus", "updateSharedRecordStatus", "removeSharedRecordStatus") @@ -771,7 +778,7 @@ class GetRecordPermissionsRequest(_message.Message): ISSHAREADMIN_FIELD_NUMBER: _ClassVar[int] recordUids: _containers.RepeatedScalarFieldContainer[bytes] isShareAdmin: bool - def __init__(self, recordUids: _Optional[_Iterable[bytes]] = ..., isShareAdmin: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUids: _Optional[_Iterable[bytes]] = ..., isShareAdmin: bool = ...) -> None: ... class GetRecordPermissionsResponse(_message.Message): __slots__ = ("recordPermissions",) @@ -791,7 +798,7 @@ class RecordPermission(_message.Message): canEdit: bool canShare: bool canTransfer: bool - def __init__(self, recordUid: _Optional[bytes] = ..., owner: _Optional[bool] = ..., canEdit: _Optional[bool] = ..., canShare: _Optional[bool] = ..., canTransfer: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., owner: bool = ..., canEdit: bool = ..., canShare: bool = ..., canTransfer: bool = ...) -> None: ... class GetShareObjectsRequest(_message.Message): __slots__ = ("startWith", "contains", "filtered", "sharedFolderUid") @@ -803,7 +810,7 @@ class GetShareObjectsRequest(_message.Message): contains: str filtered: bool sharedFolderUid: bytes - def __init__(self, startWith: _Optional[str] = ..., contains: _Optional[str] = ..., filtered: _Optional[bool] = ..., sharedFolderUid: _Optional[bytes] = ...) -> None: ... + def __init__(self, startWith: _Optional[str] = ..., contains: _Optional[str] = ..., filtered: bool = ..., sharedFolderUid: _Optional[bytes] = ...) -> None: ... class GetShareObjectsResponse(_message.Message): __slots__ = ("shareRelationships", "shareFamilyUsers", "shareEnterpriseUsers", "shareTeams", "shareMCTeams", "shareMCEnterpriseUsers", "shareEnterpriseNames") @@ -824,20 +831,22 @@ class GetShareObjectsResponse(_message.Message): def __init__(self, shareRelationships: _Optional[_Iterable[_Union[ShareUser, _Mapping]]] = ..., shareFamilyUsers: _Optional[_Iterable[_Union[ShareUser, _Mapping]]] = ..., shareEnterpriseUsers: _Optional[_Iterable[_Union[ShareUser, _Mapping]]] = ..., shareTeams: _Optional[_Iterable[_Union[ShareTeam, _Mapping]]] = ..., shareMCTeams: _Optional[_Iterable[_Union[ShareTeam, _Mapping]]] = ..., shareMCEnterpriseUsers: _Optional[_Iterable[_Union[ShareUser, _Mapping]]] = ..., shareEnterpriseNames: _Optional[_Iterable[_Union[ShareEnterprise, _Mapping]]] = ...) -> None: ... class ShareUser(_message.Message): - __slots__ = ("username", "fullname", "enterpriseId", "status", "isShareAdmin", "isAdminOfSharedFolderOwner") + __slots__ = ("username", "fullname", "enterpriseId", "status", "isShareAdmin", "isAdminOfSharedFolderOwner", "userAccountUid") USERNAME_FIELD_NUMBER: _ClassVar[int] FULLNAME_FIELD_NUMBER: _ClassVar[int] ENTERPRISEID_FIELD_NUMBER: _ClassVar[int] STATUS_FIELD_NUMBER: _ClassVar[int] ISSHAREADMIN_FIELD_NUMBER: _ClassVar[int] ISADMINOFSHAREDFOLDEROWNER_FIELD_NUMBER: _ClassVar[int] + USERACCOUNTUID_FIELD_NUMBER: _ClassVar[int] username: str fullname: str enterpriseId: int status: ShareStatus isShareAdmin: bool isAdminOfSharedFolderOwner: bool - def __init__(self, username: _Optional[str] = ..., fullname: _Optional[str] = ..., enterpriseId: _Optional[int] = ..., status: _Optional[_Union[ShareStatus, str]] = ..., isShareAdmin: _Optional[bool] = ..., isAdminOfSharedFolderOwner: _Optional[bool] = ...) -> None: ... + userAccountUid: bytes + def __init__(self, username: _Optional[str] = ..., fullname: _Optional[str] = ..., enterpriseId: _Optional[int] = ..., status: _Optional[_Union[ShareStatus, str]] = ..., isShareAdmin: bool = ..., isAdminOfSharedFolderOwner: bool = ..., userAccountUid: _Optional[bytes] = ...) -> None: ... class ShareTeam(_message.Message): __slots__ = ("teamname", "enterpriseId", "teamUid") @@ -873,7 +882,7 @@ class TransferRecord(_message.Message): recordUid: bytes recordKey: bytes useEccKey: bool - def __init__(self, username: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., useEccKey: _Optional[bool] = ...) -> None: ... + def __init__(self, username: _Optional[str] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., useEccKey: bool = ...) -> None: ... class RecordsOnwershipTransferResponse(_message.Message): __slots__ = ("transferRecordStatus",) diff --git a/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.py b/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.py new file mode 100644 index 00000000..93d5db35 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: record_sharing.proto +# 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, + 29, + 3, + '', + 'record_sharing.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import folder_pb2 as folder__pb2 +from . import tla_pb2 as tla__pb2 +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14record_sharing.proto\x12\x11record.v3.sharing\x1a\x0c\x66older.proto\x1a\ttla.proto\x1a\x1cgoogle/api/annotations.proto\"\xdd\x01\n\x07Request\x12@\n\x18\x63reateSharingPermissions\x18\x01 \x03(\x0b\x32\x1e.record.v3.sharing.Permissions\x12@\n\x18updateSharingPermissions\x18\x02 \x03(\x0b\x32\x1e.record.v3.sharing.Permissions\x12@\n\x18revokeSharingPermissions\x18\x03 \x03(\x0b\x32\x1e.record.v3.sharing.Permissions\x12\x0c\n\x04\x65\x63ho\x18\x04 \x01(\t\"\x85\x01\n\x0bPermissions\x12\x14\n\x0crecipientUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x11\n\trecordKey\x18\x04 \x01(\x0c\x12\x11\n\tuseEccKey\x18\x05 \x01(\x08\x12\'\n\x05rules\x18\x06 \x01(\x0b\x32\x18.Folder.RecordAccessData\"\xb5\x01\n\x08Response\x12\x37\n\x14\x63reatedSharingStatus\x18\x01 \x03(\x0b\x32\x19.record.v3.sharing.Status\x12\x37\n\x14updatedSharingStatus\x18\x02 \x03(\x0b\x32\x19.record.v3.sharing.Status\x12\x37\n\x14revokedSharingStatus\x18\x03 \x03(\x0b\x32\x19.record.v3.sharing.Status\"t\n\x06Status\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x30\n\x06status\x18\x02 \x01(\x0e\x32 .record.v3.sharing.SharingStatus\x12\x0f\n\x07message\x18\x03 \x01(\t\x12\x14\n\x0crecipientUid\x18\x04 \x01(\x0c\"4\n\rRevokedAccess\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08\x61\x63torUid\x18\x02 \x01(\x0c\"o\n\x12RecordSharingState\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10isDirectlyShared\x18\x02 \x01(\x08\x12\x1a\n\x12isIndirectlyShared\x18\x03 \x01(\x08\x12\x10\n\x08isShared\x18\x04 \x01(\x08*\xa9\x01\n\rSharingStatus\x12\x0b\n\x07SUCCESS\x10\x00\x12\x12\n\x0ePENDING_ACCEPT\x10\x01\x12\x12\n\x0eUSER_NOT_FOUND\x10\x02\x12\x12\n\x0e\x41LREADY_SHARED\x10\x03\x12\x18\n\x14NOT_ALLOWED_TO_SHARE\x10\x04\x12\x11\n\rACCESS_DENIED\x10\x05\x12\"\n\x1eNOT_ALLOWED_TO_SET_PERMISSIONS\x10\x06\x32\x8b\x01\n\x14RecordSharingService\x12s\n\x0bShareRecord\x12\x1a.record.v3.sharing.Request\x1a\x1b.record.v3.sharing.Response\"+\x82\xd3\xe4\x93\x02%\" /api/rest/vault/records/v3/share:\x01*B2\n.com.keepersecurity.proto.api.record.v3.sharingP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'record_sharing_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n.com.keepersecurity.proto.api.record.v3.sharingP\001' + _globals['_RECORDSHARINGSERVICE'].methods_by_name['ShareRecord']._loaded_options = None + _globals['_RECORDSHARINGSERVICE'].methods_by_name['ShareRecord']._serialized_options = b'\202\323\344\223\002%\" /api/rest/vault/records/v3/share:\001*' + _globals['_SHARINGSTATUS']._serialized_start=928 + _globals['_SHARINGSTATUS']._serialized_end=1097 + _globals['_REQUEST']._serialized_start=99 + _globals['_REQUEST']._serialized_end=320 + _globals['_PERMISSIONS']._serialized_start=323 + _globals['_PERMISSIONS']._serialized_end=456 + _globals['_RESPONSE']._serialized_start=459 + _globals['_RESPONSE']._serialized_end=640 + _globals['_STATUS']._serialized_start=642 + _globals['_STATUS']._serialized_end=758 + _globals['_REVOKEDACCESS']._serialized_start=760 + _globals['_REVOKEDACCESS']._serialized_end=812 + _globals['_RECORDSHARINGSTATE']._serialized_start=814 + _globals['_RECORDSHARINGSTATE']._serialized_end=925 + _globals['_RECORDSHARINGSERVICE']._serialized_start=1100 + _globals['_RECORDSHARINGSERVICE']._serialized_end=1239 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.pyi b/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.pyi new file mode 100644 index 00000000..c5f25e8d --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/record_sharing_pb2.pyi @@ -0,0 +1,95 @@ +import folder_pb2 as _folder_pb2 +import tla_pb2 as _tla_pb2 +from google.api import annotations_pb2 as _annotations_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 +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 + +DESCRIPTOR: _descriptor.FileDescriptor + +class SharingStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + SUCCESS: _ClassVar[SharingStatus] + PENDING_ACCEPT: _ClassVar[SharingStatus] + USER_NOT_FOUND: _ClassVar[SharingStatus] + ALREADY_SHARED: _ClassVar[SharingStatus] + NOT_ALLOWED_TO_SHARE: _ClassVar[SharingStatus] + ACCESS_DENIED: _ClassVar[SharingStatus] + NOT_ALLOWED_TO_SET_PERMISSIONS: _ClassVar[SharingStatus] +SUCCESS: SharingStatus +PENDING_ACCEPT: SharingStatus +USER_NOT_FOUND: SharingStatus +ALREADY_SHARED: SharingStatus +NOT_ALLOWED_TO_SHARE: SharingStatus +ACCESS_DENIED: SharingStatus +NOT_ALLOWED_TO_SET_PERMISSIONS: SharingStatus + +class Request(_message.Message): + __slots__ = ("createSharingPermissions", "updateSharingPermissions", "revokeSharingPermissions", "echo") + CREATESHARINGPERMISSIONS_FIELD_NUMBER: _ClassVar[int] + UPDATESHARINGPERMISSIONS_FIELD_NUMBER: _ClassVar[int] + REVOKESHARINGPERMISSIONS_FIELD_NUMBER: _ClassVar[int] + ECHO_FIELD_NUMBER: _ClassVar[int] + createSharingPermissions: _containers.RepeatedCompositeFieldContainer[Permissions] + updateSharingPermissions: _containers.RepeatedCompositeFieldContainer[Permissions] + revokeSharingPermissions: _containers.RepeatedCompositeFieldContainer[Permissions] + echo: str + def __init__(self, createSharingPermissions: _Optional[_Iterable[_Union[Permissions, _Mapping]]] = ..., updateSharingPermissions: _Optional[_Iterable[_Union[Permissions, _Mapping]]] = ..., revokeSharingPermissions: _Optional[_Iterable[_Union[Permissions, _Mapping]]] = ..., echo: _Optional[str] = ...) -> None: ... + +class Permissions(_message.Message): + __slots__ = ("recipientUid", "recordUid", "recordKey", "useEccKey", "rules") + RECIPIENTUID_FIELD_NUMBER: _ClassVar[int] + RECORDUID_FIELD_NUMBER: _ClassVar[int] + RECORDKEY_FIELD_NUMBER: _ClassVar[int] + USEECCKEY_FIELD_NUMBER: _ClassVar[int] + RULES_FIELD_NUMBER: _ClassVar[int] + recipientUid: bytes + recordUid: bytes + recordKey: bytes + useEccKey: bool + rules: _folder_pb2.RecordAccessData + def __init__(self, recipientUid: _Optional[bytes] = ..., recordUid: _Optional[bytes] = ..., recordKey: _Optional[bytes] = ..., useEccKey: bool = ..., rules: _Optional[_Union[_folder_pb2.RecordAccessData, _Mapping]] = ...) -> None: ... + +class Response(_message.Message): + __slots__ = ("createdSharingStatus", "updatedSharingStatus", "revokedSharingStatus") + CREATEDSHARINGSTATUS_FIELD_NUMBER: _ClassVar[int] + UPDATEDSHARINGSTATUS_FIELD_NUMBER: _ClassVar[int] + REVOKEDSHARINGSTATUS_FIELD_NUMBER: _ClassVar[int] + createdSharingStatus: _containers.RepeatedCompositeFieldContainer[Status] + updatedSharingStatus: _containers.RepeatedCompositeFieldContainer[Status] + revokedSharingStatus: _containers.RepeatedCompositeFieldContainer[Status] + def __init__(self, createdSharingStatus: _Optional[_Iterable[_Union[Status, _Mapping]]] = ..., updatedSharingStatus: _Optional[_Iterable[_Union[Status, _Mapping]]] = ..., revokedSharingStatus: _Optional[_Iterable[_Union[Status, _Mapping]]] = ...) -> None: ... + +class Status(_message.Message): + __slots__ = ("recordUid", "status", "message", "recipientUid") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + RECIPIENTUID_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + status: SharingStatus + message: str + recipientUid: bytes + def __init__(self, recordUid: _Optional[bytes] = ..., status: _Optional[_Union[SharingStatus, str]] = ..., message: _Optional[str] = ..., recipientUid: _Optional[bytes] = ...) -> None: ... + +class RevokedAccess(_message.Message): + __slots__ = ("recordUid", "actorUid") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + ACTORUID_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + actorUid: bytes + def __init__(self, recordUid: _Optional[bytes] = ..., actorUid: _Optional[bytes] = ...) -> None: ... + +class RecordSharingState(_message.Message): + __slots__ = ("recordUid", "isDirectlyShared", "isIndirectlyShared", "isShared") + RECORDUID_FIELD_NUMBER: _ClassVar[int] + ISDIRECTLYSHARED_FIELD_NUMBER: _ClassVar[int] + ISINDIRECTLYSHARED_FIELD_NUMBER: _ClassVar[int] + ISSHARED_FIELD_NUMBER: _ClassVar[int] + recordUid: bytes + isDirectlyShared: bool + isIndirectlyShared: bool + isShared: bool + def __init__(self, recordUid: _Optional[bytes] = ..., isDirectlyShared: bool = ..., isIndirectlyShared: bool = ..., isShared: bool = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/remove_pb2.py b/keepersdk-package/src/keepersdk/proto/remove_pb2.py new file mode 100644 index 00000000..f5a48130 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/remove_pb2.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: remove.proto +# Protobuf Python Version: 6.33.4 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.api import annotations_pb2 as google_dot_api_dot_annotations__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cremove.proto\x12\x10\x66older.v3.remove\x1a\x1cgoogle/api/annotations.proto\"v\n\rRecordRemoval\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_uid\x18\x02 \x01(\x0c\x12=\n\x0eoperation_type\x18\x03 \x01(\x0e\x32%.folder.v3.remove.RecordOperationType\"b\n\rFolderRemoval\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12=\n\x0eoperation_type\x18\x02 \x01(\x0e\x32%.folder.v3.remove.FolderOperationType\"\x93\x01\n\x13RemoveRecordRequest\x12.\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1e.folder.v3.remove.RemoveAction\x12\x30\n\x07records\x18\x02 \x03(\x0b\x32\x1f.folder.v3.remove.RecordRemoval\x12\x1a\n\x12\x63onfirmation_token\x18\x03 \x01(\x0c\"\x93\x01\n\x13RemoveFolderRequest\x12.\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1e.folder.v3.remove.RemoveAction\x12\x30\n\x07\x66olders\x18\x02 \x03(\x0b\x32\x1f.folder.v3.remove.FolderRemoval\x12\x1a\n\x12\x63onfirmation_token\x18\x03 \x01(\x0c\"\x8e\x01\n\x0eRemoveResponse\x12\x1a\n\x12\x63onfirmation_token\x18\x01 \x01(\x0c\x12\x18\n\x10token_expires_at\x18\x02 \x01(\x03\x12/\n\x07results\x18\x03 \x03(\x0b\x32\x1e.folder.v3.remove.RemoveResult\x12\x15\n\rerror_message\x18\x04 \x01(\t\"\xba\x01\n\x0cRemoveResult\x12\x10\n\x08item_uid\x18\x01 \x01(\x0c\x12\x12\n\nfolder_uid\x18\x02 \x01(\x0c\x12.\n\x06status\x18\x03 \x01(\x0e\x32\x1e.folder.v3.remove.RemoveStatus\x12(\n\x06impact\x18\x04 \x01(\x0b\x32\x18.folder.v3.remove.Impact\x12*\n\x05\x65rror\x18\x05 \x01(\x0b\x32\x1b.folder.v3.remove.ItemError\"\xb7\x01\n\x06Impact\x12\x15\n\rfolders_count\x18\x01 \x01(\x05\x12\x15\n\rrecords_count\x18\x02 \x01(\x05\x12\x1c\n\x14\x61\x66\x66\x65\x63ted_users_count\x18\x03 \x01(\x05\x12\x1c\n\x14\x61\x66\x66\x65\x63ted_teams_count\x18\x04 \x01(\x05\x12\x31\n\x0brecord_info\x18\x05 \x03(\x0b\x32\x1c.folder.v3.remove.RecordInfo\x12\x10\n\x08warnings\x18\x06 \x03(\t\"9\n\nRecordInfo\x12\x12\n\nrecord_uid\x18\x01 \x01(\x0c\x12\x17\n\x0flocations_count\x18\x02 \x01(\x05\"M\n\tItemError\x12/\n\x04\x63ode\x18\x01 \x01(\x0e\x32!.folder.v3.remove.RemoveErrorCode\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xa7\x01\n\x13RemovalTokenPayload\x12<\n\x11item_fingerprints\x18\x01 \x03(\x0b\x32!.folder.v3.remove.ItemFingerprint\x12\x0f\n\x07user_id\x18\x02 \x01(\x05\x12\x11\n\tdevice_id\x18\x03 \x01(\x03\x12\x13\n\x0bsession_uid\x18\x04 \x01(\x0c\x12\x19\n\x11\x65xpires_at_millis\x18\x05 \x01(\x03\"\x94\x01\n\x0fItemFingerprint\x12\x30\n\x06record\x18\x01 \x01(\x0b\x32\x1e.folder.v3.remove.RecordTargetH\x00\x12\x30\n\x06\x66older\x18\x02 \x01(\x0b\x32\x1e.folder.v3.remove.FolderTargetH\x00\x12\x13\n\x0b\x66ingerprint\x18\n \x01(\x0c\x42\x08\n\x06target\"u\n\x0cRecordTarget\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12\x12\n\nrecord_uid\x18\x02 \x01(\x0c\x12=\n\x0eoperation_type\x18\x03 \x01(\x0e\x32%.folder.v3.remove.RecordOperationType\"a\n\x0c\x46olderTarget\x12\x12\n\nfolder_uid\x18\x01 \x01(\x0c\x12=\n\x0eoperation_type\x18\x02 \x01(\x0e\x32%.folder.v3.remove.FolderOperationType*D\n\x0cRemoveAction\x12\x19\n\x15REMOVE_ACTION_PREVIEW\x10\x00\x12\x19\n\x15REMOVE_ACTION_CONFIRM\x10\x01*~\n\x13RecordOperationType\x12\x1c\n\x18RECORD_OPERATION_UNKNOWN\x10\x00\x12\x16\n\x12UNLINK_FROM_FOLDER\x10\x01\x12\x18\n\x14MOVE_TO_FOLDER_TRASH\x10\x02\x12\x17\n\x13MOVE_TO_OWNER_TRASH\x10\x03*\x91\x01\n\x13\x46olderOperationType\x12\x1c\n\x18\x46OLDER_OPERATION_UNKNOWN\x10\x00\x12\x1f\n\x1b\x46OLDER_MOVE_TO_FOLDER_TRASH\x10\x01\x12\x1e\n\x1a\x46OLDER_MOVE_TO_OWNER_TRASH\x10\x02\x12\x1b\n\x17\x46OLDER_DELETE_PERMANENT\x10\x03*\xcb\x01\n\x0fRemoveErrorCode\x12\x18\n\x14REMOVE_ERROR_UNKNOWN\x10\x00\x12\x1a\n\x16REMOVE_ERROR_NOT_FOUND\x10\x01\x12\x1e\n\x1aREMOVE_ERROR_ACCESS_DENIED\x10\x02\x12 \n\x1cREMOVE_ERROR_TRASHCAN_FOLDER\x10\x03\x12\x1c\n\x18REMOVE_ERROR_ROOT_FOLDER\x10\x04\x12\"\n\x1eREMOVE_ERROR_DESCENDANT_DENIED\x10\x05*\xec\x01\n\x0cRemoveStatus\x12\x19\n\x15REMOVE_STATUS_UNKNOWN\x10\x00\x12\x19\n\x15REMOVE_STATUS_SUCCESS\x10\x01\x12\x1f\n\x1bREMOVE_STATUS_STALE_PREVIEW\x10\x02\x12\x1f\n\x1bREMOVE_STATUS_TOKEN_EXPIRED\x10\x03\x12\x1f\n\x1bREMOVE_STATUS_TOKEN_INVALID\x10\x04\x12\x1f\n\x1bREMOVE_STATUS_ACCESS_DENIED\x10\x05\x12\"\n\x1eREMOVE_STATUS_VALIDATION_ERROR\x10\x06\x32\xad\x02\n\rRemoveService\x12\x8c\x01\n\x0cRemoveRecord\x12%.folder.v3.remove.RemoveRecordRequest\x1a .folder.v3.remove.RemoveResponse\"3\x82\xd3\xe4\x93\x02-\"(/api/rest/vault/folders/v3/remove_record:\x01*\x12\x8c\x01\n\x0cRemoveFolder\x12%.folder.v3.remove.RemoveFolderRequest\x1a .folder.v3.remove.RemoveResponse\"3\x82\xd3\xe4\x93\x02-\"(/api/rest/vault/folders/v3/remove_folder:\x01*B1\n-com.keepersecurity.proto.api.folder.v3.removeP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'remove_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n-com.keepersecurity.proto.api.folder.v3.removeP\001' + _globals['_REMOVESERVICE'].methods_by_name['RemoveRecord']._loaded_options = None + _globals['_REMOVESERVICE'].methods_by_name['RemoveRecord']._serialized_options = b'\202\323\344\223\002-\"(/api/rest/vault/folders/v3/remove_record:\001*' + _globals['_REMOVESERVICE'].methods_by_name['RemoveFolder']._loaded_options = None + _globals['_REMOVESERVICE'].methods_by_name['RemoveFolder']._serialized_options = b'\202\323\344\223\002-\"(/api/rest/vault/folders/v3/remove_folder:\001*' + _globals['_REMOVEACTION']._serialized_start=1781 + _globals['_REMOVEACTION']._serialized_end=1849 + _globals['_RECORDOPERATIONTYPE']._serialized_start=1851 + _globals['_RECORDOPERATIONTYPE']._serialized_end=1977 + _globals['_FOLDEROPERATIONTYPE']._serialized_start=1980 + _globals['_FOLDEROPERATIONTYPE']._serialized_end=2125 + _globals['_REMOVEERRORCODE']._serialized_start=2128 + _globals['_REMOVEERRORCODE']._serialized_end=2331 + _globals['_REMOVESTATUS']._serialized_start=2334 + _globals['_REMOVESTATUS']._serialized_end=2570 + _globals['_RECORDREMOVAL']._serialized_start=64 + _globals['_RECORDREMOVAL']._serialized_end=182 + _globals['_FOLDERREMOVAL']._serialized_start=184 + _globals['_FOLDERREMOVAL']._serialized_end=282 + _globals['_REMOVERECORDREQUEST']._serialized_start=285 + _globals['_REMOVERECORDREQUEST']._serialized_end=432 + _globals['_REMOVEFOLDERREQUEST']._serialized_start=435 + _globals['_REMOVEFOLDERREQUEST']._serialized_end=582 + _globals['_REMOVERESPONSE']._serialized_start=585 + _globals['_REMOVERESPONSE']._serialized_end=727 + _globals['_REMOVERESULT']._serialized_start=730 + _globals['_REMOVERESULT']._serialized_end=916 + _globals['_IMPACT']._serialized_start=919 + _globals['_IMPACT']._serialized_end=1102 + _globals['_RECORDINFO']._serialized_start=1104 + _globals['_RECORDINFO']._serialized_end=1161 + _globals['_ITEMERROR']._serialized_start=1163 + _globals['_ITEMERROR']._serialized_end=1240 + _globals['_REMOVALTOKENPAYLOAD']._serialized_start=1243 + _globals['_REMOVALTOKENPAYLOAD']._serialized_end=1410 + _globals['_ITEMFINGERPRINT']._serialized_start=1413 + _globals['_ITEMFINGERPRINT']._serialized_end=1561 + _globals['_RECORDTARGET']._serialized_start=1563 + _globals['_RECORDTARGET']._serialized_end=1680 + _globals['_FOLDERTARGET']._serialized_start=1682 + _globals['_FOLDERTARGET']._serialized_end=1779 + _globals['_REMOVESERVICE']._serialized_start=2573 + _globals['_REMOVESERVICE']._serialized_end=2874 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/remove_pb2.pyi b/keepersdk-package/src/keepersdk/proto/remove_pb2.pyi new file mode 100644 index 00000000..c88bbfd0 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/remove_pb2.pyi @@ -0,0 +1,208 @@ +from google.api import annotations_pb2 as _annotations_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 +from google.protobuf import message as _message +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 + +class RemoveAction(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REMOVE_ACTION_PREVIEW: _ClassVar[RemoveAction] + REMOVE_ACTION_CONFIRM: _ClassVar[RemoveAction] + +class RecordOperationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + RECORD_OPERATION_UNKNOWN: _ClassVar[RecordOperationType] + UNLINK_FROM_FOLDER: _ClassVar[RecordOperationType] + MOVE_TO_FOLDER_TRASH: _ClassVar[RecordOperationType] + MOVE_TO_OWNER_TRASH: _ClassVar[RecordOperationType] + +class FolderOperationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + FOLDER_OPERATION_UNKNOWN: _ClassVar[FolderOperationType] + FOLDER_MOVE_TO_FOLDER_TRASH: _ClassVar[FolderOperationType] + FOLDER_MOVE_TO_OWNER_TRASH: _ClassVar[FolderOperationType] + FOLDER_DELETE_PERMANENT: _ClassVar[FolderOperationType] + +class RemoveErrorCode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REMOVE_ERROR_UNKNOWN: _ClassVar[RemoveErrorCode] + REMOVE_ERROR_NOT_FOUND: _ClassVar[RemoveErrorCode] + REMOVE_ERROR_ACCESS_DENIED: _ClassVar[RemoveErrorCode] + REMOVE_ERROR_TRASHCAN_FOLDER: _ClassVar[RemoveErrorCode] + REMOVE_ERROR_ROOT_FOLDER: _ClassVar[RemoveErrorCode] + REMOVE_ERROR_DESCENDANT_DENIED: _ClassVar[RemoveErrorCode] + +class RemoveStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REMOVE_STATUS_UNKNOWN: _ClassVar[RemoveStatus] + REMOVE_STATUS_SUCCESS: _ClassVar[RemoveStatus] + REMOVE_STATUS_STALE_PREVIEW: _ClassVar[RemoveStatus] + REMOVE_STATUS_TOKEN_EXPIRED: _ClassVar[RemoveStatus] + REMOVE_STATUS_TOKEN_INVALID: _ClassVar[RemoveStatus] + REMOVE_STATUS_ACCESS_DENIED: _ClassVar[RemoveStatus] + REMOVE_STATUS_VALIDATION_ERROR: _ClassVar[RemoveStatus] +REMOVE_ACTION_PREVIEW: RemoveAction +REMOVE_ACTION_CONFIRM: RemoveAction +RECORD_OPERATION_UNKNOWN: RecordOperationType +UNLINK_FROM_FOLDER: RecordOperationType +MOVE_TO_FOLDER_TRASH: RecordOperationType +MOVE_TO_OWNER_TRASH: RecordOperationType +FOLDER_OPERATION_UNKNOWN: FolderOperationType +FOLDER_MOVE_TO_FOLDER_TRASH: FolderOperationType +FOLDER_MOVE_TO_OWNER_TRASH: FolderOperationType +FOLDER_DELETE_PERMANENT: FolderOperationType +REMOVE_ERROR_UNKNOWN: RemoveErrorCode +REMOVE_ERROR_NOT_FOUND: RemoveErrorCode +REMOVE_ERROR_ACCESS_DENIED: RemoveErrorCode +REMOVE_ERROR_TRASHCAN_FOLDER: RemoveErrorCode +REMOVE_ERROR_ROOT_FOLDER: RemoveErrorCode +REMOVE_ERROR_DESCENDANT_DENIED: RemoveErrorCode +REMOVE_STATUS_UNKNOWN: RemoveStatus +REMOVE_STATUS_SUCCESS: RemoveStatus +REMOVE_STATUS_STALE_PREVIEW: RemoveStatus +REMOVE_STATUS_TOKEN_EXPIRED: RemoveStatus +REMOVE_STATUS_TOKEN_INVALID: RemoveStatus +REMOVE_STATUS_ACCESS_DENIED: RemoveStatus +REMOVE_STATUS_VALIDATION_ERROR: RemoveStatus + +class RecordRemoval(_message.Message): + __slots__ = ("folder_uid", "record_uid", "operation_type") + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + RECORD_UID_FIELD_NUMBER: _ClassVar[int] + OPERATION_TYPE_FIELD_NUMBER: _ClassVar[int] + folder_uid: bytes + record_uid: bytes + operation_type: RecordOperationType + def __init__(self, folder_uid: _Optional[bytes] = ..., record_uid: _Optional[bytes] = ..., operation_type: _Optional[_Union[RecordOperationType, str]] = ...) -> None: ... + +class FolderRemoval(_message.Message): + __slots__ = ("folder_uid", "operation_type") + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + OPERATION_TYPE_FIELD_NUMBER: _ClassVar[int] + folder_uid: bytes + operation_type: FolderOperationType + def __init__(self, folder_uid: _Optional[bytes] = ..., operation_type: _Optional[_Union[FolderOperationType, str]] = ...) -> None: ... + +class RemoveRecordRequest(_message.Message): + __slots__ = ("action", "records", "confirmation_token") + ACTION_FIELD_NUMBER: _ClassVar[int] + RECORDS_FIELD_NUMBER: _ClassVar[int] + CONFIRMATION_TOKEN_FIELD_NUMBER: _ClassVar[int] + action: RemoveAction + records: _containers.RepeatedCompositeFieldContainer[RecordRemoval] + confirmation_token: bytes + def __init__(self, action: _Optional[_Union[RemoveAction, str]] = ..., records: _Optional[_Iterable[_Union[RecordRemoval, _Mapping]]] = ..., confirmation_token: _Optional[bytes] = ...) -> None: ... + +class RemoveFolderRequest(_message.Message): + __slots__ = ("action", "folders", "confirmation_token") + ACTION_FIELD_NUMBER: _ClassVar[int] + FOLDERS_FIELD_NUMBER: _ClassVar[int] + CONFIRMATION_TOKEN_FIELD_NUMBER: _ClassVar[int] + action: RemoveAction + folders: _containers.RepeatedCompositeFieldContainer[FolderRemoval] + confirmation_token: bytes + def __init__(self, action: _Optional[_Union[RemoveAction, str]] = ..., folders: _Optional[_Iterable[_Union[FolderRemoval, _Mapping]]] = ..., confirmation_token: _Optional[bytes] = ...) -> None: ... + +class RemoveResponse(_message.Message): + __slots__ = ("confirmation_token", "token_expires_at", "results", "error_message") + CONFIRMATION_TOKEN_FIELD_NUMBER: _ClassVar[int] + TOKEN_EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + RESULTS_FIELD_NUMBER: _ClassVar[int] + ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int] + confirmation_token: bytes + token_expires_at: int + results: _containers.RepeatedCompositeFieldContainer[RemoveResult] + error_message: str + def __init__(self, confirmation_token: _Optional[bytes] = ..., token_expires_at: _Optional[int] = ..., results: _Optional[_Iterable[_Union[RemoveResult, _Mapping]]] = ..., error_message: _Optional[str] = ...) -> None: ... + +class RemoveResult(_message.Message): + __slots__ = ("item_uid", "folder_uid", "status", "impact", "error") + ITEM_UID_FIELD_NUMBER: _ClassVar[int] + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + IMPACT_FIELD_NUMBER: _ClassVar[int] + ERROR_FIELD_NUMBER: _ClassVar[int] + item_uid: bytes + folder_uid: bytes + status: RemoveStatus + impact: Impact + error: ItemError + def __init__(self, item_uid: _Optional[bytes] = ..., folder_uid: _Optional[bytes] = ..., status: _Optional[_Union[RemoveStatus, str]] = ..., impact: _Optional[_Union[Impact, _Mapping]] = ..., error: _Optional[_Union[ItemError, _Mapping]] = ...) -> None: ... + +class Impact(_message.Message): + __slots__ = ("folders_count", "records_count", "affected_users_count", "affected_teams_count", "record_info", "warnings") + FOLDERS_COUNT_FIELD_NUMBER: _ClassVar[int] + RECORDS_COUNT_FIELD_NUMBER: _ClassVar[int] + AFFECTED_USERS_COUNT_FIELD_NUMBER: _ClassVar[int] + AFFECTED_TEAMS_COUNT_FIELD_NUMBER: _ClassVar[int] + RECORD_INFO_FIELD_NUMBER: _ClassVar[int] + WARNINGS_FIELD_NUMBER: _ClassVar[int] + folders_count: int + records_count: int + affected_users_count: int + affected_teams_count: int + record_info: _containers.RepeatedCompositeFieldContainer[RecordInfo] + warnings: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, folders_count: _Optional[int] = ..., records_count: _Optional[int] = ..., affected_users_count: _Optional[int] = ..., affected_teams_count: _Optional[int] = ..., record_info: _Optional[_Iterable[_Union[RecordInfo, _Mapping]]] = ..., warnings: _Optional[_Iterable[str]] = ...) -> None: ... + +class RecordInfo(_message.Message): + __slots__ = ("record_uid", "locations_count") + RECORD_UID_FIELD_NUMBER: _ClassVar[int] + LOCATIONS_COUNT_FIELD_NUMBER: _ClassVar[int] + record_uid: bytes + locations_count: int + def __init__(self, record_uid: _Optional[bytes] = ..., locations_count: _Optional[int] = ...) -> None: ... + +class ItemError(_message.Message): + __slots__ = ("code", "message") + CODE_FIELD_NUMBER: _ClassVar[int] + MESSAGE_FIELD_NUMBER: _ClassVar[int] + code: RemoveErrorCode + message: str + def __init__(self, code: _Optional[_Union[RemoveErrorCode, str]] = ..., message: _Optional[str] = ...) -> None: ... + +class RemovalTokenPayload(_message.Message): + __slots__ = ("item_fingerprints", "user_id", "device_id", "session_uid", "expires_at_millis") + ITEM_FINGERPRINTS_FIELD_NUMBER: _ClassVar[int] + USER_ID_FIELD_NUMBER: _ClassVar[int] + DEVICE_ID_FIELD_NUMBER: _ClassVar[int] + SESSION_UID_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_MILLIS_FIELD_NUMBER: _ClassVar[int] + item_fingerprints: _containers.RepeatedCompositeFieldContainer[ItemFingerprint] + user_id: int + device_id: int + session_uid: bytes + expires_at_millis: int + def __init__(self, item_fingerprints: _Optional[_Iterable[_Union[ItemFingerprint, _Mapping]]] = ..., user_id: _Optional[int] = ..., device_id: _Optional[int] = ..., session_uid: _Optional[bytes] = ..., expires_at_millis: _Optional[int] = ...) -> None: ... + +class ItemFingerprint(_message.Message): + __slots__ = ("record", "folder", "fingerprint") + RECORD_FIELD_NUMBER: _ClassVar[int] + FOLDER_FIELD_NUMBER: _ClassVar[int] + FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + record: RecordTarget + folder: FolderTarget + fingerprint: bytes + def __init__(self, record: _Optional[_Union[RecordTarget, _Mapping]] = ..., folder: _Optional[_Union[FolderTarget, _Mapping]] = ..., fingerprint: _Optional[bytes] = ...) -> None: ... + +class RecordTarget(_message.Message): + __slots__ = ("folder_uid", "record_uid", "operation_type") + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + RECORD_UID_FIELD_NUMBER: _ClassVar[int] + OPERATION_TYPE_FIELD_NUMBER: _ClassVar[int] + folder_uid: bytes + record_uid: bytes + operation_type: RecordOperationType + def __init__(self, folder_uid: _Optional[bytes] = ..., record_uid: _Optional[bytes] = ..., operation_type: _Optional[_Union[RecordOperationType, str]] = ...) -> None: ... + +class FolderTarget(_message.Message): + __slots__ = ("folder_uid", "operation_type") + FOLDER_UID_FIELD_NUMBER: _ClassVar[int] + OPERATION_TYPE_FIELD_NUMBER: _ClassVar[int] + folder_uid: bytes + operation_type: FolderOperationType + def __init__(self, folder_uid: _Optional[bytes] = ..., operation_type: _Optional[_Union[FolderOperationType, str]] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/router_pb2.py b/keepersdk-package/src/keepersdk/proto/router_pb2.py index 9b9959cc..56a2d647 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: router.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'router.proto' ) @@ -23,9 +23,11 @@ from . import pam_pb2 as pam__pb2 +from . import APIRequest_pb2 as APIRequest__pb2 +from . import folder_pb2 as folder__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crouter.proto\x12\x06Router\x1a\tpam.proto\x1a\x10\x41PIRequest.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\"\x9d\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\x12\x18\n\x10maxInstanceCount\x18\r \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\"\xba\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\x12\x1e\n\x11saasConfiguration\x18\x0c \x01(\x0cH\x00\x88\x01\x01\x42\x14\n\x12_saasConfiguration\"<\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\"M\n\x18UserRecordAccessRequests\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.Router.UserRecordAccessRequest\"P\n\x19UserRecordAccessResponses\x12\x33\n\tresponses\x18\x01 \x03(\x0b\x32 .Router.UserRecordAccessResponse\"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\"H\n\x18GetPEDMAdminInfoResponse\x12\x13\n\x0bisPedmAdmin\x18\x01 \x01(\x08\x12\x17\n\x0fpedmAddonActive\x18\x02 \x01(\x08\"-\n\x12PAMNetworkSettings\x12\x17\n\x0f\x61llowedSettings\x18\x01 \x01(\x0c\"\xe4\x01\n\x1ePAMNetworkConfigurationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x38\n\x0fnetworkSettings\x18\x02 \x01(\x0b\x32\x1a.Router.PAMNetworkSettingsH\x00\x88\x01\x01\x12)\n\tresources\x18\x03 \x03(\x0b\x32\x16.PAM.PAMResourceConfig\x12\x36\n\trotations\x18\x04 \x03(\x0b\x32#.Router.RouterRecordRotationRequestB\x12\n\x10_networkSettings\"R\n\x1bPAMDiscoveryRulesSetRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\r\n\x05rules\x18\x02 \x01(\x0c\x12\x10\n\x08rulesKey\x18\x03 \x01(\x0c\"X\n\x18Router2FAValidateRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\t\"~\n\x18Router2FASendPushRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\x33\n\x08pushType\x18\x03 \x01(\x0e\x32!.Authentication.TwoFactorPushType\"U\n$Router2FAGetWebAuthnChallengeRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\"P\n%Router2FAGetWebAuthnChallengeResponse\x12\x11\n\tchallenge\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t*\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\x1a\x10\x41PIRequest.proto\x1a\x0c\x66older.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\"\x9d\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\x12\x18\n\x10maxInstanceCount\x18\r \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\"\xba\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\x12\x1e\n\x11saasConfiguration\x18\x0c \x01(\x0cH\x00\x88\x01\x01\x42\x14\n\x12_saasConfiguration\"<\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\"M\n\x18UserRecordAccessRequests\x12\x31\n\x08requests\x18\x01 \x03(\x0b\x32\x1f.Router.UserRecordAccessRequest\"P\n\x19UserRecordAccessResponses\x12\x33\n\tresponses\x18\x01 \x03(\x0b\x32 .Router.UserRecordAccessResponse\"H\n\x1dUserSharedFolderAccessRequest\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x17\n\x0fsharedFolderUid\x18\x02 \x03(\x0c\"i\n\x1eUserSharedFolderAccessResponse\x12\x17\n\x0fsharedFolderUid\x18\x01 \x01(\x0c\x12.\n\x0e\x61\x63\x63\x65ssRoleType\x18\x02 \x01(\x0e\x32\x16.Folder.AccessRoleType\"\\\n\x1fUserSharedFolderAccessResponses\x12\x39\n\tresponses\x18\x01 \x03(\x0b\x32&.Router.UserSharedFolderAccessResponse\"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\"H\n\x18GetPEDMAdminInfoResponse\x12\x13\n\x0bisPedmAdmin\x18\x01 \x01(\x08\x12\x17\n\x0fpedmAddonActive\x18\x02 \x01(\x08\"-\n\x12PAMNetworkSettings\x12\x17\n\x0f\x61llowedSettings\x18\x01 \x01(\x0c\"\xe4\x01\n\x1ePAMNetworkConfigurationRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x38\n\x0fnetworkSettings\x18\x02 \x01(\x0b\x32\x1a.Router.PAMNetworkSettingsH\x00\x88\x01\x01\x12)\n\tresources\x18\x03 \x03(\x0b\x32\x16.PAM.PAMResourceConfig\x12\x36\n\trotations\x18\x04 \x03(\x0b\x32#.Router.RouterRecordRotationRequestB\x12\n\x10_networkSettings\"R\n\x1bPAMDiscoveryRulesSetRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\r\n\x05rules\x18\x02 \x01(\x0c\x12\x10\n\x08rulesKey\x18\x03 \x01(\x0c\"X\n\x18Router2FAValidateRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\r\n\x05value\x18\x03 \x01(\t\"~\n\x18Router2FASendPushRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\x12\x33\n\x08pushType\x18\x03 \x01(\x0e\x32!.Authentication.TwoFactorPushType\"U\n$Router2FAGetWebAuthnChallengeRequest\x12\x17\n\x0ftransmissionKey\x18\x01 \x01(\x0c\x12\x14\n\x0csessionToken\x18\x02 \x01(\x0c\"P\n%Router2FAGetWebAuthnChallengeResponse\x12\x11\n\tchallenge\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\"[\n\x1c\x43reateEphemeralSecretRequest\x12\x17\n\x0f\x65ncryptedSecret\x18\x01 \x01(\x0c\x12\x15\n\rsecretKeyHash\x18\x02 \x01(\x0c\x12\x0b\n\x03ttl\x18\x03 \x01(\x03*\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,80 +35,88 @@ 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=4038 - _globals['_ROUTERRESPONSECODE']._serialized_end=4318 - _globals['_ROUTERROTATIONSTATUS']._serialized_start=4320 - _globals['_ROUTERROTATIONSTATUS']._serialized_end=4427 - _globals['_USERRECORDACCESSLEVEL']._serialized_start=4429 - _globals['_USERRECORDACCESSLEVEL']._serialized_end=4554 - _globals['_SERVICETYPE']._serialized_start=4556 - _globals['_SERVICETYPE']._serialized_end=4602 - _globals['_ROUTERRESPONSE']._serialized_start=53 - _globals['_ROUTERRESPONSE']._serialized_end=167 - _globals['_ROUTERCONTROLLERMESSAGE']._serialized_start=170 - _globals['_ROUTERCONTROLLERMESSAGE']._serialized_end=345 - _globals['_ROUTERUSERAUTH']._serialized_start=348 - _globals['_ROUTERUSERAUTH']._serialized_end=629 - _globals['_ROUTERDEVICEAUTH']._serialized_start=632 - _globals['_ROUTERDEVICEAUTH']._serialized_end=917 - _globals['_ROUTERRECORDROTATION']._serialized_start=920 - _globals['_ROUTERRECORDROTATION']._serialized_end=1051 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=1053 - _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1122 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1124 - _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1221 - _globals['_ROUTERROTATIONINFO']._serialized_start=1224 - _globals['_ROUTERROTATIONINFO']._serialized_end=1461 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1464 - _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1778 - _globals['_USERRECORDACCESSREQUEST']._serialized_start=1780 - _globals['_USERRECORDACCESSREQUEST']._serialized_end=1840 - _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1842 - _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1939 - _globals['_USERRECORDACCESSREQUESTS']._serialized_start=1941 - _globals['_USERRECORDACCESSREQUESTS']._serialized_end=2018 - _globals['_USERRECORDACCESSRESPONSES']._serialized_start=2020 - _globals['_USERRECORDACCESSRESPONSES']._serialized_end=2100 - _globals['_ROTATIONSCHEDULE']._serialized_start=2102 - _globals['_ROTATIONSCHEDULE']._serialized_end=2158 - _globals['_APICALLBACKREQUEST']._serialized_start=2161 - _globals['_APICALLBACKREQUEST']._serialized_end=2305 - _globals['_APICALLBACKSCHEDULE']._serialized_start=2307 - _globals['_APICALLBACKSCHEDULE']._serialized_end=2360 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2362 - _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2426 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2428 - _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2517 - _globals['_CONNECTIONPARAMETERS']._serialized_start=2520 - _globals['_CONNECTIONPARAMETERS']._serialized_end=2653 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2655 - _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=2734 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=2736 - _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=2810 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=2812 - _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=2905 - _globals['_GETENFORCEMENTREQUEST']._serialized_start=2907 - _globals['_GETENFORCEMENTREQUEST']._serialized_end=2956 - _globals['_ENFORCEMENTTYPE']._serialized_start=2958 - _globals['_ENFORCEMENTTYPE']._serialized_end=3017 - _globals['_GETENFORCEMENTRESPONSE']._serialized_start=3019 - _globals['_GETENFORCEMENTRESPONSE']._serialized_end=3131 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=3133 - _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=3212 - _globals['_GETPEDMADMININFORESPONSE']._serialized_start=3214 - _globals['_GETPEDMADMININFORESPONSE']._serialized_end=3286 - _globals['_PAMNETWORKSETTINGS']._serialized_start=3288 - _globals['_PAMNETWORKSETTINGS']._serialized_end=3333 - _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_start=3336 - _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_end=3564 - _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_start=3566 - _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_end=3648 - _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_start=3650 - _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_end=3738 - _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_start=3740 - _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_end=3866 - _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_start=3868 - _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_end=3953 - _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_start=3955 - _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_end=4035 + _globals['_ROUTERRESPONSECODE']._serialized_start=4420 + _globals['_ROUTERRESPONSECODE']._serialized_end=4700 + _globals['_ROUTERROTATIONSTATUS']._serialized_start=4702 + _globals['_ROUTERROTATIONSTATUS']._serialized_end=4809 + _globals['_USERRECORDACCESSLEVEL']._serialized_start=4811 + _globals['_USERRECORDACCESSLEVEL']._serialized_end=4936 + _globals['_SERVICETYPE']._serialized_start=4938 + _globals['_SERVICETYPE']._serialized_end=4984 + _globals['_ROUTERRESPONSE']._serialized_start=67 + _globals['_ROUTERRESPONSE']._serialized_end=181 + _globals['_ROUTERCONTROLLERMESSAGE']._serialized_start=184 + _globals['_ROUTERCONTROLLERMESSAGE']._serialized_end=359 + _globals['_ROUTERUSERAUTH']._serialized_start=362 + _globals['_ROUTERUSERAUTH']._serialized_end=643 + _globals['_ROUTERDEVICEAUTH']._serialized_start=646 + _globals['_ROUTERDEVICEAUTH']._serialized_end=931 + _globals['_ROUTERRECORDROTATION']._serialized_start=934 + _globals['_ROUTERRECORDROTATION']._serialized_end=1065 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_start=1067 + _globals['_ROUTERRECORDROTATIONSREQUEST']._serialized_end=1136 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_start=1138 + _globals['_ROUTERRECORDROTATIONSRESPONSE']._serialized_end=1235 + _globals['_ROUTERROTATIONINFO']._serialized_start=1238 + _globals['_ROUTERROTATIONINFO']._serialized_end=1475 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_start=1478 + _globals['_ROUTERRECORDROTATIONREQUEST']._serialized_end=1792 + _globals['_USERRECORDACCESSREQUEST']._serialized_start=1794 + _globals['_USERRECORDACCESSREQUEST']._serialized_end=1854 + _globals['_USERRECORDACCESSRESPONSE']._serialized_start=1856 + _globals['_USERRECORDACCESSRESPONSE']._serialized_end=1953 + _globals['_USERRECORDACCESSREQUESTS']._serialized_start=1955 + _globals['_USERRECORDACCESSREQUESTS']._serialized_end=2032 + _globals['_USERRECORDACCESSRESPONSES']._serialized_start=2034 + _globals['_USERRECORDACCESSRESPONSES']._serialized_end=2114 + _globals['_USERSHAREDFOLDERACCESSREQUEST']._serialized_start=2116 + _globals['_USERSHAREDFOLDERACCESSREQUEST']._serialized_end=2188 + _globals['_USERSHAREDFOLDERACCESSRESPONSE']._serialized_start=2190 + _globals['_USERSHAREDFOLDERACCESSRESPONSE']._serialized_end=2295 + _globals['_USERSHAREDFOLDERACCESSRESPONSES']._serialized_start=2297 + _globals['_USERSHAREDFOLDERACCESSRESPONSES']._serialized_end=2389 + _globals['_ROTATIONSCHEDULE']._serialized_start=2391 + _globals['_ROTATIONSCHEDULE']._serialized_end=2447 + _globals['_APICALLBACKREQUEST']._serialized_start=2450 + _globals['_APICALLBACKREQUEST']._serialized_end=2594 + _globals['_APICALLBACKSCHEDULE']._serialized_start=2596 + _globals['_APICALLBACKSCHEDULE']._serialized_end=2649 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_start=2651 + _globals['_ROUTERSCHEDULEDACTIONS']._serialized_end=2715 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_start=2717 + _globals['_ROUTERRECORDSROTATIONREQUEST']._serialized_end=2806 + _globals['_CONNECTIONPARAMETERS']._serialized_start=2809 + _globals['_CONNECTIONPARAMETERS']._serialized_end=2942 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_start=2944 + _globals['_VALIDATECONNECTIONSREQUEST']._serialized_end=3023 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_start=3025 + _globals['_CONNECTIONVALIDATIONFAILURE']._serialized_end=3099 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_start=3101 + _globals['_VALIDATECONNECTIONSRESPONSE']._serialized_end=3194 + _globals['_GETENFORCEMENTREQUEST']._serialized_start=3196 + _globals['_GETENFORCEMENTREQUEST']._serialized_end=3245 + _globals['_ENFORCEMENTTYPE']._serialized_start=3247 + _globals['_ENFORCEMENTTYPE']._serialized_end=3306 + _globals['_GETENFORCEMENTRESPONSE']._serialized_start=3308 + _globals['_GETENFORCEMENTRESPONSE']._serialized_end=3420 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_start=3422 + _globals['_PEDMTOTPVALIDATEREQUEST']._serialized_end=3501 + _globals['_GETPEDMADMININFORESPONSE']._serialized_start=3503 + _globals['_GETPEDMADMININFORESPONSE']._serialized_end=3575 + _globals['_PAMNETWORKSETTINGS']._serialized_start=3577 + _globals['_PAMNETWORKSETTINGS']._serialized_end=3622 + _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_start=3625 + _globals['_PAMNETWORKCONFIGURATIONREQUEST']._serialized_end=3853 + _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_start=3855 + _globals['_PAMDISCOVERYRULESSETREQUEST']._serialized_end=3937 + _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_start=3939 + _globals['_ROUTER2FAVALIDATEREQUEST']._serialized_end=4027 + _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_start=4029 + _globals['_ROUTER2FASENDPUSHREQUEST']._serialized_end=4155 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_start=4157 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGEREQUEST']._serialized_end=4242 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_start=4244 + _globals['_ROUTER2FAGETWEBAUTHNCHALLENGERESPONSE']._serialized_end=4324 + _globals['_CREATEEPHEMERALSECRETREQUEST']._serialized_start=4326 + _globals['_CREATEEPHEMERALSECRETREQUEST']._serialized_end=4417 # @@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 486c4628..92d676da 100644 --- a/keepersdk-package/src/keepersdk/proto/router_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/router_pb2.pyi @@ -1,11 +1,11 @@ import pam_pb2 as _pam_pb2 import APIRequest_pb2 as _APIRequest_pb2 +import folder_pb2 as _folder_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 from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -91,7 +91,7 @@ class RouterControllerMessage(_message.Message): streamResponse: bool payload: bytes timeout: int - def __init__(self, messageType: _Optional[_Union[_pam_pb2.ControllerMessageType, str]] = ..., messageUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., streamResponse: _Optional[bool] = ..., payload: _Optional[bytes] = ..., timeout: _Optional[int] = ...) -> None: ... + 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", "isPedmAdmin", "mcEnterpriseId") @@ -119,7 +119,7 @@ class RouterUserAuth(_message.Message): mspEnterpriseId: int 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: _Optional[bool] = ..., username: _Optional[str] = ..., mspEnterpriseId: _Optional[int] = ..., isPedmAdmin: _Optional[bool] = ..., mcEnterpriseId: _Optional[int] = ...) -> None: ... + 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", "maxInstanceCount") @@ -163,7 +163,7 @@ class RouterRecordRotation(_message.Message): controllerUid: bytes resourceUid: bytes noSchedule: bool - def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., noSchedule: _Optional[bool] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., noSchedule: bool = ...) -> None: ... class RouterRecordRotationsRequest(_message.Message): __slots__ = ("enterpriseId", "records") @@ -179,7 +179,7 @@ class RouterRecordRotationsResponse(_message.Message): HASMORE_FIELD_NUMBER: _ClassVar[int] rotations: _containers.RepeatedCompositeFieldContainer[RouterRecordRotation] hasMore: bool - def __init__(self, rotations: _Optional[_Iterable[_Union[RouterRecordRotation, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... + def __init__(self, rotations: _Optional[_Iterable[_Union[RouterRecordRotation, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... class RouterRotationInfo(_message.Message): __slots__ = ("status", "configurationUid", "resourceUid", "nodeId", "controllerUid", "controllerName", "scriptName", "pwdComplexity", "disabled") @@ -201,7 +201,7 @@ class RouterRotationInfo(_message.Message): scriptName: str pwdComplexity: str disabled: bool - def __init__(self, status: _Optional[_Union[RouterRotationStatus, str]] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., nodeId: _Optional[int] = ..., controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., scriptName: _Optional[str] = ..., pwdComplexity: _Optional[str] = ..., disabled: _Optional[bool] = ...) -> None: ... + def __init__(self, status: _Optional[_Union[RouterRotationStatus, str]] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., nodeId: _Optional[int] = ..., controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., scriptName: _Optional[str] = ..., pwdComplexity: _Optional[str] = ..., disabled: bool = ...) -> None: ... class RouterRecordRotationRequest(_message.Message): __slots__ = ("recordUid", "revision", "configurationUid", "resourceUid", "schedule", "enterpriseUserId", "pwdComplexity", "disabled", "remoteAddress", "clientVersionId", "noop", "saasConfiguration") @@ -229,7 +229,7 @@ class RouterRecordRotationRequest(_message.Message): clientVersionId: int noop: bool saasConfiguration: bytes - def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., enterpriseUserId: _Optional[int] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: _Optional[bool] = ..., remoteAddress: _Optional[str] = ..., clientVersionId: _Optional[int] = ..., noop: _Optional[bool] = ..., saasConfiguration: _Optional[bytes] = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., revision: _Optional[int] = ..., configurationUid: _Optional[bytes] = ..., resourceUid: _Optional[bytes] = ..., schedule: _Optional[str] = ..., enterpriseUserId: _Optional[int] = ..., pwdComplexity: _Optional[bytes] = ..., disabled: bool = ..., remoteAddress: _Optional[str] = ..., clientVersionId: _Optional[int] = ..., noop: bool = ..., saasConfiguration: _Optional[bytes] = ...) -> None: ... class UserRecordAccessRequest(_message.Message): __slots__ = ("userId", "recordUid") @@ -259,6 +259,28 @@ class UserRecordAccessResponses(_message.Message): responses: _containers.RepeatedCompositeFieldContainer[UserRecordAccessResponse] def __init__(self, responses: _Optional[_Iterable[_Union[UserRecordAccessResponse, _Mapping]]] = ...) -> None: ... +class UserSharedFolderAccessRequest(_message.Message): + __slots__ = ("userId", "sharedFolderUid") + USERID_FIELD_NUMBER: _ClassVar[int] + SHAREDFOLDERUID_FIELD_NUMBER: _ClassVar[int] + userId: int + sharedFolderUid: _containers.RepeatedScalarFieldContainer[bytes] + def __init__(self, userId: _Optional[int] = ..., sharedFolderUid: _Optional[_Iterable[bytes]] = ...) -> None: ... + +class UserSharedFolderAccessResponse(_message.Message): + __slots__ = ("sharedFolderUid", "accessRoleType") + SHAREDFOLDERUID_FIELD_NUMBER: _ClassVar[int] + ACCESSROLETYPE_FIELD_NUMBER: _ClassVar[int] + sharedFolderUid: bytes + accessRoleType: _folder_pb2.AccessRoleType + def __init__(self, sharedFolderUid: _Optional[bytes] = ..., accessRoleType: _Optional[_Union[_folder_pb2.AccessRoleType, str]] = ...) -> None: ... + +class UserSharedFolderAccessResponses(_message.Message): + __slots__ = ("responses",) + RESPONSES_FIELD_NUMBER: _ClassVar[int] + responses: _containers.RepeatedCompositeFieldContainer[UserSharedFolderAccessResponse] + def __init__(self, responses: _Optional[_Iterable[_Union[UserSharedFolderAccessResponse, _Mapping]]] = ...) -> None: ... + class RotationSchedule(_message.Message): __slots__ = ("record_uid", "schedule") RECORD_UID_FIELD_NUMBER: _ClassVar[int] @@ -357,7 +379,7 @@ class GetEnforcementResponse(_message.Message): enforcementTypes: _containers.RepeatedCompositeFieldContainer[EnforcementType] addOnIds: _containers.RepeatedScalarFieldContainer[int] isInTrial: bool - def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ..., addOnIds: _Optional[_Iterable[int]] = ..., isInTrial: _Optional[bool] = ...) -> None: ... + def __init__(self, enforcementTypes: _Optional[_Iterable[_Union[EnforcementType, _Mapping]]] = ..., addOnIds: _Optional[_Iterable[int]] = ..., isInTrial: bool = ...) -> None: ... class PEDMTOTPValidateRequest(_message.Message): __slots__ = ("username", "enterpriseId", "code") @@ -375,7 +397,7 @@ class GetPEDMAdminInfoResponse(_message.Message): PEDMADDONACTIVE_FIELD_NUMBER: _ClassVar[int] isPedmAdmin: bool pedmAddonActive: bool - def __init__(self, isPedmAdmin: _Optional[bool] = ..., pedmAddonActive: _Optional[bool] = ...) -> None: ... + def __init__(self, isPedmAdmin: bool = ..., pedmAddonActive: bool = ...) -> None: ... class PAMNetworkSettings(_message.Message): __slots__ = ("allowedSettings",) @@ -440,3 +462,13 @@ class Router2FAGetWebAuthnChallengeResponse(_message.Message): challenge: str capabilities: _containers.RepeatedScalarFieldContainer[str] def __init__(self, challenge: _Optional[str] = ..., capabilities: _Optional[_Iterable[str]] = ...) -> None: ... + +class CreateEphemeralSecretRequest(_message.Message): + __slots__ = ("encryptedSecret", "secretKeyHash", "ttl") + ENCRYPTEDSECRET_FIELD_NUMBER: _ClassVar[int] + SECRETKEYHASH_FIELD_NUMBER: _ClassVar[int] + TTL_FIELD_NUMBER: _ClassVar[int] + encryptedSecret: bytes + secretKeyHash: bytes + ttl: int + def __init__(self, encryptedSecret: _Optional[bytes] = ..., secretKeyHash: _Optional[bytes] = ..., ttl: _Optional[int] = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py index f77d1963..06cc3bad 100644 --- a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: ssocloud.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'ssocloud.proto' ) diff --git a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi index ac2bf794..af152a2d 100644 --- a/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi +++ b/keepersdk-package/src/keepersdk/proto/ssocloud_pb2.pyi @@ -3,8 +3,7 @@ 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 collections.abc import Iterable as _Iterable, Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -130,7 +129,7 @@ class SsoCloudSettingValue(_message.Message): isFromFile: bool isEditable: bool isRequired: bool - def __init__(self, settingId: _Optional[int] = ..., settingName: _Optional[str] = ..., label: _Optional[str] = ..., value: _Optional[str] = ..., valueType: _Optional[_Union[DataType, str]] = ..., lastModified: _Optional[str] = ..., isFromFile: _Optional[bool] = ..., isEditable: _Optional[bool] = ..., isRequired: _Optional[bool] = ...) -> None: ... + def __init__(self, settingId: _Optional[int] = ..., settingName: _Optional[str] = ..., label: _Optional[str] = ..., value: _Optional[str] = ..., valueType: _Optional[_Union[DataType, str]] = ..., lastModified: _Optional[str] = ..., isFromFile: bool = ..., isEditable: bool = ..., isRequired: bool = ...) -> None: ... class SsoCloudSettingAction(_message.Message): __slots__ = ("settingId", "settingName", "operation", "value") @@ -188,7 +187,7 @@ class SsoCloudConfigurationResponse(_message.Message): ssoCloudSettingValue: _containers.RepeatedCompositeFieldContainer[SsoCloudSettingValue] isShared: bool sharedConfigs: _containers.RepeatedCompositeFieldContainer[SsoSharedConfigItem] - def __init__(self, ssoServiceProviderId: _Optional[int] = ..., ssoSpConfigurationId: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., name: _Optional[str] = ..., protocol: _Optional[str] = ..., lastModified: _Optional[str] = ..., ssoCloudSettingValue: _Optional[_Iterable[_Union[SsoCloudSettingValue, _Mapping]]] = ..., isShared: _Optional[bool] = ..., sharedConfigs: _Optional[_Iterable[_Union[SsoSharedConfigItem, _Mapping]]] = ...) -> None: ... + def __init__(self, ssoServiceProviderId: _Optional[int] = ..., ssoSpConfigurationId: _Optional[int] = ..., enterpriseId: _Optional[int] = ..., name: _Optional[str] = ..., protocol: _Optional[str] = ..., lastModified: _Optional[str] = ..., ssoCloudSettingValue: _Optional[_Iterable[_Union[SsoCloudSettingValue, _Mapping]]] = ..., isShared: bool = ..., sharedConfigs: _Optional[_Iterable[_Union[SsoSharedConfigItem, _Mapping]]] = ...) -> None: ... class SsoIdpTypeRequest(_message.Message): __slots__ = ("ssoIdpTypeId", "tag", "label") @@ -238,7 +237,7 @@ class SsoCloudSAMLLogEntry(_message.Message): samlContent: str isSigned: bool isOK: bool - def __init__(self, serverTime: _Optional[str] = ..., direction: _Optional[str] = ..., messageType: _Optional[str] = ..., messageIssued: _Optional[str] = ..., fromEntityId: _Optional[str] = ..., samlStatus: _Optional[str] = ..., relayState: _Optional[str] = ..., samlContent: _Optional[str] = ..., isSigned: _Optional[bool] = ..., isOK: _Optional[bool] = ...) -> None: ... + def __init__(self, serverTime: _Optional[str] = ..., direction: _Optional[str] = ..., messageType: _Optional[str] = ..., messageIssued: _Optional[str] = ..., fromEntityId: _Optional[str] = ..., samlStatus: _Optional[str] = ..., relayState: _Optional[str] = ..., samlContent: _Optional[str] = ..., isSigned: bool = ..., isOK: bool = ...) -> None: ... class SsoCloudSAMLLogResponse(_message.Message): __slots__ = ("ssoServiceProviderId", "entry") @@ -294,7 +293,7 @@ class ValidationContent(_message.Message): ssoSpConfigurationId: int isSuccessful: bool errorMessage: _containers.RepeatedScalarFieldContainer[str] - def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., isSuccessful: _Optional[bool] = ..., errorMessage: _Optional[_Iterable[str]] = ...) -> None: ... + def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., isSuccessful: bool = ..., errorMessage: _Optional[_Iterable[str]] = ...) -> None: ... class SsoCloudConfigurationValidationResponse(_message.Message): __slots__ = ("validationContent",) @@ -318,7 +317,7 @@ class ConfigurationListItem(_message.Message): name: str isSelected: bool ssoServiceProviderId: _containers.RepeatedScalarFieldContainer[int] - def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., name: _Optional[str] = ..., isSelected: _Optional[bool] = ..., ssoServiceProviderId: _Optional[_Iterable[int]] = ...) -> None: ... + def __init__(self, ssoSpConfigurationId: _Optional[int] = ..., name: _Optional[str] = ..., isSelected: bool = ..., ssoServiceProviderId: _Optional[_Iterable[int]] = ...) -> None: ... class SsoCloudServiceProviderConfigurationListResponse(_message.Message): __slots__ = ("configurationItem",) @@ -346,7 +345,7 @@ class SsoCloudRequest(_message.Message): forceLogin: bool username: str detached: bool - def __init__(self, messageSessionUid: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., embedded: _Optional[bool] = ..., json: _Optional[bool] = ..., dest: _Optional[str] = ..., idpSessionId: _Optional[str] = ..., forceLogin: _Optional[bool] = ..., username: _Optional[str] = ..., detached: _Optional[bool] = ...) -> None: ... + def __init__(self, messageSessionUid: _Optional[bytes] = ..., clientVersion: _Optional[str] = ..., embedded: bool = ..., json: bool = ..., dest: _Optional[str] = ..., idpSessionId: _Optional[str] = ..., forceLogin: bool = ..., username: _Optional[str] = ..., detached: bool = ...) -> None: ... class SsoCloudResponse(_message.Message): __slots__ = ("command", "messageSessionUid", "email", "encryptedLoginToken", "providerName", "idpSessionId", "encryptedSessionToken", "errorToken") @@ -402,7 +401,7 @@ class SamlRelayState(_message.Message): isGeneratedUid: bool deviceId: int detached: bool - def __init__(self, messageSessionUid: _Optional[bytes] = ..., username: _Optional[str] = ..., embedded: _Optional[bool] = ..., json: _Optional[bool] = ..., destId: _Optional[int] = ..., keyId: _Optional[int] = ..., supportedLanguage: _Optional[_Union[_APIRequest_pb2.SupportedLanguage, str]] = ..., checksum: _Optional[int] = ..., isGeneratedUid: _Optional[bool] = ..., deviceId: _Optional[int] = ..., detached: _Optional[bool] = ...) -> None: ... + def __init__(self, messageSessionUid: _Optional[bytes] = ..., username: _Optional[str] = ..., embedded: bool = ..., json: bool = ..., destId: _Optional[int] = ..., keyId: _Optional[int] = ..., supportedLanguage: _Optional[_Union[_APIRequest_pb2.SupportedLanguage, str]] = ..., checksum: _Optional[int] = ..., isGeneratedUid: bool = ..., deviceId: _Optional[int] = ..., detached: bool = ...) -> None: ... class SsoCloudMigrationStatusRequest(_message.Message): __slots__ = ("nodeId", "fullStatus", "includeMigratedUsers", "limit") @@ -414,7 +413,7 @@ class SsoCloudMigrationStatusRequest(_message.Message): fullStatus: bool includeMigratedUsers: bool limit: int - def __init__(self, nodeId: _Optional[int] = ..., fullStatus: _Optional[bool] = ..., includeMigratedUsers: _Optional[bool] = ..., limit: _Optional[int] = ...) -> None: ... + def __init__(self, nodeId: _Optional[int] = ..., fullStatus: bool = ..., includeMigratedUsers: bool = ..., limit: _Optional[int] = ...) -> None: ... class SsoCloudMigrationStatusResponse(_message.Message): __slots__ = ("success", "message", "nodeId", "ssoConnectId", "ssoConnectName", "ssoConnectCloudId", "ssoConnectCloudName", "totalUsersCount", "usersMigratedCount", "migratedUsers", "unmigratedUsers") @@ -440,7 +439,7 @@ class SsoCloudMigrationStatusResponse(_message.Message): usersMigratedCount: int migratedUsers: _containers.RepeatedCompositeFieldContainer[SsoCloudMigrationUserInfo] unmigratedUsers: _containers.RepeatedCompositeFieldContainer[SsoCloudMigrationUserInfo] - def __init__(self, success: _Optional[bool] = ..., message: _Optional[str] = ..., nodeId: _Optional[int] = ..., ssoConnectId: _Optional[int] = ..., ssoConnectName: _Optional[str] = ..., ssoConnectCloudId: _Optional[int] = ..., ssoConnectCloudName: _Optional[str] = ..., totalUsersCount: _Optional[int] = ..., usersMigratedCount: _Optional[int] = ..., migratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ..., unmigratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ...) -> None: ... + def __init__(self, success: bool = ..., message: _Optional[str] = ..., nodeId: _Optional[int] = ..., ssoConnectId: _Optional[int] = ..., ssoConnectName: _Optional[str] = ..., ssoConnectCloudId: _Optional[int] = ..., ssoConnectCloudName: _Optional[str] = ..., totalUsersCount: _Optional[int] = ..., usersMigratedCount: _Optional[int] = ..., migratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ..., unmigratedUsers: _Optional[_Iterable[_Union[SsoCloudMigrationUserInfo, _Mapping]]] = ...) -> None: ... class SsoCloudMigrationUserInfo(_message.Message): __slots__ = ("userId", "email", "fullName", "isMigrated") @@ -452,4 +451,4 @@ class SsoCloudMigrationUserInfo(_message.Message): email: str fullName: str isMigrated: bool - def __init__(self, userId: _Optional[int] = ..., email: _Optional[str] = ..., fullName: _Optional[str] = ..., isMigrated: _Optional[bool] = ...) -> None: ... + def __init__(self, userId: _Optional[int] = ..., email: _Optional[str] = ..., fullName: _Optional[str] = ..., isMigrated: bool = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/tla_pb2.py b/keepersdk-package/src/keepersdk/proto/tla_pb2.py new file mode 100644 index 00000000..60ee4d44 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/tla_pb2.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: tla.proto +# 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, + 29, + 3, + '', + 'tla.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ttla.proto\x12\ncommon.tla\"\x81\x01\n\rTLAProperties\x12\x12\n\nexpiration\x18\x01 \x01(\x03\x12@\n\x15timerNotificationType\x18\x02 \x01(\x0e\x32!.common.tla.TimerNotificationType\x12\x1a\n\x12rotateOnExpiration\x18\x03 \x01(\x08*\\\n\x15TimerNotificationType\x12\x14\n\x10NOTIFICATION_OFF\x10\x00\x12\x10\n\x0cNOTIFY_OWNER\x10\x01\x12\x1b\n\x17NOTIFY_PRIVILEGED_USERS\x10\x02\x42 \n\x1c\x63om.keepersecurity.proto.tlaP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tla_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\034com.keepersecurity.proto.tlaP\001' + _globals['_TIMERNOTIFICATIONTYPE']._serialized_start=157 + _globals['_TIMERNOTIFICATIONTYPE']._serialized_end=249 + _globals['_TLAPROPERTIES']._serialized_start=26 + _globals['_TLAPROPERTIES']._serialized_end=155 +# @@protoc_insertion_point(module_scope) diff --git a/keepersdk-package/src/keepersdk/proto/tla_pb2.pyi b/keepersdk-package/src/keepersdk/proto/tla_pb2.pyi new file mode 100644 index 00000000..08e7a2c3 --- /dev/null +++ b/keepersdk-package/src/keepersdk/proto/tla_pb2.pyi @@ -0,0 +1,25 @@ +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, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class TimerNotificationType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + NOTIFICATION_OFF: _ClassVar[TimerNotificationType] + NOTIFY_OWNER: _ClassVar[TimerNotificationType] + NOTIFY_PRIVILEGED_USERS: _ClassVar[TimerNotificationType] +NOTIFICATION_OFF: TimerNotificationType +NOTIFY_OWNER: TimerNotificationType +NOTIFY_PRIVILEGED_USERS: TimerNotificationType + +class TLAProperties(_message.Message): + __slots__ = ("expiration", "timerNotificationType", "rotateOnExpiration") + EXPIRATION_FIELD_NUMBER: _ClassVar[int] + TIMERNOTIFICATIONTYPE_FIELD_NUMBER: _ClassVar[int] + ROTATEONEXPIRATION_FIELD_NUMBER: _ClassVar[int] + expiration: int + timerNotificationType: TimerNotificationType + rotateOnExpiration: bool + def __init__(self, expiration: _Optional[int] = ..., timerNotificationType: _Optional[_Union[TimerNotificationType, str]] = ..., rotateOnExpiration: bool = ...) -> None: ... diff --git a/keepersdk-package/src/keepersdk/proto/version_pb2.py b/keepersdk-package/src/keepersdk/proto/version_pb2.py index ee97004f..97dd2ff8 100644 --- a/keepersdk-package/src/keepersdk/proto/version_pb2.py +++ b/keepersdk-package/src/keepersdk/proto/version_pb2.py @@ -2,7 +2,7 @@ # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE # source: version.proto -# Protobuf Python Version: 7.34.1 +# 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 @@ -11,9 +11,9 @@ from google.protobuf.internal import builder as _builder _runtime_version.ValidateProtobufRuntimeVersion( _runtime_version.Domain.PUBLIC, - 7, - 34, - 1, + 5, + 29, + 3, '', 'version.proto' ) diff --git a/keepersdk-package/src/keepersdk/vault/memory_nsf_storage.py b/keepersdk-package/src/keepersdk/vault/memory_nsf_storage.py new file mode 100644 index 00000000..83c0fb4f --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/memory_nsf_storage.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from . import nsf_storage_types as nsf +from .nsf_vault_storage import INSFStorage +from ..storage.in_memory import InMemoryEntityStorage, InMemoryLinkStorage, InMemoryRecordStorage + + +class InMemoryNSFStorage(INSFStorage): + def __init__(self) -> None: + self._settings = InMemoryRecordStorage[nsf.NSFSettings]() + self._folders = InMemoryEntityStorage[nsf.NSFFolder, str]() + self._folder_keys = InMemoryLinkStorage[nsf.NSFFolderKey, str, str]() + self._records = InMemoryEntityStorage[nsf.NSFRecord, str]() + self._record_keys = InMemoryLinkStorage[nsf.NSFRecordKey, str, str]() + self._folder_accesses = InMemoryLinkStorage[nsf.NSFFolderAccess, str, str]() + self._record_accesses = InMemoryLinkStorage[nsf.NSFRecordAccess, str, str]() + self._record_links = InMemoryLinkStorage[nsf.NSFRecordLink, str, str]() + self._folder_records = InMemoryLinkStorage[nsf.NSFFolderRecord, str, str]() + self._folder_sharing_states = InMemoryEntityStorage[nsf.NSFFolderSharingState, str]() + self._record_sharing_states = InMemoryEntityStorage[nsf.NSFRecordSharingState, str]() + self._non_shared_data = InMemoryEntityStorage[nsf.NSFNonSharedData, str]() + self._breach_watch_records = InMemoryEntityStorage[nsf.NSFBreachWatchRecord, str]() + self._security_score_data = InMemoryEntityStorage[nsf.NSFSecurityScoreData, str]() + self._breach_watch_security_data = InMemoryEntityStorage[nsf.NSFBreachWatchSecurityData, str]() + self._list_chunks = InMemoryLinkStorage[nsf.NSFListChunk, str, str]() + + @property + def settings(self): + return self._settings + + @property + def folders(self): + return self._folders + + @property + def folder_keys(self): + return self._folder_keys + + @property + def records(self): + return self._records + + @property + def record_keys(self): + return self._record_keys + + @property + def folder_accesses(self): + return self._folder_accesses + + @property + def record_accesses(self): + return self._record_accesses + + @property + def record_links(self): + return self._record_links + + @property + def folder_records(self): + return self._folder_records + + @property + def folder_sharing_states(self): + return self._folder_sharing_states + + @property + def record_sharing_states(self): + return self._record_sharing_states + + @property + def non_shared_data(self): + return self._non_shared_data + + @property + def breach_watch_records(self): + return self._breach_watch_records + + @property + def security_score_data(self): + return self._security_score_data + + @property + def breach_watch_security_data(self): + return self._breach_watch_security_data + + @property + def list_chunks(self): + return self._list_chunks + + def clear_all(self) -> None: + self._settings.delete() + self._folders.clear() + self._folder_keys.clear() + self._records.clear() + self._record_keys.clear() + self._folder_accesses.clear() + self._record_accesses.clear() + self._record_links.clear() + self._folder_records.clear() + self._folder_sharing_states.clear() + self._record_sharing_states.clear() + self._non_shared_data.clear() + self._breach_watch_records.clear() + self._security_score_data.clear() + self._breach_watch_security_data.clear() + self._list_chunks.clear() + + def close(self) -> None: + pass diff --git a/keepersdk-package/src/keepersdk/vault/memory_storage.py b/keepersdk-package/src/keepersdk/vault/memory_storage.py index a96e9385..2828c30c 100644 --- a/keepersdk-package/src/keepersdk/vault/memory_storage.py +++ b/keepersdk-package/src/keepersdk/vault/memory_storage.py @@ -1,11 +1,14 @@ from .vault_storage import IVaultStorage from ..storage.in_memory import InMemoryLinkStorage, InMemoryEntityStorage, InMemoryRecordStorage -from . import storage_types +from . import storage_types, memory_nsf_storage + class InMemoryVaultStorage(IVaultStorage): def __init__(self): self._personal_scope = 'PersonalScopeUid' + self._nsf = memory_nsf_storage.InMemoryNSFStorage() + self._user_settings = InMemoryRecordStorage[storage_types.UserSettings]() self._records = InMemoryEntityStorage[storage_types.StorageRecord, str]() self._record_types = InMemoryEntityStorage[storage_types.StorageRecordType, int]() @@ -90,6 +93,10 @@ def breach_watch_security_data(self): def notifications(self): return self._notifications + @property + def nsf(self): + return self._nsf + def clear(self): self._user_settings.delete() self._records.clear() @@ -110,3 +117,4 @@ def clear(self): self._breach_watch_security_data.clear() self._notifications.clear() + self._nsf.clear_all() diff --git a/keepersdk-package/src/keepersdk/vault/nsf_common.py b/keepersdk-package/src/keepersdk/vault/nsf_common.py new file mode 100644 index 00000000..151f1a44 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_common.py @@ -0,0 +1,340 @@ +"""Shared NSF crypto, role, and recipient-resolution helpers.""" + +from __future__ import annotations + +from typing import Any, Dict, Optional, Tuple + +from .. import crypto, utils +from ..proto import folder_pb2, record_pb2, record_sharing_pb2 +from .vault_online import VaultOnline + +ROLE_NAME_MAP: Dict[str, int] = { + 'contributor': 1, + 'requestor': 1, + 'viewer': 2, + 'shared_manager': 3, + 'share-manager': 3, + 'share_manager': 3, + 'content_manager': 4, + 'content-manager': 4, + 'content_share_manager': 5, + 'content-share-manager': 5, + 'full-manager': 6, + 'full_manager': 6, +} + +_FOLDER_ROLE_PERMISSIONS: Dict[int, Dict[str, bool]] = { + 2: { + 'canAdd': False, 'canRemove': False, 'canDelete': False, + 'canListAccess': True, 'canUpdateAccess': False, 'canChangeOwnership': False, + 'canEditRecords': False, 'canViewRecords': True, + 'canApproveAccess': False, 'canRequestAccess': False, + 'canUpdateSetting': False, 'canListRecords': True, 'canListFolders': True, + }, + 3: { + 'canAdd': False, 'canRemove': False, 'canDelete': False, + 'canListAccess': True, 'canUpdateAccess': True, 'canChangeOwnership': False, + 'canEditRecords': False, 'canViewRecords': True, + 'canApproveAccess': True, 'canRequestAccess': False, + 'canUpdateSetting': False, 'canListRecords': True, 'canListFolders': True, + }, + 4: { + 'canAdd': True, 'canRemove': False, 'canDelete': False, + 'canListAccess': True, 'canUpdateAccess': False, 'canChangeOwnership': False, + 'canEditRecords': True, 'canViewRecords': True, + 'canApproveAccess': False, 'canRequestAccess': False, + 'canUpdateSetting': False, 'canListRecords': True, 'canListFolders': True, + }, + 5: { + 'canAdd': True, 'canRemove': True, 'canDelete': False, + 'canListAccess': True, 'canUpdateAccess': True, 'canChangeOwnership': False, + 'canEditRecords': True, 'canViewRecords': True, + 'canApproveAccess': True, 'canRequestAccess': False, + 'canUpdateSetting': True, 'canListRecords': True, 'canListFolders': True, + }, + 6: { + 'canAdd': True, 'canRemove': True, 'canDelete': True, + 'canListAccess': True, 'canUpdateAccess': True, 'canChangeOwnership': True, + 'canEditRecords': True, 'canViewRecords': True, + 'canApproveAccess': True, 'canRequestAccess': False, + 'canUpdateSetting': True, 'canListRecords': True, 'canListFolders': True, + }, +} + + +def resolve_nsf_role(role: str) -> int: + value = ROLE_NAME_MAP.get(role.strip().lower()) + if value is None: + raise ValueError( + f"Invalid role '{role}'. Accepted: {', '.join(sorted(set(ROLE_NAME_MAP.keys())))}") + return value + + +def get_folder_permissions_for_role(role_type: int) -> folder_pb2.FolderPermissions: + perms_dict = _FOLDER_ROLE_PERMISSIONS.get(role_type) + if perms_dict is None: + raise ValueError(f'Unknown AccessRoleType {role_type}') + perms = folder_pb2.FolderPermissions() + for field, value in perms_dict.items(): + setattr(perms, field, value) + return perms + + +def encrypt_for_recipient(plaintext_key: bytes, public_key, use_ecc: bool) -> bytes: + if use_ecc: + return crypto.encrypt_ec(plaintext_key, public_key) + return crypto.encrypt_rsa(plaintext_key, public_key) + + +def encrypt_record_key_for_folder( + record_key: bytes, + encryption_key: bytes, + record_key_type: Optional[int]) -> Tuple[bytes, int]: + if record_key_type == folder_pb2.encrypted_by_data_key: + return crypto.encrypt_aes_v1(record_key, encryption_key), folder_pb2.encrypted_by_data_key + if record_key_type == folder_pb2.encrypted_by_data_key_gcm: + return crypto.encrypt_aes_v2(record_key, encryption_key), folder_pb2.encrypted_by_data_key_gcm + return crypto.encrypt_aes_v2(record_key, encryption_key), folder_pb2.encrypted_by_data_key_gcm + + +def encrypt_for_team( + plaintext_key: bytes, + team_keys, + *, + forbid_rsa: bool = False) -> Tuple[bytes, int]: + aes = getattr(team_keys, 'aes', None) + ec_bytes = getattr(team_keys, 'ec', None) + rsa_bytes = getattr(team_keys, 'rsa', None) + if rsa_bytes and not forbid_rsa: + rsa_key = crypto.load_rsa_public_key(rsa_bytes) + return crypto.encrypt_rsa(plaintext_key, rsa_key), folder_pb2.encrypted_by_public_key + if ec_bytes: + ec_key = crypto.load_ec_public_key(ec_bytes) + return crypto.encrypt_ec(plaintext_key, ec_key), folder_pb2.encrypted_by_public_key_ecc + if aes: + return crypto.encrypt_aes_v2(plaintext_key, aes), folder_pb2.encrypted_by_data_key_gcm + raise ValueError('No public key found for team') + + +def parse_sharing_status(status) -> Dict[str, Any]: + try: + status_name = record_sharing_pb2.SharingStatus.Name(status.status) + except Exception: + status_name = str(status.status) + is_success = status.status == record_sharing_pb2.SUCCESS + is_pending = status.status == record_sharing_pb2.PENDING_ACCEPT + return { + 'record_uid': utils.base64_url_encode(status.recordUid), + 'recipient_uid': utils.base64_url_encode(status.recipientUid), + 'status': status_name, + 'message': status.message, + 'success': is_success or is_pending, + 'pending': is_pending, + } + + +def parse_folder_access_result( + response: folder_pb2.FolderAccessResponse, + folder_uid: str, + accessor_label: str, + default_message: str) -> Dict[str, Any]: + if response.folderAccessResults: + result = response.folderAccessResults[0] + status_value = result.status + is_failure = (status_value != 0) or bool(result.message) + status_name = ( + folder_pb2.FolderModifyStatus.Name(status_value) + if status_value != 0 else 'SUCCESS') + return { + 'folder_uid': folder_uid, + 'accessor': accessor_label, + 'status': 'ERROR' if is_failure and status_value == 0 else status_name, + 'message': result.message or default_message, + 'success': not is_failure, + } + return { + 'folder_uid': folder_uid, + 'accessor': accessor_label, + 'status': 'SUCCESS', + 'message': default_message, + 'success': True, + } + + +def load_user_public_key(vault: VaultOnline, user_email: str): + auth = vault.keeper_auth + keys = auth.get_user_keys(user_email) + if not keys: + auth.load_user_public_keys([user_email], send_invites=False) + keys = auth.get_user_keys(user_email) + if not keys: + raise ValueError(f'Public key not found for user {user_email}') + if keys.rsa: + return crypto.load_rsa_public_key(keys.rsa), False + if keys.ec: + return crypto.load_ec_public_key(keys.ec), True + raise ValueError(f'No valid public key for user {user_email}') + + +def resolve_user_uid_bytes(vault: VaultOnline, identifier: str) -> Optional[bytes]: + if '@' in identifier: + lower = identifier.casefold() + rq = record_pb2.GetShareObjectsRequest() + rs = vault.keeper_auth.execute_auth_rest( + 'vault/get_share_objects', rq, response_type=record_pb2.GetShareObjectsResponse) + if rs is not None: + for users in ( + rs.shareRelationships, rs.shareFamilyUsers, + rs.shareEnterpriseUsers, rs.shareMCEnterpriseUsers): + for su in users: + if su.username.casefold() == lower and su.userAccountUid: + uid = su.userAccountUid + return uid if isinstance(uid, bytes) else utils.base64_url_decode(uid) + return None + try: + return utils.base64_url_decode(identifier) + except Exception: + return None + + +def get_user_public_key( + vault: VaultOnline, + recipient_email: str, + *, + require_uid: bool = True) -> Tuple[Any, bool, Optional[bytes], bool]: + auth = vault.keeper_auth + needs_invite = False + recipient_uid_bytes = resolve_user_uid_bytes(vault, recipient_email) + try: + public_key, use_ecc = load_user_public_key(vault, recipient_email) + except ValueError: + public_key, use_ecc = None, False + if '@' in recipient_email: + pending = auth.load_user_public_keys([recipient_email], send_invites=True) + if pending: + needs_invite = True + try: + public_key, use_ecc = load_user_public_key(vault, recipient_email) + except ValueError: + pass + if not public_key: + if needs_invite: + raise ValueError( + f"Share invitation sent to '{recipient_email}'. " + f"Repeat after the invitation is accepted.") + raise ValueError(f"User {recipient_email} has no public key") + if not recipient_uid_bytes: + recipient_uid_bytes = resolve_user_uid_bytes(vault, recipient_email) + if require_uid and not recipient_uid_bytes: + raise ValueError(f"User {recipient_email} not found") + return public_key, use_ecc, recipient_uid_bytes, needs_invite + + +def resolve_team_uid_bytes(vault: VaultOnline, team_identifier: str) -> Optional[bytes]: + from . import share_management_utils + + share_objects = share_management_utils.get_share_objects(vault) + teams = share_objects.get('teams') or {} + if team_identifier in teams: + return utils.base64_url_decode(team_identifier) + lower = team_identifier.casefold() + for uid, team in teams.items(): + name = team.get('name') if isinstance(team, dict) else '' + if name and name.casefold() == lower: + return utils.base64_url_decode(uid) + try: + return utils.base64_url_decode(team_identifier) + except Exception: + return None + + +def resolve_team_identifier(vault: VaultOnline, team_identifier: str) -> Optional[Tuple[str, bytes]]: + uid_bytes = resolve_team_uid_bytes(vault, team_identifier) + if not uid_bytes: + return None + return utils.base64_url_encode(uid_bytes), uid_bytes + + +_FOLDER_ACCESS_ROLE_DISPLAY = { + 'NAVIGATOR': 'contributor', + 'REQUESTOR': 'contributor', + 'VIEWER': 'viewer', + 'SHARED_MANAGER': 'share-manager', + 'CONTENT_MANAGER': 'content-manager', + 'CONTENT_SHARE_MANAGER': 'content-share-manager', + 'MANAGER': 'full-manager', + 'UNRESOLVED': 'unresolved', +} + + +def is_nsf_folder_owner( + accessor: Dict[str, Any], + owner_username: Optional[str] = None, + owner_account_uid: Optional[str] = None) -> bool: + """Return True when *accessor* matches folder ownerInfo from sync-down.""" + if not accessor: + return False + if accessor.get('access_type') == 'AT_OWNER': + return True + if owner_username: + username = (accessor.get('username') or '').lower() + if username and username == owner_username.lower(): + return True + if owner_account_uid: + accessor_uid = accessor.get('accessor_uid') or '' + if accessor_uid and accessor_uid == owner_account_uid: + return True + return False + + +def folder_access_role_label( + accessor: Dict[str, Any], + owner_username: Optional[str] = None, + owner_account_uid: Optional[str] = None) -> str: + """Display label for an NSF folder accessor (``owner``, ``full-manager``, etc.).""" + if accessor.get('owner') or is_nsf_folder_owner( + accessor, owner_username, owner_account_uid): + return 'owner' + role_name = accessor.get('role') + if role_name: + key = str(role_name).upper().replace('-', '_') + return _FOLDER_ACCESS_ROLE_DISPLAY.get( + key, str(role_name).lower().replace('_', '-')) + role_type = accessor.get('access_role_type') + if isinstance(role_type, int): + try: + name = folder_pb2.AccessRoleType.Name(role_type) + return _FOLDER_ACCESS_ROLE_DISPLAY.get( + name, name.lower().replace('_', '-')) + except Exception: + pass + perms = accessor.get('permissions') or {} + if perms.get('can_change_ownership'): + return 'full-manager' + if perms.get('can_update_access'): + return 'share-manager' + if perms.get('can_edit_records'): + return 'content-manager' + if perms.get('can_view_records'): + return 'viewer' + return 'unknown' + + +def access_role_label(access: Dict[str, Any]) -> str: + if access.get('owner'): + return 'owner' + role_type = access.get('access_role_type') + if isinstance(role_type, int): + try: + name = folder_pb2.AccessRoleType.Name(role_type) + return name.lower().replace('_', '-') + except Exception: + pass + role = access.get('access_type') or access.get('role') + if isinstance(role, str) and role: + return role.lower().replace('_', '-') + if access.get('can_edit'): + return 'content-manager' + if access.get('can_view') or access.get('can_view_title'): + return 'viewer' + return 'unknown' diff --git a/keepersdk-package/src/keepersdk/vault/nsf_crypto.py b/keepersdk-package/src/keepersdk/vault/nsf_crypto.py new file mode 100644 index 00000000..43cd3e57 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_crypto.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import json +from typing import Dict, List, Optional + +from .. import crypto, utils +from ..authentication import keeper_auth +from ..proto import folder_pb2 +from . import nsf_storage_types as nsf +from .nsf_vault_storage import INSFStorage + +_FOLDER_KEY_ENCRYPTION = folder_pb2.FolderKeyEncryptionType +_ENCRYPTED_KEY_TYPE = folder_pb2.EncryptedKeyType + + +def try_decrypt_symmetric(encrypted_key: bytes, symmetric_key: bytes) -> Optional[bytes]: + try: + return crypto.decrypt_aes_v2(encrypted_key, symmetric_key) + except Exception: + pass + try: + return crypto.decrypt_aes_v1(encrypted_key, symmetric_key) + except Exception: + pass + return None + + +def try_decrypt_with_user_keys(encrypted_key: bytes, auth_context: keeper_auth.AuthContext) -> Optional[bytes]: + result = try_decrypt_symmetric(encrypted_key, auth_context.data_key) + if result is not None: + return result + if auth_context.rsa_private_key is not None: + try: + return crypto.decrypt_rsa(encrypted_key, auth_context.rsa_private_key) + except Exception: + pass + if auth_context.ec_private_key is not None: + try: + return crypto.decrypt_ec(encrypted_key, auth_context.ec_private_key) + except Exception: + pass + return None + + +def try_decrypt_from_folder_access( + folder_uid: str, + storage: INSFStorage, + auth_context: keeper_auth.AuthContext) -> Optional[bytes]: + for fa in storage.folder_accesses.get_links_by_subject(folder_uid): + if not fa.folder_key_encrypted: + continue + try: + enc_key = utils.base64_url_decode(fa.folder_key_encrypted) + key_type = fa.folder_key_type + if key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_data_key_gcm): + return crypto.decrypt_aes_v2(enc_key, auth_context.data_key) + if key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_data_key): + return crypto.decrypt_aes_v1(enc_key, auth_context.data_key) + if key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_public_key): + if auth_context.rsa_private_key is not None: + return crypto.decrypt_rsa(enc_key, auth_context.rsa_private_key) + elif key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_public_key_ecc): + if auth_context.ec_private_key is not None: + return crypto.decrypt_ec(enc_key, auth_context.ec_private_key) + else: + result = try_decrypt_with_user_keys(enc_key, auth_context) + if result is not None: + return result + except Exception: + continue + return None + + +def try_decrypt_folder_key( + fk: nsf.NSFFolderKey, + auth_context: keeper_auth.AuthContext, + decrypted_folder_keys: Dict[str, bytes]) -> Optional[bytes]: + if not fk.folder_key: + return None + try: + enc_key_type = fk.encrypted_by + encrypted_key = utils.base64_url_decode(fk.folder_key) + if enc_key_type == int(_FOLDER_KEY_ENCRYPTION.ENCRYPTED_BY_USER_KEY): + return try_decrypt_with_user_keys(encrypted_key, auth_context) + if enc_key_type == int(_FOLDER_KEY_ENCRYPTION.ENCRYPTED_BY_PARENT_KEY): + if not fk.parent_uid: + return None + parent_key = decrypted_folder_keys.get(fk.parent_uid) + if parent_key is None: + return None + return try_decrypt_symmetric(encrypted_key, parent_key) + except Exception: + return None + return None + + +def try_decrypt_folder_entity_key( + row: nsf.NSFFolder, + auth_context: keeper_auth.AuthContext, + decrypted_folder_keys: Dict[str, bytes]) -> Optional[bytes]: + if not row.folder_key: + return None + try: + encrypted_key = utils.base64_url_decode(row.folder_key) + key = try_decrypt_with_user_keys(encrypted_key, auth_context) + if key is not None: + return key + if row.parent_uid: + parent_key = decrypted_folder_keys.get(row.parent_uid) + if parent_key is not None: + return try_decrypt_symmetric(encrypted_key, parent_key) + except Exception: + pass + return None + + +def decrypt_folder_keys( + storage: INSFStorage, + auth_context: keeper_auth.AuthContext) -> Dict[str, bytes]: + decrypted_keys: Dict[str, bytes] = {} + keys_by_folder: Dict[str, List[nsf.NSFFolderKey]] = {} + for fk in storage.folder_keys.get_all_links(): + keys_by_folder.setdefault(fk.folder_uid, []).append(fk) + folder_rows = list(storage.folders.get_all_entities()) + + progress = True + while progress: + progress = False + for folder_uid, folder_keys in keys_by_folder.items(): + if folder_uid in decrypted_keys: + continue + for fk in folder_keys: + key = try_decrypt_folder_key(fk, auth_context, decrypted_keys) + if key is not None: + decrypted_keys[folder_uid] = key + progress = True + break + for row in folder_rows: + if row.folder_uid in decrypted_keys: + continue + key = try_decrypt_folder_entity_key(row, auth_context, decrypted_keys) + if key is not None: + decrypted_keys[row.folder_uid] = key + progress = True + + for folder_uid in keys_by_folder: + if folder_uid not in decrypted_keys: + key = try_decrypt_from_folder_access(folder_uid, storage, auth_context) + if key is not None: + decrypted_keys[folder_uid] = key + + for row in storage.folders.get_all_entities(): + if row.folder_uid not in decrypted_keys: + key = try_decrypt_from_folder_access(row.folder_uid, storage, auth_context) + if key is not None: + decrypted_keys[row.folder_uid] = key + + return decrypted_keys + + +def decrypt_record_keys( + storage: INSFStorage, + decrypted_folder_keys: Dict[str, bytes], + auth_context: keeper_auth.AuthContext) -> Dict[str, bytes]: + decrypted_keys: Dict[str, bytes] = {} + for rk in storage.record_keys.get_all_links(): + if rk.record_uid in decrypted_keys or not rk.record_key: + continue + try: + encrypted_key = utils.base64_url_decode(rk.record_key) + enc_key_type = rk.record_key_type + folder_enc_type = rk.folder_key_encryption_type + record_key: Optional[bytes] = None + + if enc_key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_public_key): + if auth_context.rsa_private_key is not None: + record_key = crypto.decrypt_rsa(encrypted_key, auth_context.rsa_private_key) + elif enc_key_type == int(_ENCRYPTED_KEY_TYPE.encrypted_by_public_key_ecc): + if auth_context.ec_private_key is not None: + record_key = crypto.decrypt_ec(encrypted_key, auth_context.ec_private_key) + else: + folder_key = decrypted_folder_keys.get(rk.folder_uid) if rk.folder_uid else None + if (folder_enc_type == int(_FOLDER_KEY_ENCRYPTION.ENCRYPTED_BY_USER_KEY) + or not rk.folder_uid): + record_key = try_decrypt_symmetric(encrypted_key, auth_context.data_key) + if record_key is None and folder_key is not None: + record_key = try_decrypt_symmetric(encrypted_key, folder_key) + else: + if folder_key is not None: + record_key = try_decrypt_symmetric(encrypted_key, folder_key) + if record_key is None: + record_key = try_decrypt_symmetric(encrypted_key, auth_context.data_key) + if record_key is None and auth_context.rsa_private_key is not None: + try: + record_key = crypto.decrypt_rsa(encrypted_key, auth_context.rsa_private_key) + except Exception: + pass + if record_key is None and auth_context.ec_private_key is not None: + try: + record_key = crypto.decrypt_ec(encrypted_key, auth_context.ec_private_key) + except Exception: + pass + + if record_key is not None: + decrypted_keys[rk.record_uid] = record_key + except Exception: + continue + return decrypted_keys + + +def decrypt_folder_name(encrypted_data_b64: str, folder_key: bytes) -> Optional[str]: + if not encrypted_data_b64: + return None + try: + data_bytes = crypto.decrypt_aes_v2(utils.base64_url_decode(encrypted_data_b64), folder_key) + payload = json.loads(data_bytes.decode('utf-8')) + if isinstance(payload, dict): + name = payload.get('name') + return str(name) if name is not None else None + except Exception: + return None + return None + + +def decrypt_record_data( + encrypted_data_b64: str, + record_key: bytes, + *, + version: int = 3) -> Optional[str]: + if not encrypted_data_b64: + return None + try: + encrypted = utils.base64_url_decode(encrypted_data_b64) + if version <= 2: + data_bytes = crypto.decrypt_aes_v1(encrypted, record_key) + else: + data_bytes = crypto.decrypt_aes_v2(encrypted, record_key) + return data_bytes.decode('utf-8') + except Exception: + return None diff --git a/keepersdk-package/src/keepersdk/vault/nsf_data.py b/keepersdk-package/src/keepersdk/vault/nsf_data.py new file mode 100644 index 00000000..6e053fec --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_data.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, Iterable, List, Optional, Set + +from ..authentication import keeper_auth +from . import nsf_crypto, nsf_storage_types as nsf +from .nsf_vault_storage import INSFStorage + + +class NSFRebuildTask: + """Tracks UIDs touched during incremental sync-down.""" + + def __init__(self, is_full_sync: bool) -> None: + self.is_full_sync = is_full_sync + self.folder_uids: Set[str] = set() + self.record_uids: Set[str] = set() + + def add_folder(self, folder_uid: str) -> None: + if self.is_full_sync or not folder_uid: + return + self.folder_uids.add(folder_uid) + + def add_folders(self, folder_uids: Iterable[str]) -> None: + if self.is_full_sync: + return + self.folder_uids.update((x for x in folder_uids if x)) + + def add_record(self, record_uid: str) -> None: + if self.is_full_sync or not record_uid: + return + self.record_uids.add(record_uid) + + def add_records(self, record_uids: Iterable[str]) -> None: + if self.is_full_sync: + return + self.record_uids.update((x for x in record_uids if x)) + + +@dataclass +class NSFFolderNode: + folder_uid: str + parent_uid: Optional[str] = None + name: Optional[str] = None + folder_key: Optional[bytes] = None + subfolder_uids: List[str] = field(default_factory=list) + record_uids: List[str] = field(default_factory=list) + + +@dataclass +class NSFRecordEntry: + record_uid: str + revision: int = 0 + version: int = 0 + shared: bool = False + client_modified_time: int = 0 + file_size: int = 0 + thumbnail_size: int = 0 + record_key: Optional[bytes] = None + decrypted_data: Optional[str] = None + + +class NSFData: + """In-memory decrypted NSF view, rebuilt from encrypted storage after sync-down.""" + + def __init__( + self, + storage: INSFStorage, + auth_context: Optional[keeper_auth.AuthContext] = None) -> None: + self._storage = storage + self._auth_context = auth_context + self._folders: Dict[str, NSFFolderNode] = {} + self._records: Dict[str, NSFRecordEntry] = {} + if auth_context is not None: + self.rebuild_nsf(auth_context) + + @property + def storage(self) -> INSFStorage: + return self._storage + + def folders(self) -> Iterable[NSFFolderNode]: + return self._folders.values() + + def records(self) -> Iterable[NSFRecordEntry]: + return self._records.values() + + def get_folder(self, folder_uid: str) -> Optional[NSFFolderNode]: + return self._folders.get(folder_uid) + + def get_record(self, record_uid: str) -> Optional[NSFRecordEntry]: + return self._records.get(record_uid) + + @property + def folder_count(self) -> int: + return len(self._folders) + + @property + def record_count(self) -> int: + return len(self._records) + + def rebuild_data(self, changes: Optional[NSFRebuildTask] = None) -> None: + """Rebuild in-memory views (always full decrypt rebuild.""" + del changes + self.rebuild_nsf(self._auth_context) + + def rebuild_nsf(self, auth_context: Optional[keeper_auth.AuthContext]) -> None: + self._folders.clear() + self._records.clear() + if auth_context is None: + return + + decrypted_folder_keys = nsf_crypto.decrypt_folder_keys(self._storage, auth_context) + decrypted_record_keys = nsf_crypto.decrypt_record_keys( + self._storage, decrypted_folder_keys, auth_context) + + for row in self._storage.folders.get_all_entities(): + folder_key = decrypted_folder_keys.get(row.folder_uid) + name = None + if folder_key is not None: + name = nsf_crypto.decrypt_folder_name(row.data, folder_key) + node = NSFFolderNode( + folder_uid=row.folder_uid, + parent_uid=row.parent_uid or None, + name=name or '(NSF Folder)', + folder_key=folder_key, + ) + self._folders[row.folder_uid] = node + + for node in self._folders.values(): + if node.parent_uid and node.parent_uid in self._folders: + self._folders[node.parent_uid].subfolder_uids.append(node.folder_uid) + + for link in self._storage.folder_records.get_all_links(): + folder = self._folders.get(link.folder_uid) + if folder is not None: + folder.record_uids.append(link.record_uid) + + for row in self._storage.records.get_all_entities(): + record_key = decrypted_record_keys.get(row.record_uid) + if record_key is None: + continue + decrypted = nsf_crypto.decrypt_record_data( + row.data, record_key, version=row.version) + self._records[row.record_uid] = NSFRecordEntry( + record_uid=row.record_uid, + revision=row.revision, + version=row.version, + shared=row.shared, + client_modified_time=row.client_modified_time, + file_size=row.file_size, + thumbnail_size=row.thumbnail_size, + record_key=record_key, + decrypted_data=decrypted, + ) + + self._purge_orphaned_records() + + def _purge_orphaned_records(self) -> None: + linked: Set[str] = set() + for fr in self._storage.folder_records.get_all_links(): + linked.add(fr.record_uid) + for uid in list(self._records): + if uid not in linked: + del self._records[uid] diff --git a/keepersdk-package/src/keepersdk/vault/nsf_folder_records.py b/keepersdk-package/src/keepersdk/vault/nsf_folder_records.py new file mode 100644 index 00000000..dcb5b055 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_folder_records.py @@ -0,0 +1,246 @@ +"""NSF folder-record linking and shortcut management.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Dict, List, Optional, Set + +from .. import utils +from ..errors import KeeperApiError +from ..proto import folder_pb2 +from . import nsf_common +from .nsf_management import ( + NsfError, + _get_folder_key, + _get_record_key, + _nsf_view, + _request_sync, + is_nsf_folder, + resolve_nsf_folder_uid, + resolve_nsf_record_uid, +) +from .vault_online import VaultOnline + + +@dataclass +class NsfFolderRecordResult: + folder_uid: str + record_uid: str + success: bool + status: str = '' + message: str = '' + + +@dataclass(frozen=True) +class NsfShortcutRow: + record_uid: str + title: str + folder_uids: List[str] + + +def _record_key_type(vault: VaultOnline, record_uid: str) -> Optional[int]: + for rk in _nsf_view(vault).storage.record_keys.get_links_by_subject(record_uid): + if rk.record_key_type: + return rk.record_key_type + return None + + +def _build_record_metadata( + vault: VaultOnline, + folder_uid: str, + record_uid: str, + *, + expiration_timestamp: Optional[int] = None) -> folder_pb2.RecordMetadata: + folder_key = _get_folder_key(vault, folder_uid) + record_key = _get_record_key(vault, record_uid) + rkt = _record_key_type(vault, record_uid) + enc_rk, enc_rkt = nsf_common.encrypt_record_key_for_folder(record_key, folder_key, rkt) + rm = folder_pb2.RecordMetadata() + rm.recordUid = utils.base64_url_decode(record_uid) + rm.encryptedRecordKey = enc_rk + rm.encryptedRecordKeyType = enc_rkt + if expiration_timestamp is not None: + rm.tlaProperties.expiration = expiration_timestamp + return rm + + +def _build_removal_metadata(record_uid: str) -> folder_pb2.RecordMetadata: + rm = folder_pb2.RecordMetadata() + rm.recordUid = utils.base64_url_decode(record_uid) + rm.encryptedRecordKey = b'' + rm.encryptedRecordKeyType = folder_pb2.no_key + return rm + + +def _folder_record_update( + vault: VaultOnline, + folder_uid: str, + *, + add_records: Optional[List[folder_pb2.RecordMetadata]] = None, + update_records: Optional[List[folder_pb2.RecordMetadata]] = None, + remove_records: Optional[List[folder_pb2.RecordMetadata]] = None) -> folder_pb2.FolderRecordUpdateResponse: + rq = folder_pb2.FolderRecordUpdateRequest() + rq.folderUid = utils.base64_url_decode(folder_uid) + if add_records: + rq.addRecords.extend(add_records) + if update_records: + rq.updateRecords.extend(update_records) + if remove_records: + rq.removeRecords.extend(remove_records) + response = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/record_update', + rq, + response_type=folder_pb2.FolderRecordUpdateResponse) + assert response is not None + return response + + +def _parse_folder_record_result( + response: folder_pb2.FolderRecordUpdateResponse, + folder_uid: str, + record_uid: str, + default_message: str) -> NsfFolderRecordResult: + if response.folderRecordUpdateResult: + row = response.folderRecordUpdateResult[0] + status_name = folder_pb2.FolderModifyStatus.Name(row.status) + return NsfFolderRecordResult( + folder_uid=folder_uid, + record_uid=record_uid, + success=row.status == folder_pb2.SUCCESS, + status=status_name, + message=row.message, + ) + return NsfFolderRecordResult( + folder_uid=folder_uid, + record_uid=record_uid, + success=True, + status='SUCCESS', + message=default_message, + ) + + +def link_nsf_record_to_folder( + vault: VaultOnline, + record_identifier: str, + folder_identifier: str, + *, + request_sync: bool = True) -> NsfFolderRecordResult: + """Link a record into an NSF folder.""" + record_uid = resolve_nsf_record_uid(vault, record_identifier) + if not record_uid: + raise NsfError(f'NSF record not found: {record_identifier}') + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if not is_nsf_folder(vault, folder_uid): + raise NsfError(f'NSF folder not found: {folder_identifier}') + + rm = _build_record_metadata(vault, folder_uid, record_uid) + response = _folder_record_update(vault, folder_uid, add_records=[rm]) + result = _parse_folder_record_result( + response, folder_uid, record_uid, 'Record linked to folder successfully') + if not result.success: + raise KeeperApiError(result.status, result.message) + _request_sync(vault, request_sync) + return result + + +def unlink_nsf_record_from_folder( + vault: VaultOnline, + record_uid: str, + folder_uid: str, + *, + request_sync: bool = True) -> NsfFolderRecordResult: + """Remove a record link from an NSF folder.""" + resolved_folder = resolve_nsf_folder_uid(vault, folder_uid) or folder_uid + resolved_record = resolve_nsf_record_uid(vault, record_uid) or record_uid + rm = _build_removal_metadata(resolved_record) + response = _folder_record_update(vault, resolved_folder, remove_records=[rm]) + result = _parse_folder_record_result( + response, resolved_folder, resolved_record, 'Record unlinked from folder') + if not result.success: + raise KeeperApiError(result.status, result.message) + _request_sync(vault, request_sync) + return result + + +def get_nsf_shortcut_map(vault: VaultOnline) -> Dict[str, Set[str]]: + """Return ``{record_uid: {folder_uids}}`` for records in 2+ NSF folders.""" + records: Dict[str, Set[str]] = {} + for folder in _nsf_view(vault).folders(): + for record_uid in folder.record_uids: + records.setdefault(record_uid, set()).add(folder.folder_uid) + return {uid: folders for uid, folders in records.items() if len(folders) > 1} + + +def list_nsf_shortcuts( + vault: VaultOnline, + *, + target: Optional[str] = None) -> List[NsfShortcutRow]: + """List NSF records linked to multiple folders.""" + shortcuts = get_nsf_shortcut_map(vault) + if not shortcuts: + return [] + + if target: + record_uid = resolve_nsf_record_uid(vault, target) + if record_uid: + if record_uid not in shortcuts: + raise NsfError(f'Record {target} does not have shortcuts') + uids = {record_uid} + else: + folder_uid = resolve_nsf_folder_uid(vault, target) + if folder_uid: + uids = {r for r, folders in shortcuts.items() if folder_uid in folders} + else: + raise NsfError(f'Target "{target}" is not a known record or folder') + else: + uids = set(shortcuts.keys()) + + rows: List[NsfShortcutRow] = [] + view = _nsf_view(vault) + for record_uid in sorted(uids): + entry = view.get_record(record_uid) + title = record_uid + if entry and entry.decrypted_data: + try: + payload = json.loads(entry.decrypted_data) + if isinstance(payload, dict) and payload.get('title'): + title = str(payload['title']) + except json.JSONDecodeError: + pass + rows.append(NsfShortcutRow( + record_uid=record_uid, + title=title, + folder_uids=sorted(shortcuts[record_uid]), + )) + return rows + + +def keep_nsf_shortcut_in_folder( + vault: VaultOnline, + record_identifier: str, + keep_folder_identifier: str, + *, + request_sync: bool = True) -> List[NsfFolderRecordResult]: + """Keep a shortcut record in one folder; unlink from all others.""" + record_uid = resolve_nsf_record_uid(vault, record_identifier) + if not record_uid: + raise NsfError(f'NSF record not found: {record_identifier}') + keep_folder = resolve_nsf_folder_uid(vault, keep_folder_identifier) or keep_folder_identifier + if not is_nsf_folder(vault, keep_folder): + raise NsfError(f'NSF folder not found: {keep_folder_identifier}') + + shortcuts = get_nsf_shortcut_map(vault) + if record_uid not in shortcuts: + raise NsfError(f'Record "{record_identifier}" is not linked to multiple folders') + if keep_folder not in shortcuts[record_uid]: + raise NsfError(f'Record is not in folder {keep_folder_identifier}') + + results: List[NsfFolderRecordResult] = [] + for folder_uid in shortcuts[record_uid]: + if folder_uid == keep_folder: + continue + results.append(unlink_nsf_record_from_folder( + vault, record_uid, folder_uid, request_sync=False)) + _request_sync(vault, request_sync) + return results diff --git a/keepersdk-package/src/keepersdk/vault/nsf_management.py b/keepersdk-package/src/keepersdk/vault/nsf_management.py new file mode 100644 index 00000000..6d8b53e5 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_management.py @@ -0,0 +1,1294 @@ +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Mapping, Optional + +from .. import crypto, utils +from ..errors import KeeperApiError +from ..proto import folder_pb2, record_endpoints_pb2, record_pb2, remove_pb2, record_details_pb2, folder_access_pb2 +from . import nsf_crypto, nsf_data, nsf_common, sync_down, vault_extensions +from .vault_online import VaultOnline + +ROOT_FOLDER_UID = 'AAAAAAAAAAAAAAAAAPmtNA' +"""Sentinel UID the server uses for the NSF root folder.""" + + +class NsfError(ValueError): + """Raised when NSF operations cannot proceed (missing cache, bad identifier, etc.).""" + + +@dataclass(frozen=True) +class NsfListRow: + item_type: str + uid: str + title: str + record_type: str = '' + description: str = '' + + +@dataclass +class NsfModifyResult: + record_uid: str + success: bool + status: str = '' + message: str = '' + revision: int = 0 + + +@dataclass +class NsfFolderModifyResult: + folder_uid: str + success: bool + status: str = '' + message: str = '' + + +@dataclass +class NsfRemovePreviewItem: + item_uid: str + folder_uid: str = '' + status: str = '' + impact: Optional[Dict[str, Any]] = None + error: Optional[Dict[str, str]] = None + + +@dataclass +class NsfRemoveResult: + preview_results: List[NsfRemovePreviewItem] + confirmed: bool = False + confirmation_token_expires_at: Optional[int] = None + + +_RECORD_REMOVE_OPS = { + 'unlink': remove_pb2.UNLINK_FROM_FOLDER, + 'folder-trash': remove_pb2.MOVE_TO_FOLDER_TRASH, + 'owner-trash': remove_pb2.MOVE_TO_OWNER_TRASH, +} + +_FOLDER_REMOVE_OPS = { + 'folder-trash': remove_pb2.FOLDER_MOVE_TO_FOLDER_TRASH, + 'delete-permanent': remove_pb2.FOLDER_DELETE_PERMANENT, +} + + +def _nsf_view(vault: VaultOnline) -> nsf_data.NSFData: + view = vault.nsf_data + if view is None: + raise NsfError('NSF storage is not available on this vault') + return view + + +def _normalize_parent_uid(parent_uid: Optional[str]) -> str: + if not parent_uid or parent_uid == ROOT_FOLDER_UID: + return 'root' + return parent_uid + + +def is_nsf_folder(vault: VaultOnline, folder_uid: str) -> bool: + if folder_uid == ROOT_FOLDER_UID: + return True + return _nsf_view(vault).get_folder(folder_uid) is not None + + +def is_nsf_record(vault: VaultOnline, record_uid: str) -> bool: + return _nsf_view(vault).get_record(record_uid) is not None + + +def resolve_nsf_folder_uid(vault: VaultOnline, identifier: str) -> Optional[str]: + """Resolve folder UID or exact name (case-insensitive) from the NSF cache.""" + if not identifier: + return None + view = _nsf_view(vault) + if identifier in {f.folder_uid for f in view.folders()}: + return identifier + if identifier.lower() in ('root', 'my drive'): + return ROOT_FOLDER_UID + lower = identifier.casefold() + matches = [f.folder_uid for f in view.folders() + if (f.name or '').casefold() == lower] + if len(matches) == 1: + return matches[0] + return None + + +def resolve_nsf_record_uid(vault: VaultOnline, identifier: str) -> Optional[str]: + """Resolve record UID or title from decrypted NSF cache.""" + if not identifier: + return None + view = _nsf_view(vault) + if view.get_record(identifier) is not None: + return identifier + lower = identifier.casefold() + matches: List[str] = [] + for entry in view.records(): + title = _record_title_from_decrypted(entry) + if title.casefold() == lower: + matches.append(entry.record_uid) + if len(matches) == 1: + return matches[0] + return None + + +def _record_title_from_decrypted(entry: nsf_data.NSFRecordEntry) -> str: + if not entry.decrypted_data: + return entry.record_uid + try: + payload = json.loads(entry.decrypted_data) + if isinstance(payload, dict): + title = payload.get('title') + if title: + return str(title) + except json.JSONDecodeError: + pass + return entry.record_uid + + +def _parse_record_payload(decrypted: Optional[str]) -> Dict[str, Any]: + if not decrypted: + return {} + try: + payload = json.loads(decrypted) + return payload if isinstance(payload, dict) else {} + except json.JSONDecodeError: + return {} + + +_RECORD_DETAILS_URL = 'vault/get_records_details' +_RECORD_DETAILS_CHUNK = 500 +_MAX_V2_RECORD_VERSION = 2 + + +def _record_payload_from_entry( + view: nsf_data.NSFData, + entry: nsf_data.NSFRecordEntry) -> Dict[str, Any]: + """Parse decrypted cache payload, falling back to decrypting stored record data.""" + payload = _parse_record_payload(entry.decrypted_data) + if payload.get('title') or not entry.record_key: + return payload + row = view.storage.records.get_entity(entry.record_uid) + if not row or not row.data: + return payload + decrypted = nsf_crypto.decrypt_record_data( + row.data, entry.record_key, version=entry.version or row.version) + return _parse_record_payload(decrypted) + + +def _decrypt_record_details_payload( + record_data: record_pb2.RecordData, + record_key: bytes) -> Optional[bytes]: + if not record_data.encryptedRecordData: + return None + try: + data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) + except Exception: + return None + try: + if record_data.version <= _MAX_V2_RECORD_VERSION: + return crypto.decrypt_aes_v1(data_decoded, record_key) + return crypto.decrypt_aes_v2(data_decoded, record_key) + except Exception: + return None + + +def _resolve_record_key_for_details( + vault: VaultOnline, + record_data: record_pb2.RecordData, + record_uid: str, + record_keys: Dict[str, bytes]) -> Optional[bytes]: + if record_uid in record_keys and record_keys[record_uid]: + return record_keys[record_uid] + if record_data.recordUid and record_data.recordKey: + owner_uid = utils.base64_url_encode(record_data.recordUid) + owner_key = record_keys.get(owner_uid) + if owner_key: + try: + return crypto.decrypt_aes_v2(record_data.recordKey, owner_key) + except Exception: + pass + try: + return sync_down.decrypt_keeper_key( + vault.keeper_auth.auth_context, + record_data.recordKey or b'', + record_data.recordKeyType, + ) + except Exception: + return None + + +def find_nsf_folders_for_record(vault: VaultOnline, record_uid: str) -> List[str]: + view = _nsf_view(vault) + folders: List[str] = [] + for folder in view.folders(): + if record_uid in folder.record_uids: + folders.append(folder.folder_uid) + if any( + link.folder_uid == ROOT_FOLDER_UID and link.record_uid == record_uid + for link in view.storage.folder_records.get_all_links() + ): + folders.append(ROOT_FOLDER_UID) + return folders + + +def get_nsf_root_record_uids(vault: VaultOnline) -> List[str]: + """Return record UIDs linked directly to the NSF root folder.""" + view = _nsf_view(vault) + return [ + link.record_uid + for link in view.storage.folder_records.get_all_links() + if link.folder_uid == ROOT_FOLDER_UID + ] + + +def _is_nsf_root_child_folder(folder: nsf_data.NSFFolderNode, known_folders: set[str]) -> bool: + raw_parent = folder.parent_uid or '' + normalized = _normalize_parent_uid(raw_parent) + return ( + normalized in ('', 'root') + or (bool(raw_parent) and raw_parent not in known_folders) + ) + + +def list_nsf_items( + vault: VaultOnline, + *, + include_folders: bool = True, + include_records: bool = True) -> List[NsfListRow]: + """List NSF folders and records (``nsf-list``).""" + if not include_folders and not include_records: + include_folders = include_records = True + + view = _nsf_view(vault) + rows: List[NsfListRow] = [] + + if include_folders: + for folder in view.folders(): + rows.append(NsfListRow( + item_type='Folder', + uid=folder.folder_uid, + title=folder.name or '(NSF Folder)', + )) + + if include_records: + folder_names = {f.folder_uid: f.name or f.folder_uid for f in view.folders()} + entries = list(view.records()) + title_by_uid: Dict[str, str] = {} + rows_by_uid: Dict[str, NsfListRow] = {} + missing_title_uids: List[str] = [] + + for entry in entries: + payload = _record_payload_from_entry(view, entry) + title = str(payload.get('title') or '') + if not title: + missing_title_uids.append(entry.record_uid) + rec_type = str(payload.get('type') or '') + description = '' + for fld in payload.get('fields') or []: + if isinstance(fld, dict) and fld.get('type') in ('note', 'multiline'): + values = fld.get('value') or [] + if isinstance(values, list) and values: + description = str(values[0]) + break + location = '' + for fuid in find_nsf_folders_for_record(vault, entry.record_uid): + location = 'root' if fuid == ROOT_FOLDER_UID else folder_names.get(fuid, fuid) + break + rows_by_uid[entry.record_uid] = NsfListRow( + item_type='Record', + uid=entry.record_uid, + title=title or entry.record_uid, + record_type=rec_type, + description=description, + ) + + type_by_uid: Dict[str, str] = {} + if missing_title_uids: + try: + details = get_nsf_record_details(vault, missing_title_uids) + for item in details.get('data') or []: + record_uid = str(item.get('record_uid', '')) + api_title = str(item.get('title') or '') + if record_uid and api_title and api_title != 'Unknown': + title_by_uid[record_uid] = api_title + api_type = str(item.get('type') or '') + if api_type and api_type != 'Unknown': + type_by_uid[record_uid] = api_type + except Exception: + pass + + for entry in entries: + row = rows_by_uid[entry.record_uid] + api_title = title_by_uid.get(entry.record_uid) + if api_title: + rows.append(NsfListRow( + item_type=row.item_type, + uid=row.uid, + title=api_title, + record_type=type_by_uid.get(entry.record_uid) or row.record_type, + description=row.description, + )) + else: + rows.append(row) + + rows.sort(key=lambda r: (r.item_type, r.title.casefold())) + return rows + + +def load_nsf_record_metadata(vault: VaultOnline, record_uid: str) -> Dict[str, Any]: + """Load title, fields, notes from cache; optional API fallback for title/type only.""" + view = _nsf_view(vault) + entry = view.get_record(record_uid) + if entry is None: + raise NsfError(f'NSF record not found: {record_uid}') + + payload = _record_payload_from_entry(view, entry) + folder_location = '' + for fuid in find_nsf_folders_for_record(vault, record_uid): + if fuid == ROOT_FOLDER_UID: + folder_location = 'root' + else: + folder = _nsf_view(vault).get_folder(fuid) + folder_location = folder.name if folder else fuid + break + + meta = { + 'title': str(payload.get('title') or record_uid), + 'type': str(payload.get('type') or ''), + 'fields': list(payload.get('fields') or []), + 'notes': str(payload.get('notes') or ''), + 'revision': entry.revision, + 'version': entry.version, + 'folder_location': folder_location, + } + + if meta['title'] == record_uid: + details = get_nsf_record_details(vault, [record_uid]) + if details.get('data'): + d = details['data'][0] + meta['title'] = d.get('title', record_uid) + meta['type'] = d.get('type', meta['type']) + meta['revision'] = d.get('revision', meta['revision']) + meta['version'] = d.get('version', meta['version']) + return meta + + +def get_nsf_folder_detail( + vault: VaultOnline, + folder_uid: str, + *, + include_access: bool = True) -> Dict[str, Any]: + """Folder detail payload for ``nsf-get`` (folder branch).""" + folder = _nsf_view(vault).get_folder(folder_uid) + if folder is None: + raise NsfError(f'NSF folder not found: {folder_uid}') + + row = _nsf_view(vault).storage.folders.get_entity(folder_uid) + result: Dict[str, Any] = { + 'nsf_folder_uid': folder_uid, + 'name': folder.name or folder_uid, + 'subfolder_uids': list(folder.subfolder_uids), + 'record_uids': list(folder.record_uids), + } + if row is not None: + result['owner_username'] = row.owner_username + result['owner_account_uid'] = row.owner_account_uid + + if include_access: + try: + access = get_nsf_folder_access(vault, [folder_uid]) + owner_username = result.get('owner_username') or '' + owner_account_uid = result.get('owner_account_uid') or '' + for fr in access.get('results') or []: + for accessor in fr.get('accessors') or []: + if nsf_common.is_nsf_folder_owner( + accessor, owner_username, owner_account_uid): + accessor['owner'] = True + result['access'] = access + except Exception: + result['access'] = {'results': []} + return result + + +def get_nsf_record_detail( + vault: VaultOnline, + record_uid: str, + *, + include_access: bool = True) -> Dict[str, Any]: + """Record detail payload for ``nsf-get`` (record branch).""" + meta = load_nsf_record_metadata(vault, record_uid) + entry = _nsf_view(vault).get_record(record_uid) + result: Dict[str, Any] = { + 'record_uid': record_uid, + 'title': meta['title'], + 'type': meta['type'], + 'revision': meta['revision'], + 'version': meta['version'], + 'shared': entry.shared if entry else False, + 'file_size': entry.file_size if entry else 0, + 'thumbnail_size': entry.thumbnail_size if entry else 0, + 'fields': meta['fields'], + 'notes': meta['notes'], + } + if meta['folder_location']: + result['folder'] = meta['folder_location'] + if include_access: + try: + result['record_accesses'] = get_nsf_record_accesses(vault, [record_uid]).get( + 'record_accesses', []) + except Exception: + result['record_accesses'] = [] + return result + + +def get_nsf_item( + vault: VaultOnline, + uid_or_title: str, + *, + include_access: bool = True) -> Dict[str, Any]: + """Resolve and return folder or record detail.""" + folder_uid = resolve_nsf_folder_uid(vault, uid_or_title) + if folder_uid: + return {'item_type': 'folder', **get_nsf_folder_detail( + vault, folder_uid, include_access=include_access)} + record_uid = resolve_nsf_record_uid(vault, uid_or_title) + if record_uid: + return {'item_type': 'record', **get_nsf_record_detail( + vault, record_uid, include_access=include_access)} + raise NsfError(f'Cannot find NSF folder or record: {uid_or_title}') + + +def _get_folder_key(vault: VaultOnline, folder_uid: str) -> bytes: + view = _nsf_view(vault) + folder = view.get_folder(folder_uid) + if folder is not None and folder.folder_key: + return folder.folder_key + + auth_context = vault.keeper_auth.auth_context + decrypted = nsf_crypto.decrypt_folder_keys(view.storage, auth_context) + key = decrypted.get(folder_uid) + if key is None: + label = folder.name if folder and folder.name and folder.name != '(NSF Folder)' else folder_uid + raise NsfError( + f'Folder key not available for {label}. ' + 'You may not have access to this folder, or run sync-down --force to refresh the NSF cache.') + if folder is not None: + folder.folder_key = key + row = view.storage.folders.get_entity(folder_uid) + if row and row.data and (not folder.name or folder.name == '(NSF Folder)'): + name = nsf_crypto.decrypt_folder_name(row.data, key) + if name: + folder.name = name + return key + + +def _get_record_key(vault: VaultOnline, record_uid: str) -> bytes: + entry = _nsf_view(vault).get_record(record_uid) + if entry is None or not entry.record_key: + raise NsfError( + f'Record key not available for {record_uid}. Run sync-down and rebuild NSF cache.') + return entry.record_key + + +def _build_record_data( + record_type: str, + title: str, + fields: Optional[Mapping[str, Any]] = None, + notes: Optional[str] = None, + record_data: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]: + if record_data is not None: + return dict(record_data) + data: Dict[str, Any] = {'type': record_type, 'title': title, 'fields': []} + if fields: + for ft, fv in fields.items(): + data['fields'].append({ + 'type': ft, + 'value': fv if isinstance(fv, list) else [fv], + }) + if notes is not None: + data['notes'] = notes + return data + + +def _build_record_add_message( + record_uid: str, + record_key: bytes, + data: Dict[str, Any], + auth_data_key: bytes, + folder_uid: Optional[str], + folder_key: Optional[bytes]) -> record_endpoints_pb2.RecordAdd: + ra = record_endpoints_pb2.RecordAdd() + ra.recordUid = utils.base64_url_decode(record_uid) + ra.clientModifiedTime = utils.current_milli_time() + json_bytes = vault_extensions.get_padded_json_bytes(data) + if folder_uid and folder_key: + ra.folderUid = utils.base64_url_decode(folder_uid) + ra.recordKey = crypto.encrypt_aes_v2(record_key, folder_key) + ra.recordKeyEncryptedBy = folder_pb2.ENCRYPTED_BY_PARENT_KEY + elif folder_uid: + raise NsfError(f'Folder key not available for folder: {folder_uid}') + else: + ra.recordKey = crypto.encrypt_aes_v2(record_key, auth_data_key) + ra.recordKeyType = folder_pb2.encrypted_by_data_key_gcm + ra.data = crypto.encrypt_aes_v2(json_bytes, record_key) + return ra + + +def _build_legacy_record_add_message( + record_uid: str, + record_key: bytes, + data: Dict[str, Any], + auth_data_key: bytes, + folder_uid: Optional[str], + folder_key: Optional[bytes]) -> record_pb2.RecordAdd: + ra = record_pb2.RecordAdd() + ra.record_uid = utils.base64_url_decode(record_uid) + ra.client_modified_time = utils.current_milli_time() + json_bytes = vault_extensions.get_padded_json_bytes(data) + if folder_uid and folder_key: + ra.folder_uid = utils.base64_url_decode(folder_uid) + ra.record_key = crypto.encrypt_aes_v2(record_key, folder_key) + else: + ra.record_key = crypto.encrypt_aes_v2(record_key, auth_data_key) + ra.data = crypto.encrypt_aes_v2(json_bytes, record_key) + return ra + + +def _parse_modify_response( + response: record_pb2.RecordsModifyResponse, + record_uid: str) -> NsfModifyResult: + if not response.records: + raise KeeperApiError('no_results', 'No results from record modify response') + for row in response.records: + if utils.base64_url_encode(row.record_uid) == record_uid: + status_name = record_pb2.RecordModifyResult.Name(row.status) + return NsfModifyResult( + record_uid=record_uid, + success=row.status == record_pb2.RS_SUCCESS, + status=status_name, + message=row.message, + revision=getattr(response, 'revision', 0), + ) + raise KeeperApiError('no_results', f'Record {record_uid} not present in modify response') + + +def create_nsf_record( + vault: VaultOnline, + *, + title: str, + record_type: str, + folder_uid: Optional[str] = None, + fields: Optional[Mapping[str, Any]] = None, + notes: Optional[str] = None, + record_data: Optional[Mapping[str, Any]] = None, + request_sync: bool = True) -> NsfModifyResult: + """Create an NSF record.""" + if folder_uid: + resolved = resolve_nsf_folder_uid(vault, folder_uid) or folder_uid + if not is_nsf_folder(vault, resolved): + raise NsfError(f'NSF folder not found: {folder_uid}') + folder_uid = resolved + + data = _build_record_data(record_type, title, fields, notes, record_data) + record_uid = utils.generate_uid() + record_key = os.urandom(32) + auth = vault.keeper_auth + folder_key = _get_folder_key(vault, folder_uid) if folder_uid else None + + ra = _build_record_add_message( + record_uid, record_key, data, auth.auth_context.data_key, folder_uid, folder_key) + rq = record_endpoints_pb2.RecordsAddRequest() + rq.clientTime = utils.current_milli_time() + rq.records.append(ra) + + response = auth.execute_auth_rest( + 'vault/records/v3/add', rq, response_type=record_pb2.RecordsModifyResponse) + if response is None: + legacy_ra = _build_legacy_record_add_message( + record_uid, record_key, data, auth.auth_context.data_key, folder_uid, folder_key) + legacy_rq = record_pb2.RecordsAddRequest() + legacy_rq.client_time = utils.current_milli_time() + legacy_rq.records.append(legacy_ra) + response = auth.execute_auth_rest( + 'vault/records_add', legacy_rq, response_type=record_pb2.RecordsModifyResponse) + assert response is not None + + result = _parse_modify_response(response, record_uid) + if not result.success: + raise KeeperApiError(result.status, result.message) + if request_sync: + vault.sync_requested = True + vault.run_pending_jobs() + return result + + +def update_nsf_record( + vault: VaultOnline, + record_uid: str, + *, + title: Optional[str] = None, + record_type: Optional[str] = None, + fields: Optional[Mapping[str, Any]] = None, + notes: Optional[str] = None, + record_data: Optional[Mapping[str, Any]] = None, + request_sync: bool = True) -> NsfModifyResult: + """Update an NSF record.""" + resolved = resolve_nsf_record_uid(vault, record_uid) or record_uid + if not is_nsf_record(vault, resolved): + raise NsfError(f'NSF record not found: {record_uid}') + record_uid = resolved + + record_key = _get_record_key(vault, record_uid) + storage_row = _nsf_view(vault).storage.records.get_entity(record_uid) + revision = storage_row.revision if storage_row else 0 + + if record_data is not None: + data = dict(record_data) + else: + entry = _nsf_view(vault).get_record(record_uid) + data = _parse_record_payload(entry.decrypted_data if entry else None) + if not data: + data = {'fields': []} + if title is not None: + data['title'] = title + if record_type is not None: + data['type'] = record_type + if fields is not None: + by_type: Dict[str, List[Any]] = {} + for existing in data.get('fields') or []: + if isinstance(existing, dict): + by_type.setdefault(existing.get('type', ''), []).append(existing) + for ft, fv in fields.items(): + val = fv if isinstance(fv, list) else [fv] + if ft in by_type and by_type[ft]: + by_type[ft][0]['value'] = val + else: + data.setdefault('fields', []).append({'type': ft, 'value': val}) + if notes is not None: + data['notes'] = notes + + ru = record_pb2.RecordUpdate() + ru.record_uid = utils.base64_url_decode(record_uid) + ru.client_modified_time = utils.current_milli_time() + ru.revision = revision + ru.data = crypto.encrypt_aes_v2(vault_extensions.get_padded_json_bytes(data), record_key) + + rq = record_pb2.RecordsUpdateRequest() + rq.client_time = utils.current_milli_time() + rq.records.append(ru) + + auth = vault.keeper_auth + response = auth.execute_auth_rest( + 'vault/records/v3/update', rq, response_type=record_pb2.RecordsModifyResponse) + if response is None: + response = auth.execute_auth_rest( + 'vault/records_update', rq, response_type=record_pb2.RecordsModifyResponse) + assert response is not None + + result = _parse_modify_response(response, record_uid) + if not result.success: + raise KeeperApiError(result.status, result.message) + if request_sync: + vault.sync_requested = True + vault.run_pending_jobs() + return result + + +def get_nsf_record_details( + vault: VaultOnline, + record_uids: Iterable[str]) -> Dict[str, Any]: + """``vault/get_records_details`` — title/type when cache lacks decrypted payload.""" + uids = [resolve_nsf_record_uid(vault, u) or u for u in record_uids] + uids = [u for u in uids if u] + if not uids: + raise NsfError('At least one record UID is required') + + view = _nsf_view(vault) + record_keys = { + entry.record_uid: entry.record_key + for entry in view.records() + if entry.record_key + } + + out_data: List[Dict[str, Any]] = [] + forbidden: List[str] = [] + for i in range(0, len(uids), _RECORD_DETAILS_CHUNK): + chunk = uids[i:i + _RECORD_DETAILS_CHUNK] + rq = record_pb2.GetRecordDataWithAccessInfoRequest() + rq.clientTime = utils.current_milli_time() + rq.recordDetailsInclude = record_pb2.DATA_PLUS_SHARE + for uid in chunk: + try: + rq.recordUid.append(utils.base64_url_decode(uid)) + except Exception: + pass + if not rq.recordUid: + continue + rs = vault.keeper_auth.execute_auth_rest( + _RECORD_DETAILS_URL, + rq, + response_type=record_pb2.GetRecordDataWithAccessInfoResponse) + if rs is None: + continue + for nop in rs.noPermissionRecordUid: + forbidden.append(utils.base64_url_encode(nop)) + for item in rs.recordDataWithAccessInfo: + record_uid = utils.base64_url_encode(item.recordUid) if item.recordUid else '' + record_data = item.recordData + if not record_uid or record_data is None: + continue + record_key = _resolve_record_key_for_details( + vault, record_data, record_uid, record_keys) + if not record_key: + continue + plain = _decrypt_record_details_payload(record_data, record_key) + if not plain: + continue + payload = _parse_record_payload(plain.decode('utf-8')) + out_data.append({ + 'record_uid': record_uid, + 'title': str(payload.get('title') or 'Unknown'), + 'type': str(payload.get('type') or 'Unknown'), + 'revision': int(record_data.revision or 0), + 'version': int(record_data.version or 0), + }) + return {'data': out_data, 'forbidden_records': forbidden} + + +def get_nsf_record_accesses( + vault: VaultOnline, + record_uids: Iterable[str]) -> Dict[str, Any]: + """``vault/records/v3/details/access``.""" + uids = [resolve_nsf_record_uid(vault, u) or u for u in record_uids] + uids = [u for u in uids if u] + if not uids: + raise NsfError('At least one record UID is required') + + rq = record_details_pb2.RecordAccessRequest() + for uid in uids: + rq.recordUids.append(utils.base64_url_decode(uid)) + rs = vault.keeper_auth.execute_auth_rest('vault/records/v3/details/access', rq, response_type=record_details_pb2.RecordAccessResponse) + if rs is None: + return {'record_accesses': [], 'forbidden_records': []} + + result = {'record_accesses': [], 'forbidden_records': []} + for ra in rs.recordAccesses: + d = ra.data + ai = ra.accessorInfo + ao = { + 'record_uid': utils.base64_url_encode(d.recordUid), + 'accessor_name': ai.name, + 'access_type': folder_pb2.AccessType.Name(d.accessType) if hasattr(d, 'accessType') else 'UNKNOWN', + 'access_type_uid': utils.base64_url_encode(d.accessTypeUid), + 'owner': getattr(d, 'owner', False), + 'inherited': bool(getattr(d, 'inherited', False)), + 'access_role_type': int(getattr(d, 'accessRoleType', 0) or 0), + } + for flag in ('can_view_title', 'can_edit', 'can_view', 'can_list_access', + 'can_update_access', 'can_delete', 'can_change_ownership', + 'can_request_access', 'can_approve_access'): + ao[flag] = getattr(d, flag, False) + result['record_accesses'].append(ao) + for fu in rs.forbiddenRecords: + result['forbidden_records'].append(utils.base64_url_encode(fu)) + return result + + +def _resolve_uid_to_username(vault: VaultOnline, uid_b64: str) -> Optional[str]: + try: + rq = record_pb2.GetShareObjectsRequest() + rs = vault.keeper_auth.execute_auth_rest('vault/get_share_objects', rq, response_type=record_pb2.GetShareObjectsResponse) + if rs is not None: + for user_list in (rs.shareRelationships, rs.shareFamilyUsers, + rs.shareEnterpriseUsers, rs.shareMCEnterpriseUsers): + for su in user_list: + if su.userAccountUid: + su_uid = utils.base64_url_encode(su.userAccountUid) + if su_uid == uid_b64: + return su.username + except Exception: + pass + + +def get_nsf_folder_access( + vault: VaultOnline, + folder_uids: Iterable[str]) -> Dict[str, Any]: + """``vault/folders/v3/access``.""" + uids: List[str] = [] + for raw in folder_uids: + resolved = resolve_nsf_folder_uid(vault, raw) or raw + if resolved: + uids.append(resolved) + if not uids: + raise NsfError('At least one folder UID is required') + + rq = folder_access_pb2.GetFolderAccessRequest() + for uid in uids: + rq.folderUid.append(utils.base64_url_decode(uid)) + rs = vault.keeper_auth.execute_auth_rest('vault/folders/v3/access', rq, response_type=folder_access_pb2.GetFolderAccessResponse) + results = [] + for fr in rs.folderAccessResults: + fuid = utils.base64_url_encode(fr.folderUid) + if fr.HasField('error'): + err = fr.error + results.append({ + 'folder_uid': fuid, + 'error': {'status': folder_pb2.FolderModifyStatus.Name(err.status), + 'message': err.message}, + 'success': False}) + else: + accessors = [] + for a in fr.accessors: + auid = utils.base64_url_encode(a.accessTypeUid) + at = folder_pb2.AccessType.Name(a.accessType) + rt = folder_pb2.AccessRoleType.Name(a.accessRoleType) + username = None + if at == 'AT_USER': + username = _resolve_uid_to_username(vault, auid) + ai = { + 'accessor_uid': auid, 'access_type': at, 'role': rt, + 'access_role_type': int(a.accessRoleType), + 'inherited': bool(a.inherited), 'hidden': bool(a.hidden), + 'username': username, + 'date_created': a.dateCreated or None, + 'last_modified': a.lastModified or None, + } + if at == 'AT_OWNER': + ai['owner'] = True + if a.HasField('permissions'): + p = a.permissions + ai['permissions'] = { + 'can_add': bool(p.canAdd), 'can_remove': bool(p.canRemove), + 'can_delete': bool(p.canDelete), + 'can_list_access': bool(p.canListAccess), + 'can_update_access': bool(p.canUpdateAccess), + 'can_change_ownership': bool(p.canChangeOwnership), + 'can_edit_records': bool(p.canEditRecords), + 'can_view_records': bool(p.canViewRecords), + 'can_approve_access': bool(p.canApproveAccess), + 'can_request_access': bool(p.canRequestAccess), + 'can_update_setting': bool(p.canUpdateSetting), + 'can_list_records': bool(p.canListRecords), + 'can_list_folders': bool(p.canListFolders), + } + accessors.append(ai) + results.append({'folder_uid': fuid, 'accessors': accessors, 'success': True}) + + rd = {'results': results, 'has_more': bool(rs.hasMore)} + if rs.HasField('continuationToken'): + rd['continuation_token'] = rs.continuationToken.lastModified + return rd + + +def _request_sync(vault: VaultOnline, request_sync: bool) -> None: + if request_sync: + vault.sync_requested = True + vault.run_pending_jobs() + + +def _api_parent_uid(parent_uid: Optional[str]) -> Optional[str]: + if not parent_uid or parent_uid == ROOT_FOLDER_UID: + return None + return parent_uid + + +def find_nsf_child_folder( + vault: VaultOnline, + folder_name: str, + parent_uid: Optional[str] = None) -> Optional[str]: + """Return child folder UID matching name under parent (case-insensitive). + + ``parent_uid=None`` (or the root sentinel) means a direct child of the NSF root. + """ + view = _nsf_view(vault) + known_folders = {f.folder_uid for f in view.folders()} + name_lower = folder_name.casefold() + looking_for_root = _api_parent_uid(parent_uid) is None + + for folder in view.folders(): + if (folder.name or '').casefold() != name_lower: + continue + raw_parent = folder.parent_uid or '' + if looking_for_root: + if _is_nsf_root_child_folder(folder, known_folders): + return folder.folder_uid + elif raw_parent == parent_uid: + return folder.folder_uid + return None + + +def _decrypt_folder_payload(encrypted_data_b64: str, folder_key: bytes) -> Dict[str, Any]: + if not encrypted_data_b64: + return {} + try: + data_bytes = crypto.decrypt_aes_v2(utils.base64_url_decode(encrypted_data_b64), folder_key) + payload = json.loads(data_bytes.decode('utf-8')) + return payload if isinstance(payload, dict) else {} + except Exception: + return {} + + +def _encrypt_folder_key(folder_key: bytes, parent_key: bytes) -> bytes: + return crypto.encrypt_aes_v2(folder_key, parent_key) + + +def _create_folder_data_message( + folder_uid: str, + folder_name: str, + encryption_key: bytes, + *, + parent_uid: Optional[str] = None, + inherit_permissions: bool = True, + color: Optional[str] = None, + owner_username: Optional[str] = None, + owner_account_uid: Optional[bytes] = None) -> folder_pb2.FolderData: + fd = folder_pb2.FolderData() + fd.folderUid = utils.base64_url_decode(folder_uid) + data_dict: Dict[str, Any] = {'name': folder_name} + if color and color != 'none': + data_dict['color'] = color + fd.data = crypto.encrypt_aes_v2(json.dumps(data_dict).encode('utf-8'), encryption_key) + api_parent = _api_parent_uid(parent_uid) + if api_parent: + fd.parentUid = utils.base64_url_decode(api_parent) + fd.type = folder_pb2.UT_NORMAL + fd.inheritUserPermissions = ( + folder_pb2.BOOLEAN_TRUE if inherit_permissions else folder_pb2.BOOLEAN_FALSE) + if owner_username or owner_account_uid: + oi = folder_pb2.UserInfo() + if owner_username: + oi.username = owner_username + if owner_account_uid: + oi.accountUid = owner_account_uid + fd.ownerInfo.CopyFrom(oi) + return fd + + +def _prepare_folder_for_creation( + vault: VaultOnline, + folder_uid: str, + folder_name: str, + parent_uid: Optional[str], + color: Optional[str], + inherit_permissions: bool) -> folder_pb2.FolderData: + folder_key = os.urandom(32) + auth = vault.keeper_auth.auth_context + enc_key = auth.data_key + api_parent = _api_parent_uid(parent_uid) + if api_parent: + try: + parent_key = _get_folder_key(vault, api_parent) + enc_key = parent_key + except NsfError: + pass + encrypted_fk = _encrypt_folder_key(folder_key, enc_key) + username = getattr(vault.keeper_auth, 'login', None) or '' + fd = _create_folder_data_message( + folder_uid, folder_name, folder_key, + parent_uid=parent_uid, + inherit_permissions=inherit_permissions, + color=color, + owner_username=username or None, + owner_account_uid=auth.account_uid or None, + ) + fd.folderKey = encrypted_fk + return fd + + +def _parse_folder_modify_result( + response: Any, + folder_uid: str, + results_attr: str) -> NsfFolderModifyResult: + results = getattr(response, results_attr, None) or [] + if not results: + raise KeeperApiError('no_results', 'No results from folder modify response') + row = results[0] + status_name = folder_pb2.FolderModifyStatus.Name(row.status) + return NsfFolderModifyResult( + folder_uid=folder_uid, + success=row.status == folder_pb2.SUCCESS, + status=status_name, + message=row.message, + ) + + +def create_nsf_folder( + vault: VaultOnline, + folder_name: str, + *, + parent_uid: Optional[str] = None, + color: Optional[str] = None, + inherit_permissions: bool = True, + request_sync: bool = True) -> NsfFolderModifyResult: + """Create an NSF folder.""" + if parent_uid: + resolved = resolve_nsf_folder_uid(vault, parent_uid) or parent_uid + if resolved and resolved != ROOT_FOLDER_UID and not is_nsf_folder(vault, resolved): + raise NsfError(f'NSF parent folder not found: {parent_uid}') + parent_uid = resolved + + folder_uid = utils.generate_uid() + fd = _prepare_folder_for_creation( + vault, folder_uid, folder_name, parent_uid, color, inherit_permissions) + rq = folder_pb2.FolderAddRequest() + rq.folderData.append(fd) + response = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/add', rq, response_type=folder_pb2.FolderAddResponse) + assert response is not None + result = _parse_folder_modify_result(response, folder_uid, 'folderAddResults') + if not result.success: + raise KeeperApiError(result.status, result.message) + _request_sync(vault, request_sync) + return result + + +def update_nsf_folder( + vault: VaultOnline, + folder_identifier: str, + *, + folder_name: Optional[str] = None, + color: Optional[str] = None, + inherit_permissions: Optional[bool] = None, + request_sync: bool = True) -> NsfFolderModifyResult: + """Rename or recolor an NSF folder.""" + if folder_name is None and color is None and inherit_permissions is None: + raise NsfError('At least one of folder_name, color, or inherit_permissions is required') + + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if not is_nsf_folder(vault, folder_uid): + raise NsfError(f'NSF folder not found: {folder_identifier}') + + folder_key = _get_folder_key(vault, folder_uid) + row = _nsf_view(vault).storage.folders.get_entity(folder_uid) + payload = _decrypt_folder_payload(row.data if row else '', folder_key) + node = _nsf_view(vault).get_folder(folder_uid) + + dd: Dict[str, Any] = {} + dd['name'] = folder_name if folder_name is not None else (node.name if node else payload.get('name', '')) + if color is not None: + if color not in ('none', ''): + dd['color'] = color + elif payload.get('color') and payload.get('color') != 'none': + dd['color'] = payload['color'] + + fd = folder_pb2.FolderData() + fd.folderUid = utils.base64_url_decode(folder_uid) + fd.data = crypto.encrypt_aes_v2(json.dumps(dd).encode('utf-8'), folder_key) + if inherit_permissions is not None: + fd.inheritUserPermissions = ( + folder_pb2.BOOLEAN_TRUE if inherit_permissions else folder_pb2.BOOLEAN_FALSE) + + rq = folder_pb2.FolderUpdateRequest() + rq.folderData.append(fd) + response = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/update', rq, response_type=folder_pb2.FolderUpdateResponse) + assert response is not None + result = _parse_folder_modify_result(response, folder_uid, 'folderUpdateResults') + if not result.success: + raise KeeperApiError(result.status, result.message) + _request_sync(vault, request_sync) + return result + + +def _parse_remove_impact(impact_msg: Any) -> Optional[Dict[str, Any]]: + if impact_msg is None or not impact_msg: + return None + return { + 'folders_count': getattr(impact_msg, 'folders_count', 0), + 'records_count': getattr(impact_msg, 'records_count', 0), + 'affected_users_count': getattr(impact_msg, 'affected_users_count', 0), + 'affected_teams_count': getattr(impact_msg, 'affected_teams_count', 0), + 'record_info': [ + { + 'record_uid': utils.base64_url_encode(ri.record_uid), + 'locations_count': ri.locations_count, + } + for ri in getattr(impact_msg, 'record_info', []) + ], + 'warnings': list(getattr(impact_msg, 'warnings', [])), + } + + +def _parse_remove_error(error_msg: Any) -> Optional[Dict[str, str]]: + if error_msg is None or not error_msg: + return None + return { + 'code': remove_pb2.RemoveErrorCode.Name(error_msg.code), + 'message': error_msg.message, + } + + +def _parse_record_remove_preview(response: remove_pb2.RemoveResponse) -> List[NsfRemovePreviewItem]: + items: List[NsfRemovePreviewItem] = [] + for res in response.results: + item_uid = utils.base64_url_encode(res.item_uid) if res.item_uid else '' + folder_uid = utils.base64_url_encode(res.folder_uid) if res.folder_uid else '' + items.append(NsfRemovePreviewItem( + item_uid=item_uid, + folder_uid=folder_uid, + status=remove_pb2.RemoveStatus.Name(res.status), + impact=_parse_remove_impact(res.impact if res.HasField('impact') else None), + error=_parse_remove_error(res.error if res.HasField('error') else None), + )) + return items + + +def remove_nsf_records( + vault: VaultOnline, + removals: List[Dict[str, str]], + *, + dry_run: bool = False, + request_sync: bool = True) -> NsfRemoveResult: + """Remove NSF records — preview or confirm.""" + if not removals: + raise NsfError('At least one record removal is required') + if len(removals) > 500: + raise NsfError('Maximum 500 records per request') + + preview_rq = remove_pb2.RemoveRecordRequest() + preview_rq.action = remove_pb2.REMOVE_ACTION_PREVIEW + for item in removals: + op = item.get('operation_type', 'owner-trash') + if op not in _RECORD_REMOVE_OPS: + raise NsfError( + f"Invalid operation_type '{op}'. Use: {', '.join(_RECORD_REMOVE_OPS)}") + record_uid = resolve_nsf_record_uid(vault, item['record_uid']) or item['record_uid'] + rr = remove_pb2.RecordRemoval() + rr.record_uid = utils.base64_url_decode(record_uid) + fuid = item.get('folder_uid') + if fuid: + resolved_folder = resolve_nsf_folder_uid(vault, fuid) or fuid + rr.folder_uid = utils.base64_url_decode(resolved_folder) + rr.operation_type = _RECORD_REMOVE_OPS[op] + preview_rq.records.append(rr) + + preview_rs = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/remove_record', + preview_rq, + response_type=remove_pb2.RemoveResponse) + assert preview_rs is not None + + preview_items = _parse_record_remove_preview(preview_rs) + token_expires = preview_rs.token_expires_at or None + + if dry_run or not preview_rs.confirmation_token: + return NsfRemoveResult( + preview_results=preview_items, + confirmed=False, + confirmation_token_expires_at=token_expires, + ) + + confirm_rq = remove_pb2.RemoveRecordRequest() + confirm_rq.action = remove_pb2.REMOVE_ACTION_CONFIRM + confirm_rq.confirmation_token = preview_rs.confirmation_token + confirm_rq.records.extend(preview_rq.records) + vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/remove_record', + confirm_rq, + response_type=remove_pb2.RemoveResponse) + _request_sync(vault, request_sync) + return NsfRemoveResult( + preview_results=preview_items, + confirmed=True, + confirmation_token_expires_at=token_expires, + ) + + +def remove_nsf_folders( + vault: VaultOnline, + removals: List[Dict[str, str]], + *, + dry_run: bool = False, + request_sync: bool = True) -> NsfRemoveResult: + """Remove NSF folders — preview or confirm.""" + if not removals: + raise NsfError('At least one folder removal is required') + if len(removals) > 100: + raise NsfError('Maximum 100 folders per request') + + preview_rq = remove_pb2.RemoveFolderRequest() + preview_rq.action = remove_pb2.REMOVE_ACTION_PREVIEW + for item in removals: + op = item.get('operation_type', 'folder-trash') + if op not in _FOLDER_REMOVE_OPS: + raise NsfError( + f"Invalid operation_type '{op}'. Use: {', '.join(_FOLDER_REMOVE_OPS)}") + folder_uid = resolve_nsf_folder_uid(vault, item['folder_uid']) or item['folder_uid'] + fr = remove_pb2.FolderRemoval() + fr.folder_uid = utils.base64_url_decode(folder_uid) + fr.operation_type = _FOLDER_REMOVE_OPS[op] + preview_rq.folders.append(fr) + + preview_rs = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/remove_folder', + preview_rq, + response_type=remove_pb2.RemoveResponse) + assert preview_rs is not None + + preview_items: List[NsfRemovePreviewItem] = [] + for res in preview_rs.results: + preview_items.append(NsfRemovePreviewItem( + item_uid=utils.base64_url_encode(res.item_uid), + status=remove_pb2.RemoveStatus.Name(res.status), + impact=_parse_remove_impact(res.impact if res.HasField('impact') else None), + error=_parse_remove_error(res.error if res.HasField('error') else None), + )) + + token_expires = preview_rs.token_expires_at or None + if dry_run or not preview_rs.confirmation_token: + return NsfRemoveResult( + preview_results=preview_items, + confirmed=False, + confirmation_token_expires_at=token_expires, + ) + + confirm_rq = remove_pb2.RemoveFolderRequest() + confirm_rq.action = remove_pb2.REMOVE_ACTION_CONFIRM + confirm_rq.confirmation_token = preview_rs.confirmation_token + confirm_rq.folders.extend(preview_rq.folders) + vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/remove_folder', + confirm_rq, + response_type=remove_pb2.RemoveResponse) + _request_sync(vault, request_sync) + return NsfRemoveResult( + preview_results=preview_items, + confirmed=True, + confirmation_token_expires_at=token_expires, + ) + + +def build_nsf_record_removals( + vault: VaultOnline, + record_identifiers: Iterable[str], + *, + operation_type: str = 'owner-trash', + folder_uid: Optional[str] = None) -> List[Dict[str, str]]: + """Resolve record UIDs and folder context for remove_nsf_records.""" + resolved_folder: Optional[str] = None + if folder_uid: + resolved_folder = resolve_nsf_folder_uid(vault, folder_uid) or folder_uid + if not is_nsf_folder(vault, resolved_folder): + raise NsfError(f'NSF folder not found: {folder_uid}') + + removals: List[Dict[str, str]] = [] + for identifier in record_identifiers: + record_uid = resolve_nsf_record_uid(vault, identifier) + if not record_uid: + raise NsfError(f"NSF record not found: {identifier}") + ctx_folder = resolved_folder + if not ctx_folder: + folders = find_nsf_folders_for_record(vault, record_uid) + if not folders and operation_type != 'owner-trash': + raise NsfError( + f"No folder context for record '{identifier}'. " + f"Use folder_uid or operation owner-trash.") + ctx_folder = folders[0] if folders else None + entry: Dict[str, str] = { + 'record_uid': record_uid, + 'operation_type': operation_type, + } + if ctx_folder: + entry['folder_uid'] = ctx_folder + removals.append(entry) + return removals diff --git a/keepersdk-package/src/keepersdk/vault/nsf_sharing.py b/keepersdk-package/src/keepersdk/vault/nsf_sharing.py new file mode 100644 index 00000000..e35be897 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_sharing.py @@ -0,0 +1,555 @@ +"""NSF sharing, bulk record permissions, and ownership transfer.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Set, Tuple + +from .. import utils +from ..errors import KeeperApiError +from ..proto import folder_pb2, record_pb2, record_sharing_pb2 +from . import nsf_common +from .nsf_management import ( + NsfError, + ROOT_FOLDER_UID, + _get_folder_key, + _get_record_key, + _nsf_view, + _request_sync, + get_nsf_record_accesses, + is_nsf_folder, + resolve_nsf_folder_uid, + resolve_nsf_record_uid, +) +from .vault_online import VaultOnline + +_SHARE_BATCH_SIZE = 200 + + +@dataclass +class NsfShareResult: + success: bool + results: List[Dict[str, Any]] = field(default_factory=list) + message: str = '' + + +@dataclass +class NsfRecordPermissionPlan: + updates: List[Dict[str, Any]] = field(default_factory=list) + creates: List[Dict[str, Any]] = field(default_factory=list) + revokes: List[Dict[str, Any]] = field(default_factory=list) + skipped: List[Dict[str, Any]] = field(default_factory=list) + + +def _folder_access_update( + vault: VaultOnline, + *, + adds: Optional[List[folder_pb2.FolderAccessData]] = None, + updates: Optional[List[folder_pb2.FolderAccessData]] = None, + removes: Optional[List[folder_pb2.FolderAccessData]] = None) -> folder_pb2.FolderAccessResponse: + rq = folder_pb2.FolderAccessRequest() + if adds: + rq.folderAccessAdds.extend(adds) + if updates: + rq.folderAccessUpdates.extend(updates) + if removes: + rq.folderAccessRemoves.extend(removes) + response = vault.keeper_auth.execute_auth_rest( + 'vault/folders/v3/access_update', + rq, + response_type=folder_pb2.FolderAccessResponse) + assert response is not None + return response + + +def collect_nsf_records_in_folder( + vault: VaultOnline, + folder_identifier: Optional[str], + *, + recursive: bool = False) -> Set[str]: + view = _nsf_view(vault) + record_uids: Set[str] = set() + known = {f.folder_uid for f in view.folders()} + + def walk(folder_uid: str, visited: Set[str]) -> None: + if folder_uid in visited: + return + visited.add(folder_uid) + folder = view.get_folder(folder_uid) + if folder is None: + return + record_uids.update(folder.record_uids) + if recursive: + for child_uid in folder.subfolder_uids: + if child_uid in known: + walk(child_uid, visited) + + if folder_identifier: + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if folder_uid == ROOT_FOLDER_UID or is_nsf_folder(vault, folder_uid): + walk(folder_uid, set()) + else: + for folder in view.folders(): + walk(folder.folder_uid, set()) + return record_uids + + +def grant_nsf_folder_access( + vault: VaultOnline, + folder_identifier: str, + recipient: str, + *, + role: str = 'viewer', + expiration_timestamp: Optional[int] = None, + as_team: bool = False, + request_sync: bool = True) -> Dict[str, Any]: + """Grant user or team access to an NSF folder.""" + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if not is_nsf_folder(vault, folder_uid): + raise NsfError(f'NSF folder not found: {folder_identifier}') + + access_role = nsf_common.resolve_nsf_role(role) + ad = folder_pb2.FolderAccessData() + ad.folderUid = utils.base64_url_decode(folder_uid) + ad.accessRoleType = access_role + ad.permissions.CopyFrom(nsf_common.get_folder_permissions_for_role(access_role)) + + if as_team: + resolved = nsf_common.resolve_team_identifier(vault, recipient) + if not resolved: + raise NsfError(f"Team '{recipient}' not found") + _, uid_bytes = resolved + ad.accessTypeUid = uid_bytes + ad.accessType = folder_pb2.AT_TEAM + fk = _get_folder_key(vault, folder_uid) + vault.keeper_auth.load_team_keys([utils.base64_url_encode(uid_bytes)]) + team_keys = vault.keeper_auth.get_team_keys(utils.base64_url_encode(uid_bytes)) + if not team_keys: + raise NsfError(f'Team keys not available for {recipient}') + efk, key_type = nsf_common.encrypt_for_team(fk, team_keys, forbid_rsa=True) + ek = folder_pb2.EncryptedDataKey() + ek.encryptedKey = efk + ek.encryptedKeyType = key_type + ad.folderKey.CopyFrom(ek) + label = recipient + else: + pub_key, use_ecc, uid_bytes, _ = nsf_common.get_user_public_key(vault, recipient) + ad.accessTypeUid = uid_bytes + ad.accessType = folder_pb2.AT_USER + fk = _get_folder_key(vault, folder_uid) + ek = folder_pb2.EncryptedDataKey() + ek.encryptedKey = nsf_common.encrypt_for_recipient(fk, pub_key, use_ecc) + ek.encryptedKeyType = ( + folder_pb2.encrypted_by_public_key_ecc if use_ecc + else folder_pb2.encrypted_by_public_key) + ad.folderKey.CopyFrom(ek) + label = recipient + + if expiration_timestamp is not None: + ad.tlaProperties.expiration = expiration_timestamp + + response = _folder_access_update(vault, adds=[ad]) + result = nsf_common.parse_folder_access_result( + response, folder_uid, label, 'Access granted successfully') + if not result['success']: + raise KeeperApiError(result['status'], result['message']) + _request_sync(vault, request_sync) + return result + + +def revoke_nsf_folder_access( + vault: VaultOnline, + folder_identifier: str, + recipient: str, + *, + as_team: bool = False, + request_sync: bool = True) -> Dict[str, Any]: + """Revoke user or team access from an NSF folder.""" + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if not is_nsf_folder(vault, folder_uid): + raise NsfError(f'NSF folder not found: {folder_identifier}') + + ad = folder_pb2.FolderAccessData() + ad.folderUid = utils.base64_url_decode(folder_uid) + if as_team: + resolved = nsf_common.resolve_team_identifier(vault, recipient) + if not resolved: + raise NsfError(f"Team '{recipient}' not found") + _, uid_bytes = resolved + ad.accessTypeUid = uid_bytes + ad.accessType = folder_pb2.AT_TEAM + else: + uid_bytes = nsf_common.resolve_user_uid_bytes(vault, recipient) + if not uid_bytes: + raise NsfError(f"User '{recipient}' not found") + ad.accessTypeUid = uid_bytes + ad.accessType = folder_pb2.AT_USER + + response = _folder_access_update(vault, removes=[ad]) + result = nsf_common.parse_folder_access_result( + response, folder_uid, recipient, 'Access revoked successfully') + if not result['success']: + raise KeeperApiError(result['status'], result['message']) + _request_sync(vault, request_sync) + return result + + +def _build_share_permission( + vault: VaultOnline, + record_uid: str, + recipient_email: str, + access_role_type: Optional[int], + expiration_timestamp: Optional[int], + *, + include_role: bool) -> record_sharing_pb2.Permissions: + record_key = _get_record_key(vault, record_uid) + pub_key, use_ecc, uid_bytes, _ = nsf_common.get_user_public_key(vault, recipient_email) + enc_rk = nsf_common.encrypt_for_recipient(record_key, pub_key, use_ecc) + uid_b = utils.base64_url_decode(record_uid) + perm = record_sharing_pb2.Permissions() + perm.recipientUid = uid_bytes + perm.recordUid = uid_b + perm.recordKey = enc_rk + perm.useEccKey = use_ecc + perm.rules.accessTypeUid = uid_bytes + perm.rules.accessType = folder_pb2.AT_USER + perm.rules.recordUid = uid_b + perm.rules.owner = False + if include_role and access_role_type is not None: + perm.rules.accessRoleType = access_role_type + if expiration_timestamp: + perm.rules.tlaProperties.expiration = expiration_timestamp + return perm + + +def _share_rest( + vault: VaultOnline, + rq: record_sharing_pb2.Request, + status_attr: str) -> NsfShareResult: + rs = vault.keeper_auth.execute_auth_rest( + 'vault/records/v3/share', rq, response_type=record_sharing_pb2.Response) + assert rs is not None + statuses = getattr(rs, status_attr, []) + results = [nsf_common.parse_sharing_status(s) for s in statuses] + return NsfShareResult( + success=all(r['success'] for r in results) if results else True, + results=results, + ) + + +def share_nsf_record( + vault: VaultOnline, + record_uid: str, + recipient_email: str, + *, + role: str, + expiration_timestamp: Optional[int] = None, + request_sync: bool = True) -> NsfShareResult: + """Grant record share.""" + resolved = resolve_nsf_record_uid(vault, record_uid) or record_uid + role_type = nsf_common.resolve_nsf_role(role) + perm = _build_share_permission( + vault, resolved, recipient_email, role_type, expiration_timestamp, include_role=True) + rq = record_sharing_pb2.Request() + rq.createSharingPermissions.append(perm) + result = _share_rest(vault, rq, 'createdSharingStatus') + if not result.success: + msg = result.results[0]['message'] if result.results else 'Share failed' + raise KeeperApiError('share_failed', msg) + _request_sync(vault, request_sync) + return result + + +def update_nsf_record_share( + vault: VaultOnline, + record_uid: str, + recipient_email: str, + *, + role: str, + expiration_timestamp: Optional[int] = None, + request_sync: bool = True) -> NsfShareResult: + resolved = resolve_nsf_record_uid(vault, record_uid) or record_uid + role_type = nsf_common.resolve_nsf_role(role) + perm = _build_share_permission( + vault, resolved, recipient_email, role_type, expiration_timestamp, include_role=True) + rq = record_sharing_pb2.Request() + rq.updateSharingPermissions.append(perm) + result = _share_rest(vault, rq, 'updatedSharingStatus') + if not result.success: + msg = result.results[0]['message'] if result.results else 'Update failed' + raise KeeperApiError('share_update_failed', msg) + _request_sync(vault, request_sync) + return result + + +def unshare_nsf_record( + vault: VaultOnline, + record_uid: str, + recipient_email: str, + *, + request_sync: bool = True) -> NsfShareResult: + resolved = resolve_nsf_record_uid(vault, record_uid) or record_uid + uid_bytes = nsf_common.resolve_user_uid_bytes(vault, recipient_email) + if not uid_bytes: + raise NsfError(f"User '{recipient_email}' not found") + uid_b = utils.base64_url_decode(resolved) + perm = record_sharing_pb2.Permissions() + perm.recipientUid = uid_bytes + perm.recordUid = uid_b + perm.rules.accessTypeUid = uid_bytes + perm.rules.accessType = folder_pb2.AT_USER + perm.rules.recordUid = uid_b + rq = record_sharing_pb2.Request() + rq.revokeSharingPermissions.append(perm) + result = _share_rest(vault, rq, 'revokedSharingStatus') + if not result.success: + msg = result.results[0]['message'] if result.results else 'Revoke failed' + raise KeeperApiError('share_revoke_failed', msg) + _request_sync(vault, request_sync) + return result + + +def transfer_nsf_record_ownership( + vault: VaultOnline, + record_identifier: str, + new_owner_email: str, + *, + request_sync: bool = True) -> NsfShareResult: + """Transfer NSF record ownership.""" + record_uid = resolve_nsf_record_uid(vault, record_identifier) + if not record_uid: + raise NsfError(f'NSF record not found: {record_identifier}') + record_key = _get_record_key(vault, record_uid) + pub_key, use_ecc, _, _ = nsf_common.get_user_public_key( + vault, new_owner_email, require_uid=False) + enc_rk = nsf_common.encrypt_for_recipient(record_key, pub_key, use_ecc) + tr = record_pb2.TransferRecord() + tr.username = new_owner_email + tr.recordUid = utils.base64_url_decode(record_uid) + tr.recordKey = enc_rk + tr.useEccKey = use_ecc + rq = record_pb2.RecordsOnwershipTransferRequest() + rq.transferRecords.append(tr) + rs = vault.keeper_auth.execute_auth_rest( + 'vault/records/v3/transfer', + rq, + response_type=record_pb2.RecordsOnwershipTransferResponse) + assert rs is not None + results = [{ + 'record_uid': utils.base64_url_encode(s.recordUid), + 'username': s.username, + 'status': s.status, + 'message': s.message, + 'success': 'success' in s.status.lower(), + } for s in rs.transferRecordStatus] + result = NsfShareResult( + success=all(r['success'] for r in results) if results else False, + results=results, + ) + if not result.success: + msg = results[0]['message'] if results else 'Transfer failed' + raise KeeperApiError('transfer_failed', msg) + _request_sync(vault, request_sync) + return result + + +def resolve_nsf_share_record_uids( + vault: VaultOnline, + record_arg: str, + *, + recursive: bool = False) -> List[str]: + resolved = resolve_nsf_record_uid(vault, record_arg) + if resolved: + return [resolved] + folder_uid = resolve_nsf_folder_uid(vault, record_arg) + if folder_uid: + uids = collect_nsf_records_in_folder(vault, folder_uid, recursive=recursive) + if not uids: + raise NsfError('No records found in the specified folder') + return sorted(uids) + raise NsfError(f"Record or folder '{record_arg}' not found") + + +def share_nsf_record_with_action( + vault: VaultOnline, + record_uid: str, + recipient_email: str, + *, + action: str = 'grant', + role: Optional[str] = None, + expiration_timestamp: Optional[int] = None, + request_sync: bool = True) -> Tuple[NsfShareResult, str]: + """Dispatch grant/update/revoke/owner for a single record share. + + Returns (result, action) tuple where: + - result: NsfShareResult with success status and results + - action: 'grant', 'update', 'revoke', or 'owner' + """ + resolved = resolve_nsf_record_uid(vault, record_uid) or record_uid + if action == 'owner': + return transfer_nsf_record_ownership( + vault, resolved, recipient_email, request_sync=request_sync), 'owner' + if action == 'revoke': + return unshare_nsf_record( + vault, resolved, recipient_email, request_sync=request_sync), 'revoke' + if not role: + raise NsfError('Role is required for grant action') + accesses = get_nsf_record_accesses(vault, [resolved]).get('record_accesses', []) + already_shared = any( + a.get('record_uid') == resolved + and not a.get('owner') + and a.get('access_type', '') in ('AT_USER', '') + and not a.get('inherited') + and (a.get('accessor_name') or '').casefold() == recipient_email.casefold() + for a in accesses) + if already_shared: + return update_nsf_record_share( + vault, resolved, recipient_email, role=role, + expiration_timestamp=expiration_timestamp, request_sync=request_sync), 'update' + return share_nsf_record( + vault, resolved, recipient_email, role=role, + expiration_timestamp=expiration_timestamp, request_sync=request_sync), 'grant' + + +def plan_nsf_record_permissions( + vault: VaultOnline, + folder_identifier: Optional[str], + *, + action: str, + role: Optional[str], + recursive: bool, + current_user: str) -> NsfRecordPermissionPlan: + """Compute bulk permission changes for nsf-record-permission.""" + folder_uid = None + if folder_identifier: + folder_uid = resolve_nsf_folder_uid(vault, folder_identifier) or folder_identifier + if not is_nsf_folder(vault, folder_uid): + raise NsfError(f'NSF folder not found: {folder_identifier}') + + record_uids = collect_nsf_records_in_folder(vault, folder_uid, recursive=recursive) + if not record_uids: + raise NsfError('No records found in the specified folder') + + accesses_result = get_nsf_record_accesses(vault, list(record_uids)) + role_map = {name: nsf_common.resolve_nsf_role(name) for name in ( + 'viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager')} + + plan = NsfRecordPermissionPlan() + forbidden = set(accesses_result.get('forbidden_records', [])) + owner_flags = { + a.get('record_uid'): a.get('can_update_access', False) + for a in accesses_result.get('record_accesses', []) + if a.get('accessor_name', '') == current_user + } + + for rec_uid in record_uids: + if rec_uid in forbidden: + plan.skipped.append({ + 'record_uid': rec_uid, 'email': '', 'cur_role': '', + 'reason': 'No access — record is forbidden', + }) + + for access in accesses_result.get('record_accesses', []): + rec_uid = access.get('record_uid') + if not rec_uid or rec_uid not in record_uids or access.get('owner'): + continue + email = access.get('accessor_name', '') + if not email or email == current_user: + continue + cur_role = nsf_common.access_role_label(access) + if not owner_flags.get(rec_uid, False): + plan.skipped.append({ + 'record_uid': rec_uid, 'email': email, 'cur_role': cur_role, + 'reason': 'Insufficient permission (can_update_access is false)', + }) + continue + if action == 'grant': + if role and cur_role != role: + entry = { + 'record_uid': rec_uid, 'email': email, + 'cur_role': cur_role, 'new_role': role, + 'access_role_type': role_map.get(role), + } + if access.get('inherited'): + plan.creates.append(entry) + else: + plan.updates.append(entry) + elif not role or cur_role == role: + if access.get('inherited'): + plan.skipped.append({ + 'record_uid': rec_uid, 'email': email, 'cur_role': cur_role, + 'reason': 'Inherited — revoke at parent folder', + }) + else: + plan.revokes.append({'record_uid': rec_uid, 'email': email, 'cur_role': cur_role}) + return plan + + +def _batch_share( + vault: VaultOnline, + items: List[Dict[str, Any]], + *, + mode: str) -> List[Tuple[Dict[str, Any], Dict[str, Any]]]: + outcomes: List[Tuple[Dict[str, Any], Dict[str, Any]]] = [] + for i in range(0, len(items), _SHARE_BATCH_SIZE): + chunk = items[i:i + _SHARE_BATCH_SIZE] + rq = record_sharing_pb2.Request() + built: List[Dict[str, Any]] = [] + for item in chunk: + try: + if mode == 'revoke': + uid_bytes = nsf_common.resolve_user_uid_bytes(vault, item['email']) + if not uid_bytes: + raise ValueError(f"User {item['email']} not found") + uid_b = utils.base64_url_decode(item['record_uid']) + perm = record_sharing_pb2.Permissions() + perm.recipientUid = uid_bytes + perm.recordUid = uid_b + perm.rules.accessTypeUid = uid_bytes + perm.rules.accessType = folder_pb2.AT_USER + perm.rules.recordUid = uid_b + rq.revokeSharingPermissions.append(perm) + else: + perm = _build_share_permission( + vault, item['record_uid'], item['email'], + item['access_role_type'], None, + include_role=True) + if mode == 'create': + rq.createSharingPermissions.append(perm) + else: + rq.updateSharingPermissions.append(perm) + built.append(item) + except Exception as exc: + outcomes.append((item, {'success': False, 'skipped': True, 'message': str(exc)})) + if not built: + continue + status_attr = { + 'create': 'createdSharingStatus', + 'update': 'updatedSharingStatus', + 'revoke': 'revokedSharingStatus', + }[mode] + try: + result = _share_rest(vault, rq, status_attr) + by_uid = {r['record_uid']: r for r in result.results} + for item in built: + outcomes.append((item, by_uid.get( + item['record_uid'], {'success': False, 'message': 'No status returned'}))) + except Exception as exc: + for item in built: + outcomes.append((item, {'success': False, 'message': str(exc)})) + return outcomes + + +def apply_nsf_record_permissions( + vault: VaultOnline, + plan: NsfRecordPermissionPlan, + *, + request_sync: bool = True) -> Dict[str, List[Tuple[Dict[str, Any], Dict[str, Any]]]]: + """Apply a permission plan from plan_nsf_record_permissions.""" + results = { + 'updates': _batch_share(vault, plan.updates, mode='update'), + 'creates': _batch_share(vault, plan.creates, mode='create'), + 'revokes': _batch_share(vault, plan.revokes, mode='revoke'), + } + _request_sync(vault, request_sync) + return results diff --git a/keepersdk-package/src/keepersdk/vault/nsf_storage_types.py b/keepersdk-package/src/keepersdk/vault/nsf_storage_types.py new file mode 100644 index 00000000..ad504e38 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_storage_types.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from ..storage.storage_types import IUid, IUidLink + + +@dataclass +class NSFSettings: + continuation_token: bytes = b'' + + +@dataclass +class NSFFolder(IUid): + folder_uid: str = '' + parent_uid: str = '' + data: str = '' + folder_type: int = 0 + inherit_user_permissions: int = 0 + folder_key: str = '' + owner_account_uid: str = '' + owner_username: str = '' + date_created: int = 0 + last_modified: int = 0 + + def uid(self) -> str: + return self.folder_uid + + +@dataclass +class NSFFolderKey(IUidLink[str, str]): + folder_uid: str = '' + parent_uid: str = '' + folder_key: str = '' + encrypted_by: int = 0 + + def subject_uid(self) -> str: + return self.folder_uid + + def object_uid(self) -> str: + return self.parent_uid + + +@dataclass +class NSFRecord(IUid): + record_uid: str = '' + revision: int = 0 + version: int = 0 + shared: bool = False + client_modified_time: int = 0 + file_size: int = 0 + thumbnail_size: int = 0 + data: str = '' + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFRecordKey(IUidLink[str, str]): + record_uid: str = '' + folder_uid: str = '' + record_key: str = '' + record_key_type: int = 0 + folder_key_encryption_type: int = 0 + + def subject_uid(self) -> str: + return self.record_uid + + def object_uid(self) -> str: + return self.folder_uid + + +@dataclass +class NSFFolderAccess(IUidLink[str, str]): + folder_uid: str = '' + access_type_uid: str = '' + access_type: int = 0 + access_role_type: int = 0 + folder_key_encrypted: str = '' + folder_key_type: int = 0 + inherited: bool = False + hidden: bool = False + denied_access: bool = False + permissions_json: str = '' + tla_properties_json: str = '' + date_created: int = 0 + last_modified: int = 0 + + def subject_uid(self) -> str: + return self.folder_uid + + def object_uid(self) -> str: + return self.access_type_uid + + +@dataclass +class NSFRecordAccess(IUidLink[str, str]): + record_uid: str = '' + access_type_uid: str = '' + access_type: int = 0 + access_role_type: int = 0 + owner: bool = False + inherited: bool = False + hidden: bool = False + denied_access: bool = False + can_view_title: bool = False + can_edit: bool = False + can_view: bool = False + can_list_access: bool = False + can_update_access: bool = False + can_delete: bool = False + can_change_ownership: bool = False + can_request_access: bool = False + can_approve_access: bool = False + date_created: int = 0 + last_modified: int = 0 + tla_properties_json: str = '' + + def subject_uid(self) -> str: + return self.record_uid + + def object_uid(self) -> str: + return self.access_type_uid + + +@dataclass +class NSFRecordLink(IUidLink[str, str]): + parent_record_uid: str = '' + child_record_uid: str = '' + record_key: str = '' + revision: int = 0 + + def subject_uid(self) -> str: + return self.parent_record_uid + + def object_uid(self) -> str: + return self.child_record_uid + + +@dataclass +class NSFFolderRecord(IUidLink[str, str]): + folder_uid: str = '' + record_uid: str = '' + + def subject_uid(self) -> str: + return self.folder_uid + + def object_uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFFolderSharingState(IUid): + folder_uid: str = '' + shared: bool = False + count: int = 0 + + def uid(self) -> str: + return self.folder_uid + + +@dataclass +class NSFRecordSharingState(IUid): + record_uid: str = '' + is_directly_shared: bool = False + is_indirectly_shared: bool = False + is_shared: bool = False + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFNonSharedData(IUid): + record_uid: str = '' + data: str = '' + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFBreachWatchRecord(IUid): + record_uid: str = '' + data: str = '' + type: int = 0 + scanned_by: str = '' + revision: int = 0 + scanned_by_account_uid: str = '' + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFSecurityScoreData(IUid): + record_uid: str = '' + data: str = '' + revision: int = 0 + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFBreachWatchSecurityData(IUid): + record_uid: str = '' + revision: int = 0 + removed: bool = False + + def uid(self) -> str: + return self.record_uid + + +@dataclass +class NSFListChunk(IUidLink[str, str]): + chunk_group: str = '' + chunk_key: str = '' + payload_json: str = '' + + def subject_uid(self) -> str: + return self.chunk_group + + def object_uid(self) -> str: + return self.chunk_key diff --git a/keepersdk-package/src/keepersdk/vault/nsf_sync.py b/keepersdk-package/src/keepersdk/vault/nsf_sync.py new file mode 100644 index 00000000..ab6bddd1 --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_sync.py @@ -0,0 +1,955 @@ +from __future__ import annotations + +import dataclasses +import json +from typing import Any, Dict, List, Mapping, Optional, TYPE_CHECKING, Tuple, Union + +from .. import utils +from . import nsf_storage_types as nsf +from .nsf_vault_storage import INSFStorage +from ..proto import SyncDown_pb2, folder_pb2, breachwatch_pb2, record_pb2 + +if TYPE_CHECKING: + from .nsf_data import NSFRebuildTask + +CHUNK_RECORD_ROTATION = 'recordRotationData' +CHUNK_RAW_DAG = 'rawDagData' + +_PROTO_ENUM_TYPES = ( + folder_pb2.FolderUsageType, + folder_pb2.FolderKeyEncryptionType, + folder_pb2.SetBooleanValue, + folder_pb2.EncryptedKeyType, + folder_pb2.AccessType, + folder_pb2.AccessRoleType, + breachwatch_pb2.BreachWatchInfoType, + record_pb2.RecordKeyType, +) + + +def _coerce_int(value: Any, default: int = 0) -> int: + if value is None or value == '': + return default + if isinstance(value, bool): + return int(value) + if isinstance(value, int): + return value + if isinstance(value, float): + return int(value) + if isinstance(value, str): + stripped = value.strip() + if stripped.lstrip('-').isdigit(): + return int(stripped) + for enum_type in _PROTO_ENUM_TYPES: + try: + return int(enum_type.Value(stripped)) + except (ValueError, AttributeError): + pass + if stripped in enum_type.keys(): + return int(enum_type.Value(stripped)) + return default + return default + + +def _wire_b64(data: bytes) -> str: + return utils.base64_url_encode(data) if data else '' + + +def _uid_b64(uid: bytes) -> str: + return utils.base64_url_encode(uid) if uid else '' + + +def _proto_submessage_set(msg: Any) -> bool: + return bool(msg and list(msg.ListFields())) + + +def _j(value: Any) -> str: + if value is None: + return '' + if isinstance(value, str): + return value + if hasattr(value, 'DESCRIPTOR'): + try: + from google.protobuf.json_format import MessageToDict + return json.dumps( + MessageToDict(value, preserving_proto_field_name=False, use_integers_for_enums=True), + separators=(',', ':')) + except (TypeError, ValueError): + return '' + return json.dumps(value, separators=(',', ':')) + + +def _folder_uid(item: Union[str, Mapping[str, Any]]) -> str: + if isinstance(item, str): + return item + return str(item.get('folderUid') or item.get('folder_uid') or '') + + +def _access_uid(x: Mapping[str, Any], *, proto_actor: bool = False) -> str: + if proto_actor: + return str(x.get('actorUid') or x.get('accessTypeUid') or '') + return str(x.get('actorUid') or x.get('accessTypeUid') or '') + + +def extract_nsf_data(sync_payload: Mapping[str, Any]) -> Optional[Dict[str, Any]]: + raw = sync_payload.get('keeperDriveData') + if isinstance(raw, dict): + return raw + return None + + +def try_apply_nsf_from_sync_down_proto( + response: SyncDown_pb2.SyncDownResponse, + nsf_storage: INSFStorage, + task: Optional['NSFRebuildTask'] = None) -> bool: + if not response.HasField('keeperDriveData'): + return False + nsf_msg = response.keeperDriveData + if not list(nsf_msg.ListFields()): + return False + apply_nsf_proto_message(nsf_msg, nsf_storage, task) + return True + + +def try_apply_nsf_from_sync_down_json( + auth: Any, + nsf_storage: INSFStorage, + continuation_token: bytes, + task: Optional['NSFRebuildTask'] = None) -> bool: + logger = utils.get_logger() + try: + rq: Dict[str, Any] = {'continuationToken': utils.base64_url_encode(continuation_token)} + rs = auth.execute_router_json('vault/sync_down', rq) + except Exception as e: + logger.debug('NSF JSON sync_down skipped: %s', e) + return False + if not isinstance(rs, dict): + return False + inner = extract_nsf_data(rs) + if inner is None: + return False + apply_nsf_data_dict(nsf_storage, inner, task) + return True + + +def apply_nsf_from_full_sync_json( + nsf_storage: INSFStorage, + sync_payload: Mapping[str, Any], + task: Optional['NSFRebuildTask'] = None) -> None: + inner = extract_nsf_data(sync_payload) + if inner is not None: + apply_nsf_data_dict(nsf_storage, inner, task) + + +def apply_nsf_proto_message( + nsf_msg: SyncDown_pb2.KeeperDriveData, + nsf_storage: INSFStorage, + task: Optional['NSFRebuildTask'] = None) -> None: + _process_removed_folders_proto(nsf_msg, nsf_storage, task) + _process_removed_folder_records_proto(nsf_msg, nsf_storage, task) + _process_removed_record_links_proto(nsf_msg, nsf_storage, task) + _store_folders_proto(nsf_msg, nsf_storage, task) + _store_folder_keys_proto(nsf_msg, nsf_storage) + _store_records_proto(nsf_msg, nsf_storage, task) + _store_record_data_proto(nsf_msg, nsf_storage, task) + _store_folder_records_proto(nsf_msg, nsf_storage, task) + _process_revoked_folder_accesses_proto(nsf_msg, nsf_storage, task) + _store_folder_accesses_proto(nsf_msg, nsf_storage) + _process_revoked_record_accesses_proto(nsf_msg, nsf_storage, task) + _store_record_accesses_proto(nsf_msg, nsf_storage, task) + _store_record_links_proto(nsf_msg, nsf_storage, task) + _store_folder_sharing_states_proto(nsf_msg, nsf_storage) + _store_record_sharing_states_proto(nsf_msg, nsf_storage) + _store_optional_extras_proto(nsf_msg, nsf_storage, task) + + +def apply_nsf_data_dict( + nsf_storage: INSFStorage, + nsf_data: Mapping[str, Any], + task: Optional['NSFRebuildTask'] = None) -> None: + _process_removed_folders_dict(nsf_data, nsf_storage, task) + _process_removed_folder_records_dict(nsf_data, nsf_storage, task) + _process_removed_record_links_dict(nsf_data, nsf_storage, task) + _store_folders_dict(nsf_data, nsf_storage, task) + _store_folder_keys_dict(nsf_data, nsf_storage) + _store_records_dict(nsf_data, nsf_storage, task) + _store_record_data_dict(nsf_data, nsf_storage, task) + _store_folder_records_dict(nsf_data, nsf_storage, task) + _process_revoked_folder_accesses_dict(nsf_data, nsf_storage, task) + _store_folder_accesses_dict(nsf_data, nsf_storage) + _process_revoked_record_accesses_dict(nsf_data, nsf_storage, task) + _store_record_accesses_dict(nsf_data, nsf_storage, task) + _store_record_links_dict(nsf_data, nsf_storage, task) + _store_folder_sharing_states_dict(nsf_data, nsf_storage) + _store_record_sharing_states_dict(nsf_data, nsf_storage) + _store_optional_extras_dict(nsf_data, nsf_storage, task) + + +def _process_removed_folders_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + removed = [_uid_b64(x.folder_uid) for x in nsf_msg.removedFolders if x.folder_uid] + if not removed: + return + storage.folder_keys.delete_links_by_subjects(removed) + storage.folder_records.delete_links_by_subjects(removed) + storage.folders.delete_uids(removed) + if task: + task.add_folders(removed) + + +def _process_removed_folder_records_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links: List[Tuple[str, str]] = [] + key_links: List[Tuple[str, str]] = [] + for x in nsf_msg.removedFolderRecords: + fu, ru = _uid_b64(x.folder_uid), _uid_b64(x.record_uid) + if fu and ru: + links.append((fu, ru)) + key_links.append((ru, fu)) + if links: + storage.folder_records.delete_links(links) + if key_links: + storage.record_keys.delete_links(key_links) + if task and links: + task.add_records((ru for _, ru in links)) + + +def _process_removed_record_links_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + for x in nsf_msg.removedRecordLinks: + parent_uid, child_uid = _uid_b64(x.parentRecordUid), _uid_b64(x.childRecordUid) + storage.record_links.delete_links([(parent_uid, child_uid)]) + if task and child_uid: + task.add_record(child_uid) + + +def _process_revoked_folder_accesses_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links = [(_uid_b64(x.folderUid), _uid_b64(x.actorUid)) for x in nsf_msg.revokedFolderAccesses] + if links: + storage.folder_accesses.delete_links(links) + if task: + task.add_folders((fu for fu, _ in links if fu)) + + +def _process_revoked_record_accesses_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links = [(_uid_b64(x.recordUid), _uid_b64(x.actorUid)) for x in nsf_msg.revokedRecordAccesses] + if links: + storage.record_accesses.delete_links(links) + if task: + task.add_records((ru for ru, _ in links if ru)) + + +def _store_folders_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + folders = [_proto_folder(x) for x in nsf_msg.folders] + if folders: + storage.folders.put_entities(folders) + if task: + task.add_folders((f.folder_uid for f in folders)) + + +def _store_folder_keys_proto(nsf_msg: SyncDown_pb2.KeeperDriveData, storage: INSFStorage) -> None: + keys = [_proto_folder_key(x) for x in nsf_msg.folderKeys] + if keys: + storage.folder_keys.put_links(keys) + + +def _store_record_data_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + updated: List[nsf.NSFRecord] = [] + for rd in nsf_msg.recordData: + record_uid = _uid_b64(rd.recordUid) + if not record_uid: + continue + existing = storage.records.get_entity(record_uid) + if existing is None: + existing = nsf.NSFRecord(record_uid=record_uid) + row = dataclasses.replace(existing, data=_wire_b64(rd.data)) + updated.append(row) + if task: + task.add_record(record_uid) + if updated: + storage.records.put_entities(updated) + + +def _store_folder_records_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + folder_records: List[nsf.NSFFolderRecord] = [] + record_keys: List[nsf.NSFRecordKey] = [] + for fr in nsf_msg.folderRecords: + folder_uid = _uid_b64(fr.folderUid) + md = fr.recordMetadata + if md is None or not list(md.ListFields()): + continue + record_uid = _uid_b64(md.recordUid) + folder_records.append(nsf.NSFFolderRecord(folder_uid=folder_uid, record_uid=record_uid)) + if md.encryptedRecordKey: + record_keys.append(nsf.NSFRecordKey( + record_uid=record_uid, + folder_uid=folder_uid, + record_key=_wire_b64(md.encryptedRecordKey), + record_key_type=int(md.encryptedRecordKeyType), + folder_key_encryption_type=int(fr.folderKeyEncryptionType), + )) + if folder_records: + storage.folder_records.put_links(folder_records) + if task: + task.add_records((r.record_uid for r in folder_records)) + if record_keys: + storage.record_keys.put_links(record_keys) + + +def _store_records_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + rows: List[nsf.NSFRecord] = [] + for dr in nsf_msg.records: + record_uid = _uid_b64(dr.recordUid) + existing = storage.records.get_entity(record_uid) + rows.append(nsf.NSFRecord( + record_uid=record_uid, + revision=dr.revision, + version=dr.version, + shared=dr.shared, + client_modified_time=dr.clientModifiedTime, + file_size=dr.fileSize, + thumbnail_size=dr.thumbnailSize, + data=existing.data if existing else '', + )) + if rows: + storage.records.put_entities(rows) + if task: + task.add_records((r.record_uid for r in rows)) + + +def _store_folder_accesses_proto(nsf_msg: SyncDown_pb2.KeeperDriveData, storage: INSFStorage) -> None: + rows = [_proto_folder_access(x) for x in nsf_msg.folderAccesses] + if rows: + storage.folder_accesses.put_links(rows) + + +def _store_record_accesses_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + rows = [_proto_record_access(x) for x in nsf_msg.recordAccesses] + if rows: + storage.record_accesses.put_links(rows) + if task: + task.add_records((r.record_uid for r in rows)) + + +def _store_record_links_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + rows = [_proto_record_link(x) for x in nsf_msg.recordLinks] + if rows: + storage.record_links.put_links(rows) + if task: + task.add_records((r.child_record_uid for r in rows)) + + +def _store_folder_sharing_states_proto(nsf_msg: SyncDown_pb2.KeeperDriveData, storage: INSFStorage) -> None: + rows = [nsf.NSFFolderSharingState( + folder_uid=_uid_b64(x.folderUid), + shared=x.shared, + count=x.count, + ) for x in nsf_msg.folderSharingState] + if rows: + storage.folder_sharing_states.put_entities(rows) + + +def _store_record_sharing_states_proto(nsf_msg: SyncDown_pb2.KeeperDriveData, storage: INSFStorage) -> None: + rows = [nsf.NSFRecordSharingState( + record_uid=_uid_b64(x.recordUid), + is_directly_shared=x.isDirectlyShared, + is_indirectly_shared=x.isIndirectlyShared, + is_shared=x.isShared, + ) for x in nsf_msg.recordSharingStates] + if rows: + storage.record_sharing_states.put_entities(rows) + + +def _store_optional_extras_proto( + nsf_msg: SyncDown_pb2.KeeperDriveData, + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + nsd = [_proto_non_shared(x) for x in nsf_msg.nonSharedData] + if nsd: + storage.non_shared_data.put_entities(nsd) + if task: + task.add_records((r.record_uid for r in nsd)) + bw = [_proto_bw_record(x) for x in nsf_msg.breachWatchRecords] + if bw: + storage.breach_watch_records.put_entities(bw) + if task: + task.add_records((r.record_uid for r in bw)) + ss = [_proto_security_score(x) for x in nsf_msg.securityScoreData] + if ss: + storage.security_score_data.put_entities(ss) + if task: + task.add_records((r.record_uid for r in ss)) + bws = [_proto_bw_security(x) for x in nsf_msg.breachWatchSecurityData] + if bws: + storage.breach_watch_security_data.put_entities(bws) + if task: + task.add_records((r.record_uid for r in bws)) + chunk_payload: Dict[str, Any] = { + CHUNK_RECORD_ROTATION: list(nsf_msg.recordRotationData), + CHUNK_RAW_DAG: list(nsf_msg.rawDagData), + } + _replace_json_lists(storage, chunk_payload) + + +def _process_removed_folders_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + removed = [_folder_uid(y) for y in (d.get('removedFolders') or []) if _folder_uid(y)] + if not removed: + return + storage.folder_keys.delete_links_by_subjects(removed) + storage.folder_records.delete_links_by_subjects(removed) + storage.folders.delete_uids(removed) + if task: + task.add_folders(removed) + + +def _process_removed_folder_records_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links: List[Tuple[str, str]] = [] + key_links: List[Tuple[str, str]] = [] + for x in d.get('removedFolderRecords') or []: + if not isinstance(x, dict): + continue + fu = _folder_uid(x) + ru = str(x.get('recordUid') or x.get('record_uid') or '') + if fu and ru: + links.append((fu, ru)) + key_links.append((ru, fu)) + if links: + storage.folder_records.delete_links(links) + if key_links: + storage.record_keys.delete_links(key_links) + if task and links: + task.add_records((ru for _, ru in links)) + + +def _process_removed_record_links_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + for x in d.get('removedRecordLinks') or []: + if not isinstance(x, dict): + continue + child_uid = str(x.get('childRecordUid', '')) + storage.record_links.delete_links([( + str(x.get('parentRecordUid', '')), + child_uid, + )]) + if task and child_uid: + task.add_record(child_uid) + + +def _process_revoked_folder_accesses_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links: List[Tuple[str, str]] = [] + for x in d.get('revokedFolderAccesses') or []: + if not isinstance(x, dict): + continue + fu = str(x.get('folderUid', '')) + au = _access_uid(x) + if fu and au: + links.append((fu, au)) + if links: + storage.folder_accesses.delete_links(links) + if task: + task.add_folders((fu for fu, _ in links)) + + +def _process_revoked_record_accesses_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + links: List[Tuple[str, str]] = [] + for x in d.get('revokedRecordAccesses') or []: + if not isinstance(x, dict): + continue + ru = str(x.get('recordUid', '')) + au = _access_uid(x) + if ru and au: + links.append((ru, au)) + if links: + storage.record_accesses.delete_links(links) + if task: + task.add_records((ru for ru, _ in links)) + + +def _store_folders_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + folders = [_dict_to_folder(x) for x in d.get('folders') or [] if isinstance(x, dict)] + if folders: + storage.folders.put_entities(folders) + if task: + task.add_folders((f.folder_uid for f in folders)) + + +def _store_folder_keys_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: + keys = [_dict_to_folder_key(x) for x in d.get('folderKeys') or [] if isinstance(x, dict)] + if keys: + storage.folder_keys.put_links(keys) + + +def _store_record_data_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + updated: List[nsf.NSFRecord] = [] + for x in d.get('recordData') or []: + if not isinstance(x, dict): + continue + record_uid = str(x.get('recordUid', '')) + if not record_uid: + continue + existing = storage.records.get_entity(record_uid) + if existing is None: + existing = nsf.NSFRecord(record_uid=record_uid) + updated.append(dataclasses.replace(existing, data=str(x.get('data', '')))) + if task: + task.add_record(record_uid) + if updated: + storage.records.put_entities(updated) + + +def _store_folder_records_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + folder_records: List[nsf.NSFFolderRecord] = [] + record_keys: List[nsf.NSFRecordKey] = [] + for x in d.get('folderRecords') or []: + if not isinstance(x, dict): + continue + folder_uid = str(x.get('folderUid', '')) + md = x.get('recordMetadata') or {} + record_uid = str(md.get('recordUid', '')) + if not folder_uid or not record_uid: + continue + folder_records.append(nsf.NSFFolderRecord(folder_uid=folder_uid, record_uid=record_uid)) + enc_key = str(md.get('encryptedRecordKey', '')) + if enc_key: + record_keys.append(nsf.NSFRecordKey( + record_uid=record_uid, + folder_uid=folder_uid, + record_key=enc_key, + record_key_type=_coerce_int(md.get('encryptedRecordKeyType'), 0), + folder_key_encryption_type=_coerce_int(x.get('folderKeyEncryptionType'), 0), + )) + if folder_records: + storage.folder_records.put_links(folder_records) + if task: + task.add_records((r.record_uid for r in folder_records)) + if record_keys: + storage.record_keys.put_links(record_keys) + + +def _store_records_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + rows: List[nsf.NSFRecord] = [] + for x in d.get('records') or []: + if not isinstance(x, dict): + continue + record_uid = str(x.get('recordUid', '')) + existing = storage.records.get_entity(record_uid) + rows.append(nsf.NSFRecord( + record_uid=record_uid, + revision=_coerce_int(x.get('revision'), 0), + version=_coerce_int(x.get('version'), 0), + shared=bool(x.get('shared')), + client_modified_time=_coerce_int(x.get('clientModifiedTime'), 0), + file_size=_coerce_int(x.get('fileSize'), 0), + thumbnail_size=_coerce_int(x.get('thumbnailSize'), 0), + data=existing.data if existing else '', + )) + if rows: + storage.records.put_entities(rows) + if task: + task.add_records((r.record_uid for r in rows)) + + +def _store_folder_accesses_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: + rows = [_dict_to_folder_access(x) for x in d.get('folderAccesses') or [] if isinstance(x, dict)] + if rows: + storage.folder_accesses.put_links(rows) + + +def _store_record_accesses_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask'] = None) -> None: + rows = [_dict_to_record_access(x) for x in d.get('recordAccesses') or [] if isinstance(x, dict)] + if rows: + storage.record_accesses.put_links(rows) + if task: + task.add_records((r.record_uid for r in rows)) + + +def _store_record_links_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + rows = [_dict_to_record_link(x) for x in d.get('recordLinks') or [] if isinstance(x, dict)] + if rows: + storage.record_links.put_links(rows) + if task: + task.add_records((r.child_record_uid for r in rows)) + + +def _store_folder_sharing_states_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: + rows: List[nsf.NSFFolderSharingState] = [] + for x in d.get('folderSharingState') or []: + if not isinstance(x, dict): + continue + rows.append(nsf.NSFFolderSharingState( + folder_uid=str(x.get('folderUid', '')), + shared=bool(x.get('shared')), + count=_coerce_int(x.get('count'), 0), + )) + if rows: + storage.folder_sharing_states.put_entities(rows) + + +def _store_record_sharing_states_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: + rows: List[nsf.NSFRecordSharingState] = [] + for x in d.get('recordSharingStates') or []: + if not isinstance(x, dict): + continue + rows.append(nsf.NSFRecordSharingState( + record_uid=str(x.get('recordUid', '')), + is_directly_shared=bool(x.get('isDirectlyShared')), + is_indirectly_shared=bool(x.get('isIndirectlyShared')), + is_shared=bool(x.get('isShared')), + )) + if rows: + storage.record_sharing_states.put_entities(rows) + + +def _store_optional_extras_dict( + d: Mapping[str, Any], + storage: INSFStorage, + task: Optional['NSFRebuildTask']) -> None: + nsd = [_dict_to_non_shared(x) for x in d.get('nonSharedData') or [] if isinstance(x, dict)] + if nsd: + storage.non_shared_data.put_entities(nsd) + if task: + task.add_records((r.record_uid for r in nsd)) + bw = [_dict_to_bw_record(x) for x in d.get('breachWatchRecords') or [] if isinstance(x, dict)] + if bw: + storage.breach_watch_records.put_entities(bw) + if task: + task.add_records((r.record_uid for r in bw)) + ss = [_dict_to_security_score(x) for x in d.get('securityScoreData') or [] if isinstance(x, dict)] + if ss: + storage.security_score_data.put_entities(ss) + if task: + task.add_records((r.record_uid for r in ss)) + bws = [_dict_to_bw_security(x) for x in d.get('breachWatchSecurityData') or [] if isinstance(x, dict)] + if bws: + storage.breach_watch_security_data.put_entities(bws) + if task: + task.add_records((r.record_uid for r in bws)) + chunk_payload: Dict[str, Any] = { + CHUNK_RECORD_ROTATION: d.get('recordRotationData'), + CHUNK_RAW_DAG: d.get('rawDagData'), + } + _replace_json_lists(storage, chunk_payload) + + +def _replace_json_lists(storage: INSFStorage, d: Mapping[str, Any]) -> None: + _replace_chunk_group(storage, CHUNK_RECORD_ROTATION, d.get(CHUNK_RECORD_ROTATION)) + _replace_chunk_group(storage, CHUNK_RAW_DAG, d.get(CHUNK_RAW_DAG)) + + +def _replace_chunk_group(storage: INSFStorage, group: str, items: Any) -> None: + storage.list_chunks.delete_links_by_subjects([group]) + if not isinstance(items, list) or not items: + return + links: List[nsf.NSFListChunk] = [] + for i, it in enumerate(items): + links.append(nsf.NSFListChunk(chunk_group=group, chunk_key=f'{i:010d}', payload_json=_j(it))) + storage.list_chunks.put_links(links) + + +def _proto_folder(fd: folder_pb2.FolderData) -> nsf.NSFFolder: + oi = fd.ownerInfo + return nsf.NSFFolder( + folder_uid=_uid_b64(fd.folderUid), + parent_uid=_uid_b64(fd.parentUid), + data=_wire_b64(fd.data), + folder_type=int(fd.type), + inherit_user_permissions=int(fd.inheritUserPermissions), + folder_key=_wire_b64(fd.folderKey), + owner_account_uid=_uid_b64(oi.accountUid) if _proto_submessage_set(oi) else '', + owner_username=oi.username if _proto_submessage_set(oi) else '', + date_created=fd.dateCreated, + last_modified=fd.lastModified, + ) + + +def _proto_folder_key(fk: folder_pb2.FolderKey) -> nsf.NSFFolderKey: + return nsf.NSFFolderKey( + folder_uid=_uid_b64(fk.folderUid), + parent_uid=_uid_b64(fk.parentUid), + folder_key=_wire_b64(fk.folderKey), + encrypted_by=int(fk.encryptedBy), + ) + + +def _proto_folder_access(fa: folder_pb2.FolderAccessData) -> nsf.NSFFolderAccess: + enc, kt = '', 0 + fk = fa.folderKey + if _proto_submessage_set(fk): + enc = _wire_b64(fk.encryptedKey) + kt = int(fk.encryptedKeyType) + return nsf.NSFFolderAccess( + folder_uid=_uid_b64(fa.folderUid), + access_type_uid=_uid_b64(fa.accessTypeUid), + access_type=int(fa.accessType), + access_role_type=int(fa.accessRoleType), + folder_key_encrypted=enc, + folder_key_type=kt, + inherited=fa.inherited, + hidden=fa.hidden, + denied_access=fa.deniedAccess, + permissions_json=_j(fa.permissions) if _proto_submessage_set(fa.permissions) else '', + tla_properties_json='', + date_created=fa.dateCreated, + last_modified=fa.lastModified, + ) + + +def _proto_non_shared(nsd: SyncDown_pb2.NonSharedData) -> nsf.NSFNonSharedData: + return nsf.NSFNonSharedData( + record_uid=_uid_b64(nsd.recordUid), + data=_wire_b64(nsd.data), + ) + + +def _proto_record_access(ra: folder_pb2.RecordAccessData) -> nsf.NSFRecordAccess: + return nsf.NSFRecordAccess( + record_uid=_uid_b64(ra.recordUid), + access_type_uid=_uid_b64(ra.accessTypeUid), + access_type=int(ra.accessType), + access_role_type=int(ra.accessRoleType), + owner=ra.owner, + inherited=ra.inherited, + hidden=ra.hidden, + denied_access=ra.deniedAccess, + can_view_title=ra.can_view_title, + can_edit=ra.can_edit, + can_view=ra.can_view, + can_list_access=ra.can_list_access, + can_update_access=ra.can_update_access, + can_delete=ra.can_delete, + can_change_ownership=ra.can_change_ownership, + can_request_access=ra.can_request_access, + can_approve_access=ra.can_approve_access, + date_created=ra.dateCreated, + last_modified=ra.lastModified, + tla_properties_json='', + ) + + +def _proto_record_link(rl: SyncDown_pb2.RecordLink) -> nsf.NSFRecordLink: + return nsf.NSFRecordLink( + parent_record_uid=_uid_b64(rl.parentRecordUid), + child_record_uid=_uid_b64(rl.childRecordUid), + record_key=_wire_b64(rl.recordKey), + revision=rl.revision, + ) + + +def _proto_bw_record(bwr: SyncDown_pb2.BreachWatchRecord) -> nsf.NSFBreachWatchRecord: + return nsf.NSFBreachWatchRecord( + record_uid=_uid_b64(bwr.recordUid), + data=_wire_b64(bwr.data), + type=int(bwr.type), + scanned_by=bwr.scannedBy, + revision=bwr.revision, + scanned_by_account_uid=_uid_b64(bwr.scannedByAccountUid), + ) + + +def _proto_security_score(ss: SyncDown_pb2.SecurityScoreData) -> nsf.NSFSecurityScoreData: + return nsf.NSFSecurityScoreData( + record_uid=_uid_b64(ss.recordUid), + data=_wire_b64(ss.data), + revision=ss.revision, + ) + + +def _proto_bw_security(bws: SyncDown_pb2.BreachWatchSecurityData) -> nsf.NSFBreachWatchSecurityData: + return nsf.NSFBreachWatchSecurityData( + record_uid=_uid_b64(bws.recordUid), + revision=bws.revision, + removed=bws.removed, + ) + + +def _dict_to_folder(x: Dict[str, Any]) -> nsf.NSFFolder: + oi = x.get('ownerInfo') or {} + return nsf.NSFFolder( + folder_uid=str(x.get('folderUid', '')), + parent_uid=str(x.get('parentUid', '')), + data=str(x.get('data', '')), + folder_type=_coerce_int(x.get('type'), 0), + inherit_user_permissions=_coerce_int(x.get('inheritUserPermissions'), 0), + folder_key=str(x.get('folderKey', '')), + owner_account_uid=str(oi.get('accountUid', '')), + owner_username=str(oi.get('username', '')), + date_created=_coerce_int(x.get('dateCreated'), 0), + last_modified=_coerce_int(x.get('lastModified'), 0), + ) + + +def _dict_to_folder_key(x: Dict[str, Any]) -> nsf.NSFFolderKey: + return nsf.NSFFolderKey( + folder_uid=str(x.get('folderUid', '')), + parent_uid=str(x.get('parentUid', '')), + folder_key=str(x.get('folderKey', '')), + encrypted_by=_coerce_int(x.get('encryptedBy'), 0), + ) + + +def _dict_to_folder_access(x: Dict[str, Any]) -> nsf.NSFFolderAccess: + fk = x.get('folderKey') + enc, kt = '', 0 + if isinstance(fk, dict): + enc = str(fk.get('encryptedKey', '')) + kt = _coerce_int(fk.get('encryptedKeyType'), 0) + elif fk is not None and fk != '': + enc = str(fk) + return nsf.NSFFolderAccess( + folder_uid=str(x.get('folderUid', '')), + access_type_uid=str(x.get('accessTypeUid', '')), + access_type=_coerce_int(x.get('accessType'), 0), + access_role_type=_coerce_int(x.get('accessRoleType'), 0), + folder_key_encrypted=enc, + folder_key_type=kt, + inherited=bool(x.get('inherited')), + hidden=bool(x.get('hidden')), + denied_access=bool(x.get('deniedAccess')), + permissions_json=_j(x.get('permissions')), + tla_properties_json=_j(x.get('tlaProperties')), + date_created=_coerce_int(x.get('dateCreated'), 0), + last_modified=_coerce_int(x.get('lastModified'), 0), + ) + + +def _dict_to_non_shared(x: Dict[str, Any]) -> nsf.NSFNonSharedData: + return nsf.NSFNonSharedData( + record_uid=str(x.get('recordUid', '')), + data=str(x.get('data', '')), + ) + + +def _dict_to_record_access(x: Dict[str, Any]) -> nsf.NSFRecordAccess: + return nsf.NSFRecordAccess( + record_uid=str(x.get('recordUid', '')), + access_type_uid=str(x.get('accessTypeUid', '')), + access_type=_coerce_int(x.get('accessType'), 0), + access_role_type=_coerce_int(x.get('accessRoleType'), 0), + owner=bool(x.get('owner')), + inherited=bool(x.get('inherited')), + hidden=bool(x.get('hidden')), + denied_access=bool(x.get('deniedAccess')), + can_view_title=bool(x.get('canViewTitle')), + can_edit=bool(x.get('canEdit')), + can_view=bool(x.get('canView')), + can_list_access=bool(x.get('canListAccess')), + can_update_access=bool(x.get('canUpdateAccess')), + can_delete=bool(x.get('canDelete')), + can_change_ownership=bool(x.get('canChangeOwnership')), + can_request_access=bool(x.get('canRequestAccess')), + can_approve_access=bool(x.get('canApproveAccess')), + date_created=_coerce_int(x.get('dateCreated'), 0), + last_modified=_coerce_int(x.get('lastModified'), 0), + tla_properties_json=_j(x.get('tlaProperties')), + ) + + +def _dict_to_record_link(x: Dict[str, Any]) -> nsf.NSFRecordLink: + return nsf.NSFRecordLink( + parent_record_uid=str(x.get('parentRecordUid', '')), + child_record_uid=str(x.get('childRecordUid', '')), + record_key=str(x.get('recordKey', '')), + revision=_coerce_int(x.get('revision'), 0), + ) + + +def _dict_to_bw_record(x: Dict[str, Any]) -> nsf.NSFBreachWatchRecord: + return nsf.NSFBreachWatchRecord( + record_uid=str(x.get('recordUid', '')), + data=str(x.get('data', '')), + type=_coerce_int(x.get('type'), 0), + scanned_by=str(x.get('scannedBy', '')), + revision=_coerce_int(x.get('revision'), 0), + scanned_by_account_uid=str(x.get('scannedByAccountUid', '')), + ) + + +def _dict_to_security_score(x: Dict[str, Any]) -> nsf.NSFSecurityScoreData: + return nsf.NSFSecurityScoreData( + record_uid=str(x.get('recordUid', '')), + data=str(x.get('data', '')), + revision=_coerce_int(x.get('revision'), 0), + ) + + +def _dict_to_bw_security(x: Dict[str, Any]) -> nsf.NSFBreachWatchSecurityData: + return nsf.NSFBreachWatchSecurityData( + record_uid=str(x.get('recordUid', '')), + revision=_coerce_int(x.get('revision'), 0), + removed=bool(x.get('removed')), + ) + + +def load_list_chunks(storage: INSFStorage, group: str) -> List[Any]: + """Decode ``NSFListChunk`` rows for ``group`` back into Python values.""" + out: List[Any] = [] + for link in storage.list_chunks.get_links_by_subject(group): + if not isinstance(link, nsf.NSFListChunk) or not link.payload_json: + continue + try: + out.append(json.loads(link.payload_json)) + except json.JSONDecodeError: + out.append(link.payload_json) + return out diff --git a/keepersdk-package/src/keepersdk/vault/nsf_vault_storage.py b/keepersdk-package/src/keepersdk/vault/nsf_vault_storage.py new file mode 100644 index 00000000..84abffee --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/nsf_vault_storage.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import abc + +from ..storage.storage_types import IEntityReaderStorage, ILinkReaderStorage, IRecordStorage +from . import nsf_storage_types as nsf + + +class INSFStorage(abc.ABC): + + @property + @abc.abstractmethod + def settings(self) -> IRecordStorage[nsf.NSFSettings]: + pass + + @property + @abc.abstractmethod + def folders(self) -> IEntityReaderStorage[nsf.NSFFolder, str]: + pass + + @property + @abc.abstractmethod + def folder_keys(self) -> ILinkReaderStorage[nsf.NSFFolderKey, str, str]: + pass + + @property + @abc.abstractmethod + def records(self) -> IEntityReaderStorage[nsf.NSFRecord, str]: + pass + + @property + @abc.abstractmethod + def record_keys(self) -> ILinkReaderStorage[nsf.NSFRecordKey, str, str]: + pass + + @property + @abc.abstractmethod + def folder_accesses(self) -> ILinkReaderStorage[nsf.NSFFolderAccess, str, str]: + pass + + @property + @abc.abstractmethod + def record_accesses(self) -> ILinkReaderStorage[nsf.NSFRecordAccess, str, str]: + pass + + @property + @abc.abstractmethod + def record_links(self) -> ILinkReaderStorage[nsf.NSFRecordLink, str, str]: + pass + + @property + @abc.abstractmethod + def folder_records(self) -> ILinkReaderStorage[nsf.NSFFolderRecord, str, str]: + pass + + @property + @abc.abstractmethod + def folder_sharing_states(self) -> IEntityReaderStorage[nsf.NSFFolderSharingState, str]: + pass + + @property + @abc.abstractmethod + def record_sharing_states(self) -> IEntityReaderStorage[nsf.NSFRecordSharingState, str]: + pass + + @property + @abc.abstractmethod + def non_shared_data(self) -> IEntityReaderStorage[nsf.NSFNonSharedData, str]: + pass + + @property + @abc.abstractmethod + def breach_watch_records(self) -> IEntityReaderStorage[nsf.NSFBreachWatchRecord, str]: + pass + + @property + @abc.abstractmethod + def security_score_data(self) -> IEntityReaderStorage[nsf.NSFSecurityScoreData, str]: + pass + + @property + @abc.abstractmethod + def breach_watch_security_data(self) -> IEntityReaderStorage[nsf.NSFBreachWatchSecurityData, str]: + pass + + @property + @abc.abstractmethod + def list_chunks(self) -> ILinkReaderStorage[nsf.NSFListChunk, str, str]: + pass + + @abc.abstractmethod + def clear_all(self) -> None: + pass + + def close(self) -> None: + pass diff --git a/keepersdk-package/src/keepersdk/vault/share_management_utils.py b/keepersdk-package/src/keepersdk/vault/share_management_utils.py index cf9a55e7..31608ea1 100644 --- a/keepersdk-package/src/keepersdk/vault/share_management_utils.py +++ b/keepersdk-package/src/keepersdk/vault/share_management_utils.py @@ -94,6 +94,18 @@ def get_share_expiration(expire_at: Optional[str], expire_in: Optional[str]) -> raise ShareValidationError(f'Invalid expiration format: {e}') from e +def parse_nsf_share_expiration( + expire_at: Optional[str], + expire_in: Optional[str]) -> Optional[int]: + """Parse NSF TLA expiration as a millisecond timestamp (vault/records/v3/share).""" + if not expire_at and not expire_in: + return None + value = get_share_expiration(expire_at, expire_in) + if value == NEVER_EXPIRES: + return NEVER_EXPIRES + return value * 1000 + + def get_share_objects(vault: vault_online.VaultOnline) -> Dict[str, Dict[str, Any]]: try: request = record_pb2.GetShareObjectsRequest() diff --git a/keepersdk-package/src/keepersdk/vault/sqlite_nsf_storage.py b/keepersdk-package/src/keepersdk/vault/sqlite_nsf_storage.py new file mode 100644 index 00000000..bb2c659e --- /dev/null +++ b/keepersdk-package/src/keepersdk/vault/sqlite_nsf_storage.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import sqlite3 +from typing import Callable, Tuple, Type + +from . import nsf_storage_types as nsf +from .nsf_vault_storage import INSFStorage +from .. import sqlite_dao, utils +from ..storage import sqlite + + +def nsf_table_schemas(owner_column: str, owner_type: Type[sqlite_dao.KeyTypes] + ) -> Tuple[sqlite_dao.TableSchema, ...]: + + settings_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFSettings, [], owner_column=owner_column, owner_type=owner_type) + folder_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFFolder, 'folder_uid', owner_column=owner_column, owner_type=owner_type) + folder_key_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFFolderKey, ['folder_uid', 'parent_uid'], + indexes={'ParentUid': ['parent_uid']}, owner_column=owner_column, owner_type=owner_type) + record_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFRecord, 'record_uid', owner_column=owner_column, owner_type=owner_type) + record_key_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFRecordKey, ['record_uid', 'folder_uid'], + indexes={'FolderUid': ['folder_uid']}, owner_column=owner_column, owner_type=owner_type) + folder_access_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFFolderAccess, ['folder_uid', 'access_type_uid'], + indexes={'AccessTypeUid': ['access_type_uid']}, owner_column=owner_column, owner_type=owner_type) + record_access_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFRecordAccess, ['record_uid', 'access_type_uid'], + indexes={'AccessTypeUid': ['access_type_uid']}, owner_column=owner_column, owner_type=owner_type) + record_link_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFRecordLink, ['parent_record_uid', 'child_record_uid'], + indexes={'ChildRecordUid': ['child_record_uid']}, owner_column=owner_column, owner_type=owner_type) + folder_record_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFFolderRecord, ['folder_uid', 'record_uid'], + indexes={'RecordUid': ['record_uid']}, owner_column=owner_column, owner_type=owner_type) + folder_sharing_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFFolderSharingState, 'folder_uid', owner_column=owner_column, owner_type=owner_type) + record_sharing_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFRecordSharingState, 'record_uid', owner_column=owner_column, owner_type=owner_type) + non_shared_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFNonSharedData, 'record_uid', owner_column=owner_column, owner_type=owner_type) + bw_record_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFBreachWatchRecord, 'record_uid', owner_column=owner_column, owner_type=owner_type) + security_score_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFSecurityScoreData, 'record_uid', owner_column=owner_column, owner_type=owner_type) + bw_sec_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFBreachWatchSecurityData, 'record_uid', owner_column=owner_column, owner_type=owner_type) + list_chunk_schema = sqlite_dao.TableSchema.load_schema( + nsf.NSFListChunk, ['chunk_group', 'chunk_key'], + indexes={'ChunkKey': ['chunk_key']}, owner_column=owner_column, owner_type=owner_type) + return (settings_schema, folder_schema, folder_key_schema, record_schema, record_key_schema, + folder_access_schema, record_access_schema, record_link_schema, folder_record_schema, + folder_sharing_schema, record_sharing_schema, non_shared_schema, bw_record_schema, + security_score_schema, bw_sec_schema, list_chunk_schema) + + +class SqliteNSFStorage(INSFStorage): + def __init__(self, get_connection: Callable[[], sqlite3.Connection], vault_owner: bytes, *, + verify: bool = True) -> None: + self.get_connection = get_connection + self.vault_owner = vault_owner + self.owner_column = 'owner_uid' + + schemas = nsf_table_schemas(self.owner_column, bytes) + if verify: + sqlite_dao.verify_database(self.get_connection(), schemas) + + (settings_schema, folder_schema, folder_key_schema, record_schema, record_key_schema, + folder_access_schema, record_access_schema, record_link_schema, folder_record_schema, + folder_sharing_schema, record_sharing_schema, non_shared_schema, bw_record_schema, + security_score_schema, bw_sec_schema, list_chunk_schema) = schemas + + self._settings = sqlite.SqliteRecordStorage( + self.get_connection, settings_schema, owner=self.vault_owner) + self._folders = sqlite.SqliteEntityStorage( + self.get_connection, folder_schema, owner=self.vault_owner) + self._folder_keys = sqlite.SqliteLinkStorage( + self.get_connection, folder_key_schema, owner=self.vault_owner) + self._records = sqlite.SqliteEntityStorage( + self.get_connection, record_schema, owner=self.vault_owner) + self._record_keys = sqlite.SqliteLinkStorage( + self.get_connection, record_key_schema, owner=self.vault_owner) + self._folder_accesses = sqlite.SqliteLinkStorage( + self.get_connection, folder_access_schema, owner=self.vault_owner) + self._record_accesses = sqlite.SqliteLinkStorage( + self.get_connection, record_access_schema, owner=self.vault_owner) + self._record_links = sqlite.SqliteLinkStorage( + self.get_connection, record_link_schema, owner=self.vault_owner) + self._folder_records = sqlite.SqliteLinkStorage( + self.get_connection, folder_record_schema, owner=self.vault_owner) + self._folder_sharing_states = sqlite.SqliteEntityStorage( + self.get_connection, folder_sharing_schema, owner=self.vault_owner) + self._record_sharing_states = sqlite.SqliteEntityStorage( + self.get_connection, record_sharing_schema, owner=self.vault_owner) + self._non_shared_data = sqlite.SqliteEntityStorage( + self.get_connection, non_shared_schema, owner=self.vault_owner) + self._breach_watch_records = sqlite.SqliteEntityStorage( + self.get_connection, bw_record_schema, owner=self.vault_owner) + self._security_score_data = sqlite.SqliteEntityStorage( + self.get_connection, security_score_schema, owner=self.vault_owner) + self._breach_watch_security_data = sqlite.SqliteEntityStorage( + self.get_connection, bw_sec_schema, owner=self.vault_owner) + self._list_chunks = sqlite.SqliteLinkStorage( + self.get_connection, list_chunk_schema, owner=self.vault_owner) + + @property + def personal_scope_uid(self) -> str: + return utils.base64_url_encode(self.vault_owner) + + @property + def settings(self): + return self._settings + + @property + def folders(self): + return self._folders + + @property + def folder_keys(self): + return self._folder_keys + + @property + def records(self): + return self._records + + @property + def record_keys(self): + return self._record_keys + + @property + def folder_accesses(self): + return self._folder_accesses + + @property + def record_accesses(self): + return self._record_accesses + + @property + def record_links(self): + return self._record_links + + @property + def folder_records(self): + return self._folder_records + + @property + def folder_sharing_states(self): + return self._folder_sharing_states + + @property + def record_sharing_states(self): + return self._record_sharing_states + + @property + def non_shared_data(self): + return self._non_shared_data + + @property + def breach_watch_records(self): + return self._breach_watch_records + + @property + def security_score_data(self): + return self._security_score_data + + @property + def breach_watch_security_data(self): + return self._breach_watch_security_data + + @property + def list_chunks(self): + return self._list_chunks + + def clear_all(self) -> None: + self._settings.delete_all() + self._folders.delete_all() + self._folder_keys.delete_all() + self._records.delete_all() + self._record_keys.delete_all() + self._folder_accesses.delete_all() + self._record_accesses.delete_all() + self._record_links.delete_all() + self._folder_records.delete_all() + self._folder_sharing_states.delete_all() + self._record_sharing_states.delete_all() + self._non_shared_data.delete_all() + self._breach_watch_records.delete_all() + self._security_score_data.delete_all() + self._breach_watch_security_data.delete_all() + self._list_chunks.delete_all() + + def close(self) -> None: + pass diff --git a/keepersdk-package/src/keepersdk/vault/sqlite_storage.py b/keepersdk-package/src/keepersdk/vault/sqlite_storage.py index 4862aea2..868180cd 100644 --- a/keepersdk-package/src/keepersdk/vault/sqlite_storage.py +++ b/keepersdk-package/src/keepersdk/vault/sqlite_storage.py @@ -1,7 +1,7 @@ import sqlite3 from typing import Callable -from . import vault_storage, storage_types +from . import vault_storage, storage_types, sqlite_nsf_storage, nsf_vault_storage from .. import sqlite_dao, utils from ..storage import sqlite @@ -49,12 +49,14 @@ def __init__(self, get_connection: Callable[[], sqlite3.Connection], vault_owner record_type_schema = sqlite_dao.TableSchema.load_schema( storage_types.StorageRecordType, 'id', owner_column=self.owner_column, owner_type=bytes) + nsf_schemas = sqlite_nsf_storage.nsf_table_schemas(self.owner_column, bytes) + sqlite_dao.verify_database(self.get_connection(), (settings_schema, team_schema, record_schema, shared_folder_schema, non_shared_data_schema, record_key_schema, shared_folder_key_schema, shared_folder_permission_schema, user_email_schema, folder_schema, folder_record_schema, breach_watch_record_schema, breach_watch_security_data_schema, - record_type_schema, notification_schema)) + record_type_schema, notification_schema) + nsf_schemas) self._settings_storage = sqlite.SqliteRecordStorage( self.get_connection, settings_schema, owner=self.vault_owner) @@ -93,6 +95,13 @@ def __init__(self, get_connection: Callable[[], sqlite3.Connection], vault_owner self._notifications = sqlite.SqliteEntityStorage( self.get_connection, notification_schema, owner=self.vault_owner) + self._nsf = sqlite_nsf_storage.SqliteNSFStorage( + self.get_connection, self.vault_owner, verify=False) + + @property + def nsf(self) -> nsf_vault_storage.INSFStorage: + return self._nsf + @property def user_settings(self): return self._settings_storage @@ -173,6 +182,7 @@ def clear(self): self._breach_watch_security_data.delete_all() self._user_emails.delete_all() self._notifications.delete_all() + self._nsf.clear_all() def close(self) -> None: pass diff --git a/keepersdk-package/src/keepersdk/vault/sync_down.py b/keepersdk-package/src/keepersdk/vault/sync_down.py index 9cf6c679..c8250b16 100644 --- a/keepersdk-package/src/keepersdk/vault/sync_down.py +++ b/keepersdk-package/src/keepersdk/vault/sync_down.py @@ -1,7 +1,8 @@ +import dataclasses import json from typing import List, Optional, Iterable -from . import vault_data, vault_storage +from . import vault_data, vault_storage, nsf_sync, nsf_data from .storage_types import ( StorageRecord, StorageSharedFolder, StorageRecordKey, StorageTeam, StorageSharedFolderKey, StorageNonSharedData, StorageSharedFolderPermission, StorageFolder, StorageFolderRecord, StorageRecordType, StorageKeyType, @@ -31,12 +32,18 @@ def decrypt_keeper_key(auth_context: keeper_auth.AuthContext, encrypted: bytes, raise ValueError(f'unsupported key type {key_type}') +@dataclasses.dataclass +class SyncDownResult: + vault: vault_data.RebuildTask + nsf: Optional[nsf_data.NSFRebuildTask] = None + + def sync_down_request(auth: keeper_auth.KeeperAuth, storage: vault_storage.IVaultStorage, *, pending_shares: Optional[IPendingSharePlugin]=None, audit_data: Optional[IAuditDataPlugin]=None, - sync_record_types: bool=False) -> vault_data.RebuildTask: + sync_record_types: bool=False) -> SyncDownResult: logger = utils.get_logger() user_settings = storage.user_settings.load() @@ -45,6 +52,7 @@ def sync_down_request(auth: keeper_auth.KeeperAuth, token = user_settings.continuation_token task: Optional[vault_data.RebuildTask] = None + nsf_task: Optional[nsf_data.NSFRebuildTask] = None done = False rq = SyncDown_pb2.SyncDownRequest() while not done: @@ -56,10 +64,19 @@ def sync_down_request(auth: keeper_auth.KeeperAuth, if response.cacheStatus == SyncDown_pb2.CLEAR: sync_record_types = True storage.clear() + nsf_task = nsf_data.NSFRebuildTask(True) logger.info('Syncing...') if task is None: task = vault_data.RebuildTask(response.cacheStatus == SyncDown_pb2.CLEAR) + nsf_storage = storage.nsf + if nsf_storage is not None: + if nsf_task is None: + nsf_task = nsf_data.NSFRebuildTask(False) + if not nsf_sync.try_apply_nsf_from_sync_down_proto(response, nsf_storage, nsf_task): + nsf_sync.try_apply_nsf_from_sync_down_json( + auth, nsf_storage, rq.continuationToken, nsf_task) + if len(response.removedRecords) > 0: record_uids = [utils.base64_url_encode(x) for x in response.removedRecords] task.add_records(record_uids) @@ -583,4 +600,4 @@ def to_record_type(rt: record_pb2.RecordType) -> Optional[StorageRecordType]: old_notifications = old_notifications[:to_delete] storage.notifications.delete_uids([x[0] for x in old_notifications]) - return task + return SyncDownResult(vault=task, nsf=nsf_task) diff --git a/keepersdk-package/src/keepersdk/vault/vault_online.py b/keepersdk-package/src/keepersdk/vault/vault_online.py index 03b11169..6d2504da 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_online.py +++ b/keepersdk-package/src/keepersdk/vault/vault_online.py @@ -3,7 +3,7 @@ from typing import Optional, Any, Dict from . import sync_down, vault_plugins -from . import vault_data, vault_storage +from . import vault_data, vault_storage, nsf_data, nsf_vault_storage from .. import utils from ..authentication import keeper_auth @@ -12,6 +12,11 @@ class VaultOnline(vault_plugins.IVaultData, keeper_auth.IKeeperAuth): def __init__(self, auth: keeper_auth.KeeperAuth, storage: vault_storage.IVaultStorage) -> None: super(VaultOnline, self).__init__() self._vault_data = vault_data.VaultData(auth.auth_context.client_key, storage) + self._nsf_data: Optional[nsf_data.NSFData] = None + nsf_storage = storage.nsf + if nsf_storage is not None: + self._nsf_data = nsf_data.NSFData( + nsf_storage, auth.auth_context) self._keeper_auth = auth self._auto_sync = False self._lock = threading.Lock() @@ -29,6 +34,14 @@ def vault_data(self)-> vault_data.VaultData: def keeper_auth(self) -> keeper_auth.KeeperAuth: return self._keeper_auth + @property + def nsf(self) -> nsf_vault_storage.INSFStorage: + return self._vault_data.storage.nsf + + @property + def nsf_data(self) -> Optional[nsf_data.NSFData]: + return self._nsf_data + @property def lock(self)-> threading.Lock: return self._lock @@ -95,13 +108,15 @@ def sync_down(self, force=False): if force: self._vault_data.storage.clear() - changes = sync_down.sync_down_request(self._keeper_auth, self._vault_data.storage, - sync_record_types=self._sync_record_types, - pending_shares=self.pending_share_plugin(), - audit_data=self.audit_data_plugin()) + result = sync_down.sync_down_request(self._keeper_auth, self._vault_data.storage, + sync_record_types=self._sync_record_types, + pending_shares=self.pending_share_plugin(), + audit_data=self.audit_data_plugin()) self.sync_requested = False self._sync_record_types = False - self._vault_data.rebuild_data(changes) + self._vault_data.rebuild_data(result.vault) + if self._nsf_data is not None: + self._nsf_data.rebuild_nsf(self._keeper_auth.auth_context) def _background_task(self): if self._keeper_auth.auth_context.enterprise_ec_public_key: diff --git a/keepersdk-package/src/keepersdk/vault/vault_storage.py b/keepersdk-package/src/keepersdk/vault/vault_storage.py index 0bb605f9..0111685d 100644 --- a/keepersdk-package/src/keepersdk/vault/vault_storage.py +++ b/keepersdk-package/src/keepersdk/vault/vault_storage.py @@ -1,7 +1,7 @@ import abc from ..storage.storage_types import IEntityReaderStorage, ILinkReaderStorage, IRecordStorage -from . import storage_types +from . import nsf_vault_storage, storage_types class IVaultStorage(abc.ABC): @@ -85,6 +85,11 @@ def breach_watch_security_data(self) -> IEntityReaderStorage[storage_types.Breac def notifications(self) -> IEntityReaderStorage[storage_types.StorageNotification, str]: pass + @property + @abc.abstractmethod + def nsf(self) -> nsf_vault_storage.INSFStorage: + pass + @abc.abstractmethod def clear(self) -> None: pass diff --git a/keepersdk-package/unit_tests/data_vault.py b/keepersdk-package/unit_tests/data_vault.py index 25f089d4..729a8db6 100644 --- a/keepersdk-package/unit_tests/data_vault.py +++ b/keepersdk-package/unit_tests/data_vault.py @@ -400,7 +400,7 @@ def get_connected_auth() -> keeper_auth.KeeperAuth: return keeper_auth.KeeperAuth(keeper_endpoint, auth_context) -TestClientVersion = 'c17.0.0' +TestClientVersion = 'c18.0.0' DefaultEnvironment = 'env.company.com' AccountUid = crypto.get_random_bytes(16) UserName = 'some_fake_user@company.com' diff --git a/keepersdk-package/unit_tests/test_nsf_management.py b/keepersdk-package/unit_tests/test_nsf_management.py new file mode 100644 index 00000000..be7b4d18 --- /dev/null +++ b/keepersdk-package/unit_tests/test_nsf_management.py @@ -0,0 +1,72 @@ +import unittest + +from keepersdk.vault import nsf_sync, memory_nsf_storage, nsf_management +from keepersdk.vault.nsf_data import NSFData +class _FakeVault: + def __init__(self, view: NSFData) -> None: + self.nsf_data = view + self.sync_requested = False + + def run_pending_jobs(self) -> None: + pass + + +class TestNsfManagement(unittest.TestCase): + def test_list_and_resolve_from_cache(self): + nsf = memory_nsf_storage.InMemoryNSFStorage() + nsf_sync.apply_nsf_data_dict( + nsf, + { + 'folders': [{'folderUid': 'F1', 'parentUid': '', 'data': '', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': '', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}], + 'folderRecords': [{'folderUid': 'F1', 'recordMetadata': {'recordUid': 'R1'}, + 'folderKeyEncryptionType': 0}], + 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, + 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], + 'recordData': [{'recordUid': 'R1', 'user': {'accountUid': 'a', 'username': 'u'}, + 'data': ''}], + }, + ) + view = NSFData(nsf) + vault = _FakeVault(view) + + rows = nsf_management.list_nsf_items(vault) + self.assertEqual(len(rows), 2) + self.assertEqual(nsf_management.resolve_nsf_folder_uid(vault, 'F1'), 'F1') + self.assertIsNone(nsf_management.resolve_nsf_record_uid(vault, 'missing')) + + def test_find_child_folder_and_build_removals(self): + nsf = memory_nsf_storage.InMemoryNSFStorage() + nsf_sync.apply_nsf_data_dict( + nsf, + { + 'folders': [ + {'folderUid': 'P1', 'parentUid': '', 'data': '', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': '', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}, + {'folderUid': 'C1', 'parentUid': 'P1', 'data': '', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': '', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}, + ], + 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, + 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], + 'recordData': [{'recordUid': 'R1', 'user': {'accountUid': 'a', 'username': 'u'}, + 'data': ''}], + 'folderRecords': [{'folderUid': 'P1', 'recordMetadata': {'recordUid': 'R1'}, + 'folderKeyEncryptionType': 0}], + }, + ) + view = NSFData(nsf) + vault = _FakeVault(view) + removals = nsf_management.build_nsf_record_removals( + vault, ['R1'], operation_type='owner-trash') + self.assertEqual(len(removals), 1) + self.assertEqual(removals[0]['record_uid'], 'R1') + + +if __name__ == '__main__': + unittest.main() diff --git a/keepersdk-package/unit_tests/test_nsf_sync.py b/keepersdk-package/unit_tests/test_nsf_sync.py new file mode 100644 index 00000000..78f5dea1 --- /dev/null +++ b/keepersdk-package/unit_tests/test_nsf_sync.py @@ -0,0 +1,202 @@ +import json +import os +import sqlite3 +import unittest + +from keepersdk import utils +from keepersdk.proto import SyncDown_pb2, folder_pb2 +from keepersdk.vault import nsf_data, nsf_sync, nsf_storage_types as nsf, memory_nsf_storage, sqlite_storage, storage_types + + +class TestNSFSync(unittest.TestCase): + def test_coerce_int_folder_usage_type_enum_name(self): + self.assertEqual(nsf_sync._coerce_int('UT_NORMAL'), int(folder_pb2.UT_NORMAL)) + self.assertEqual(nsf_sync._coerce_int(1), 1) + + def test_dict_to_folder_accepts_enum_name(self): + folder = nsf_sync._dict_to_folder({ + 'folderUid': 'F1', + 'parentUid': 'P', + 'data': 'd', + 'type': 'UT_NORMAL', + 'inheritUserPermissions': 1, + 'folderKey': 'k', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, + 'lastModified': 2, + }) + self.assertEqual(folder.folder_type, int(folder_pb2.UT_NORMAL)) + + def test_nsf_data_rebuild_after_sync(self): + conn = sqlite3.connect(':memory:') + vault = sqlite_storage.SqliteVaultStorage(lambda: conn, b'owner') + nsf = vault.nsf + view = nsf_data.NSFData(nsf) + nsf_sync.apply_nsf_data_dict( + nsf, + {'folders': [{'folderUid': 'F1', 'parentUid': 'P', 'data': 'd', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': 'k', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}]}, + ) + self.assertEqual(len(list(nsf.folders.get_all_entities())), 1) + view.rebuild_data(nsf_data.NSFRebuildTask(True)) + self.assertEqual(view.folder_count, 0) + conn.close() + + def test_apply_nsf_sync_down_sample(self): + root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) + path = os.path.join(root, 'nsf_sync_down.txt') + with open(path, 'r', encoding='utf-8') as f: + payload = json.load(f) + + conn = sqlite3.connect(':memory:') + owner = b'owner-test' + vault = sqlite_storage.SqliteVaultStorage(lambda: conn, owner) + nsf = vault.nsf + assert nsf is not None + nsf_sync.apply_nsf_from_full_sync_json(nsf, payload) + + inner = payload['nsfData'] + self.assertEqual(len(list(nsf.folders.get_all_entities())), len(inner['folders'])) + self.assertEqual(len(list(nsf.records.get_all_entities())), len(inner['records'])) + + f0 = nsf.folders.get_entity(inner['folders'][0]['folderUid']) + assert f0 is not None + self.assertEqual(f0.owner_username, inner['folders'][0]['ownerInfo']['username']) + + conn.close() + + def test_revoked_and_removed_sync_down_delete_main_tables(self): + """Revoked/removed sync-down payloads delete rows from main tables only.""" + nsf = memory_nsf_storage.InMemoryNSFStorage() + nsf_sync.apply_nsf_data_dict( + nsf, + { + 'folders': [{'folderUid': 'F1', 'parentUid': '', 'data': 'd', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': 'k', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}], + 'folderAccesses': [{'folderUid': 'F1', 'accessTypeUid': 'A1', 'accessType': 1, + 'accessRoleType': 0, 'inherited': False, 'hidden': False, + 'deniedAccess': False, 'dateCreated': 1, 'lastModified': 2}], + 'folderRecords': [{'folderUid': 'F1', 'recordMetadata': {'recordUid': 'R1'}, + 'folderKeyEncryptionType': 0}], + 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, + 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], + 'recordLinks': [{'parentRecordUid': 'P1', 'childRecordUid': 'C1', + 'recordKey': 'k', 'revision': 1}], + 'recordAccesses': [{'recordUid': 'R1', 'accessTypeUid': 'A2', 'accessType': 1, + 'accessRoleType': 0, 'owner': True, 'inherited': False, + 'hidden': False, 'deniedAccess': False, + 'canViewTitle': True, 'canEdit': True, 'canView': True, + 'canListAccess': True, 'canUpdateAccess': True, + 'canDelete': True, 'canChangeOwnership': True, + 'canRequestAccess': True, 'canApproveAccess': True, + 'dateCreated': 1, 'lastModified': 2}], + }, + ) + self.assertIsNotNone(nsf.folders.get_entity('F1')) + self.assertEqual(len(list(nsf.folder_accesses.get_links_by_subject('F1'))), 1) + self.assertEqual(len(list(nsf.folder_records.get_links_by_subject('F1'))), 1) + self.assertEqual(len(list(nsf.record_links.get_all_links())), 1) + self.assertEqual(len(list(nsf.record_accesses.get_links_by_subject('R1'))), 1) + + nsf_sync.apply_nsf_data_dict( + nsf, + { + 'revokedFolderAccesses': [{'folderUid': 'F1', 'actorUid': 'A1'}], + 'removedFolderRecords': [{'folderUid': 'F1', 'recordUid': 'R1'}], + 'removedRecordLinks': [{'parentRecordUid': 'P1', 'childRecordUid': 'C1'}], + 'revokedRecordAccesses': [{'recordUid': 'R1', 'actorUid': 'A2'}], + 'removedFolders': ['F1'], + }, + ) + self.assertIsNone(nsf.folders.get_entity('F1')) + self.assertEqual(len(list(nsf.folder_accesses.get_links_by_subject('F1'))), 1) + self.assertEqual(len(list(nsf.folder_records.get_links_by_subject('F1'))), 0) + self.assertEqual(len(list(nsf.record_links.get_all_links())), 0) + self.assertEqual(len(list(nsf.record_accesses.get_links_by_subject('R1'))), 0) + + def test_removed_folder_records_proto_uses_folder_record_key_fields(self): + """Proto removedFolderRecords are FolderRecordKey (snake_case fields).""" + storage = memory_nsf_storage.InMemoryNSFStorage() + folder_uid = utils.generate_uid() + record_uid = utils.generate_uid() + storage.folder_records.put_links([ + nsf.NSFFolderRecord(folder_uid=folder_uid, record_uid=record_uid), + ]) + self.assertEqual(len(list(storage.folder_records.get_links_by_subject(folder_uid))), 1) + + nsf_msg = SyncDown_pb2.KeeperDriveData() + removed = nsf_msg.removedFolderRecords.add() + removed.folder_uid = utils.base64_url_decode(folder_uid) + removed.record_uid = utils.base64_url_decode(record_uid) + nsf_sync.apply_nsf_proto_message(nsf_msg, storage, None) + + self.assertEqual(len(list(storage.folder_records.get_links_by_subject(folder_uid))), 0) + + def test_store_record_data_before_records_row_creates_stub(self): + """recordData may arrive in an earlier sync chunk than records metadata.""" + nsf = memory_nsf_storage.InMemoryNSFStorage() + nsf_sync.apply_nsf_data_dict( + nsf, + {'recordData': [{'recordUid': 'R1', 'data': 'payload-data'}]}, + ) + row = nsf.records.get_entity('R1') + self.assertIsNotNone(row) + assert row is not None + self.assertEqual(row.data, 'payload-data') + + nsf_sync.apply_nsf_data_dict( + nsf, + {'records': [{'recordUid': 'R1', 'revision': 2, 'version': 3, 'shared': False, + 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}]}, + ) + row = nsf.records.get_entity('R1') + assert row is not None + self.assertEqual(row.data, 'payload-data') + self.assertEqual(row.revision, 2) + + def test_store_records_before_record_data_preserves_payload(self): + """recordData must apply after records exist in storage.""" + nsf = memory_nsf_storage.InMemoryNSFStorage() + nsf_sync.apply_nsf_data_dict( + nsf, + { + 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, + 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], + 'recordData': [{'recordUid': 'R1', 'data': 'payload-data'}], + }, + ) + row = nsf.records.get_entity('R1') + self.assertIsNotNone(row) + assert row is not None + self.assertEqual(row.data, 'payload-data') + + def test_vault_clear_wipes_nsf_tables(self): + """``SqliteVaultStorage.clear`` clears vault and NSF rows in the same database.""" + conn = sqlite3.connect(':memory:') + owner = b'vault-owner' + vault = sqlite_storage.SqliteVaultStorage(lambda: conn, owner) + nsf = vault.nsf + + vault.user_settings.store(storage_types.UserSettings(continuation_token=b'x')) + nsf_sync.apply_nsf_data_dict( + nsf, + {'folders': [{'folderUid': 'F1', 'parentUid': 'P', 'data': 'd', 'type': 1, + 'inheritUserPermissions': 0, 'folderKey': 'k', + 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, + 'dateCreated': 1, 'lastModified': 2}]}, + ) + self.assertIsNotNone(nsf.folders.get_entity('F1')) + + vault.clear() + self.assertIsNone(vault.user_settings.load()) + self.assertIsNone(nsf.folders.get_entity('F1')) + + conn.close() + + +if __name__ == '__main__': + unittest.main() From 2428d9c7b384dba6bf12158acd93d71f4d51db78 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Sat, 20 Jun 2026 01:49:10 +0530 Subject: [PATCH 21/23] Enterprise commands bug fixes (#180) --- .../src/keepercli/commands/enterprise_role.py | 2 +- .../src/keepercli/commands/enterprise_user.py | 6 +- .../keepercli/commands/enterprise_utils.py | 3 +- .../keepersdk/enterprise/batch_management.py | 23 +- .../src/keepersdk/vault/nsf_sync.py | 485 ------------------ .../keepersdk/vault/share_management_utils.py | 34 +- .../src/keepersdk/vault/shares_management.py | 2 +- .../src/keepersdk/vault/sync_down.py | 4 +- .../unit_tests/test_nsf_management.py | 72 --- keepersdk-package/unit_tests/test_nsf_sync.py | 177 +------ 10 files changed, 38 insertions(+), 770 deletions(-) delete mode 100644 keepersdk-package/unit_tests/test_nsf_management.py diff --git a/keepercli-package/src/keepercli/commands/enterprise_role.py b/keepercli-package/src/keepercli/commands/enterprise_role.py index b366c9f0..7a79529b 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_role.py +++ b/keepercli-package/src/keepercli/commands/enterprise_role.py @@ -352,7 +352,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: parent_node = enterprise_utils.NodeUtils.resolve_single_node(context.enterprise_data, kwargs.get('parent')) parent_id = parent_node.node_id else: - parent_id = context.enterprise_data.root_node.node_id + parent_id = role_list[0].node_id new_user_inherit: Optional[bool] = None visible_below: Optional[bool] = None diff --git a/keepercli-package/src/keepercli/commands/enterprise_user.py b/keepercli-package/src/keepercli/commands/enterprise_user.py index b6a2044b..1c77a374 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_user.py +++ b/keepercli-package/src/keepercli/commands/enterprise_user.py @@ -540,12 +540,10 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if not emails: raise base.CommandError('No email(s) provided') - parent_id: Optional[int] + parent_id: Optional[int] = None if kwargs.get('parent'): parent_node = enterprise_utils.NodeUtils.resolve_single_node(context.enterprise_data, kwargs.get('parent')) parent_id = parent_node.node_id - else: - parent_id = context.enterprise_data.root_node.node_id users = enterprise_utils.UserUtils.resolve_existing_users(context.enterprise_data, emails) if not users: @@ -603,7 +601,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: if parent_id or full_name or job_title: users_to_update = [enterprise_management.UserEdit( - enterprise_user_id=x.enterprise_user_id, node_id=parent_id, full_name=full_name, job_title=job_title) + enterprise_user_id=x.enterprise_user_id, node_id=parent_id or x.node_id, full_name=full_name, job_title=job_title) for x in users] batch.modify_users(to_update=users_to_update) diff --git a/keepercli-package/src/keepercli/commands/enterprise_utils.py b/keepercli-package/src/keepercli/commands/enterprise_utils.py index 41c6df9e..81a21471 100644 --- a/keepercli-package/src/keepercli/commands/enterprise_utils.py +++ b/keepercli-package/src/keepercli/commands/enterprise_utils.py @@ -203,7 +203,7 @@ def enforcement_value_from_file(filepath: str) -> str: def parse_enforcements(enforcement_names: Any) -> Tuple[Dict[str, Any], List[str]]: enforcements: Dict[str, Any] = {} errors: List[str] = [] - if isinstance(enforcements, str): + if isinstance(enforcement_names, str): enforcement_names = [enforcement_names] file_prefix = '$FILE=' for enf in enforcement_names: @@ -232,6 +232,7 @@ def parse_enforcements(enforcement_names: Any) -> Tuple[Dict[str, Any], List[str else: errors.append(f'Enforcement {key} is skipped. Expected format: KEY:$FILE=') continue + enforcements[key] = enforcement_value return enforcements, errors diff --git a/keepersdk-package/src/keepersdk/enterprise/batch_management.py b/keepersdk-package/src/keepersdk/enterprise/batch_management.py index cd5255f5..81d95a68 100644 --- a/keepersdk-package/src/keepersdk/enterprise/batch_management.py +++ b/keepersdk-package/src/keepersdk/enterprise/batch_management.py @@ -851,18 +851,21 @@ def _to_role_enforcements(self) -> List[Dict[str, Any]]: if enforcement_type: enforcement_value = self._to_enforcement_value(enforcement_type, enforcement_value) rq = { + 'command': 'role_enforcement_update' if existing_enforcement else 'role_enforcement_add', 'role_id': role_id, 'enforcement': enforcement } if enforcement_value is not None: - rq['command'] = 'role_enforcement_update' if existing_enforcement else 'role_enforcement_add' - if not isinstance(enforcement_value, bool): - rq['value'] = enforcement_value + if isinstance(enforcement_value, bool) and existing_enforcement: + self.logger.warning('Enforcement \"%s\" is already set for role %d. Skipping', enforcement, role_id) + continue + rq['value'] = enforcement_value else: if existing_enforcement: rq['command'] = 'role_enforcement_remove' else: continue + requests.append(rq) except Exception as e: self.logger.warning(f'Role Enforcement {action.name}: Role ID = \"{role_enforcement.role_id}\", ' f'Enforcement = \"{role_enforcement.name}\": {e}') @@ -1203,9 +1206,7 @@ def _to_enforcement_value(self, enforcement_type: str, enforcement_value: Any) - if enforcement_value.lower() in {'true', 't', '1'}: enforcement_value = True elif enforcement_value.lower() in {'false', 'f', '0'}: - enforcement_value = False - if enforcement_value is False: - enforcement_value = None + enforcement_value = None else: raise Exception(f'{enforcement_type} \"{enforcement_value}\" is invalid') elif enforcement_type == 'long': @@ -1267,7 +1268,8 @@ def _to_enforcement_value(self, enforcement_type: str, enforcement_value: Any) - raise Exception(f'IP address range \"{range_str}\" not valid') enforcement_value = ','.join(ip_ranges) elif enforcement_type == 'record_types': - if self._record_types is None: + self._load_record_types() + if not self._record_types: raise Exception('Record types could not be loaded') record_types: Dict[str, List[int]] = { 'std': [], @@ -1275,7 +1277,7 @@ def _to_enforcement_value(self, enforcement_type: str, enforcement_value: Any) - } rtypes = [x.strip().lower() for x in enforcement_value.split(',')] for rtype in rtypes: - if rtype is self._record_types: + if rtype in self._record_types: rt_id, rt_scope = self._record_types[rtype] if rt_scope: if rt_scope == record_pb2.RT_STANDARD: @@ -1311,7 +1313,7 @@ def _to_enforcement_value(self, enforcement_type: str, enforcement_value: Any) - return enforcement_value def _load_record_types(self): - if self._record_types: + if self._record_types is not None: return rt_rq = record_pb2.RecordTypesRequest() rt_rq.standard = True @@ -1325,8 +1327,7 @@ def _load_record_types(self): rto = json.loads(rti.content) if '$id' in rto: record_type = rto['$id'].lower() - if rti.scope == record_pb2.RT_STANDARD and rti.scope == record_pb2.RT_ENTERPRISE: - self._record_types[record_type] = (rti.recordTypeId, rti.scope) + self._record_types[record_type] = (rti.recordTypeId, rti.scope) except Exception: pass diff --git a/keepersdk-package/src/keepersdk/vault/nsf_sync.py b/keepersdk-package/src/keepersdk/vault/nsf_sync.py index ab6bddd1..78e03c2d 100644 --- a/keepersdk-package/src/keepersdk/vault/nsf_sync.py +++ b/keepersdk-package/src/keepersdk/vault/nsf_sync.py @@ -27,30 +27,6 @@ ) -def _coerce_int(value: Any, default: int = 0) -> int: - if value is None or value == '': - return default - if isinstance(value, bool): - return int(value) - if isinstance(value, int): - return value - if isinstance(value, float): - return int(value) - if isinstance(value, str): - stripped = value.strip() - if stripped.lstrip('-').isdigit(): - return int(stripped) - for enum_type in _PROTO_ENUM_TYPES: - try: - return int(enum_type.Value(stripped)) - except (ValueError, AttributeError): - pass - if stripped in enum_type.keys(): - return int(enum_type.Value(stripped)) - return default - return default - - def _wire_b64(data: bytes) -> str: return utils.base64_url_encode(data) if data else '' @@ -79,25 +55,6 @@ def _j(value: Any) -> str: return json.dumps(value, separators=(',', ':')) -def _folder_uid(item: Union[str, Mapping[str, Any]]) -> str: - if isinstance(item, str): - return item - return str(item.get('folderUid') or item.get('folder_uid') or '') - - -def _access_uid(x: Mapping[str, Any], *, proto_actor: bool = False) -> str: - if proto_actor: - return str(x.get('actorUid') or x.get('accessTypeUid') or '') - return str(x.get('actorUid') or x.get('accessTypeUid') or '') - - -def extract_nsf_data(sync_payload: Mapping[str, Any]) -> Optional[Dict[str, Any]]: - raw = sync_payload.get('keeperDriveData') - if isinstance(raw, dict): - return raw - return None - - def try_apply_nsf_from_sync_down_proto( response: SyncDown_pb2.SyncDownResponse, nsf_storage: INSFStorage, @@ -111,36 +68,6 @@ def try_apply_nsf_from_sync_down_proto( return True -def try_apply_nsf_from_sync_down_json( - auth: Any, - nsf_storage: INSFStorage, - continuation_token: bytes, - task: Optional['NSFRebuildTask'] = None) -> bool: - logger = utils.get_logger() - try: - rq: Dict[str, Any] = {'continuationToken': utils.base64_url_encode(continuation_token)} - rs = auth.execute_router_json('vault/sync_down', rq) - except Exception as e: - logger.debug('NSF JSON sync_down skipped: %s', e) - return False - if not isinstance(rs, dict): - return False - inner = extract_nsf_data(rs) - if inner is None: - return False - apply_nsf_data_dict(nsf_storage, inner, task) - return True - - -def apply_nsf_from_full_sync_json( - nsf_storage: INSFStorage, - sync_payload: Mapping[str, Any], - task: Optional['NSFRebuildTask'] = None) -> None: - inner = extract_nsf_data(sync_payload) - if inner is not None: - apply_nsf_data_dict(nsf_storage, inner, task) - - def apply_nsf_proto_message( nsf_msg: SyncDown_pb2.KeeperDriveData, nsf_storage: INSFStorage, @@ -163,28 +90,6 @@ def apply_nsf_proto_message( _store_optional_extras_proto(nsf_msg, nsf_storage, task) -def apply_nsf_data_dict( - nsf_storage: INSFStorage, - nsf_data: Mapping[str, Any], - task: Optional['NSFRebuildTask'] = None) -> None: - _process_removed_folders_dict(nsf_data, nsf_storage, task) - _process_removed_folder_records_dict(nsf_data, nsf_storage, task) - _process_removed_record_links_dict(nsf_data, nsf_storage, task) - _store_folders_dict(nsf_data, nsf_storage, task) - _store_folder_keys_dict(nsf_data, nsf_storage) - _store_records_dict(nsf_data, nsf_storage, task) - _store_record_data_dict(nsf_data, nsf_storage, task) - _store_folder_records_dict(nsf_data, nsf_storage, task) - _process_revoked_folder_accesses_dict(nsf_data, nsf_storage, task) - _store_folder_accesses_dict(nsf_data, nsf_storage) - _process_revoked_record_accesses_dict(nsf_data, nsf_storage, task) - _store_record_accesses_dict(nsf_data, nsf_storage, task) - _store_record_links_dict(nsf_data, nsf_storage, task) - _store_folder_sharing_states_dict(nsf_data, nsf_storage) - _store_record_sharing_states_dict(nsf_data, nsf_storage) - _store_optional_extras_dict(nsf_data, nsf_storage, task) - - def _process_removed_folders_proto( nsf_msg: SyncDown_pb2.KeeperDriveData, storage: INSFStorage, @@ -421,278 +326,6 @@ def _store_optional_extras_proto( _replace_json_lists(storage, chunk_payload) -def _process_removed_folders_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - removed = [_folder_uid(y) for y in (d.get('removedFolders') or []) if _folder_uid(y)] - if not removed: - return - storage.folder_keys.delete_links_by_subjects(removed) - storage.folder_records.delete_links_by_subjects(removed) - storage.folders.delete_uids(removed) - if task: - task.add_folders(removed) - - -def _process_removed_folder_records_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - links: List[Tuple[str, str]] = [] - key_links: List[Tuple[str, str]] = [] - for x in d.get('removedFolderRecords') or []: - if not isinstance(x, dict): - continue - fu = _folder_uid(x) - ru = str(x.get('recordUid') or x.get('record_uid') or '') - if fu and ru: - links.append((fu, ru)) - key_links.append((ru, fu)) - if links: - storage.folder_records.delete_links(links) - if key_links: - storage.record_keys.delete_links(key_links) - if task and links: - task.add_records((ru for _, ru in links)) - - -def _process_removed_record_links_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - for x in d.get('removedRecordLinks') or []: - if not isinstance(x, dict): - continue - child_uid = str(x.get('childRecordUid', '')) - storage.record_links.delete_links([( - str(x.get('parentRecordUid', '')), - child_uid, - )]) - if task and child_uid: - task.add_record(child_uid) - - -def _process_revoked_folder_accesses_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - links: List[Tuple[str, str]] = [] - for x in d.get('revokedFolderAccesses') or []: - if not isinstance(x, dict): - continue - fu = str(x.get('folderUid', '')) - au = _access_uid(x) - if fu and au: - links.append((fu, au)) - if links: - storage.folder_accesses.delete_links(links) - if task: - task.add_folders((fu for fu, _ in links)) - - -def _process_revoked_record_accesses_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - links: List[Tuple[str, str]] = [] - for x in d.get('revokedRecordAccesses') or []: - if not isinstance(x, dict): - continue - ru = str(x.get('recordUid', '')) - au = _access_uid(x) - if ru and au: - links.append((ru, au)) - if links: - storage.record_accesses.delete_links(links) - if task: - task.add_records((ru for ru, _ in links)) - - -def _store_folders_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - folders = [_dict_to_folder(x) for x in d.get('folders') or [] if isinstance(x, dict)] - if folders: - storage.folders.put_entities(folders) - if task: - task.add_folders((f.folder_uid for f in folders)) - - -def _store_folder_keys_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: - keys = [_dict_to_folder_key(x) for x in d.get('folderKeys') or [] if isinstance(x, dict)] - if keys: - storage.folder_keys.put_links(keys) - - -def _store_record_data_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - updated: List[nsf.NSFRecord] = [] - for x in d.get('recordData') or []: - if not isinstance(x, dict): - continue - record_uid = str(x.get('recordUid', '')) - if not record_uid: - continue - existing = storage.records.get_entity(record_uid) - if existing is None: - existing = nsf.NSFRecord(record_uid=record_uid) - updated.append(dataclasses.replace(existing, data=str(x.get('data', '')))) - if task: - task.add_record(record_uid) - if updated: - storage.records.put_entities(updated) - - -def _store_folder_records_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - folder_records: List[nsf.NSFFolderRecord] = [] - record_keys: List[nsf.NSFRecordKey] = [] - for x in d.get('folderRecords') or []: - if not isinstance(x, dict): - continue - folder_uid = str(x.get('folderUid', '')) - md = x.get('recordMetadata') or {} - record_uid = str(md.get('recordUid', '')) - if not folder_uid or not record_uid: - continue - folder_records.append(nsf.NSFFolderRecord(folder_uid=folder_uid, record_uid=record_uid)) - enc_key = str(md.get('encryptedRecordKey', '')) - if enc_key: - record_keys.append(nsf.NSFRecordKey( - record_uid=record_uid, - folder_uid=folder_uid, - record_key=enc_key, - record_key_type=_coerce_int(md.get('encryptedRecordKeyType'), 0), - folder_key_encryption_type=_coerce_int(x.get('folderKeyEncryptionType'), 0), - )) - if folder_records: - storage.folder_records.put_links(folder_records) - if task: - task.add_records((r.record_uid for r in folder_records)) - if record_keys: - storage.record_keys.put_links(record_keys) - - -def _store_records_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - rows: List[nsf.NSFRecord] = [] - for x in d.get('records') or []: - if not isinstance(x, dict): - continue - record_uid = str(x.get('recordUid', '')) - existing = storage.records.get_entity(record_uid) - rows.append(nsf.NSFRecord( - record_uid=record_uid, - revision=_coerce_int(x.get('revision'), 0), - version=_coerce_int(x.get('version'), 0), - shared=bool(x.get('shared')), - client_modified_time=_coerce_int(x.get('clientModifiedTime'), 0), - file_size=_coerce_int(x.get('fileSize'), 0), - thumbnail_size=_coerce_int(x.get('thumbnailSize'), 0), - data=existing.data if existing else '', - )) - if rows: - storage.records.put_entities(rows) - if task: - task.add_records((r.record_uid for r in rows)) - - -def _store_folder_accesses_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: - rows = [_dict_to_folder_access(x) for x in d.get('folderAccesses') or [] if isinstance(x, dict)] - if rows: - storage.folder_accesses.put_links(rows) - - -def _store_record_accesses_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask'] = None) -> None: - rows = [_dict_to_record_access(x) for x in d.get('recordAccesses') or [] if isinstance(x, dict)] - if rows: - storage.record_accesses.put_links(rows) - if task: - task.add_records((r.record_uid for r in rows)) - - -def _store_record_links_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - rows = [_dict_to_record_link(x) for x in d.get('recordLinks') or [] if isinstance(x, dict)] - if rows: - storage.record_links.put_links(rows) - if task: - task.add_records((r.child_record_uid for r in rows)) - - -def _store_folder_sharing_states_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: - rows: List[nsf.NSFFolderSharingState] = [] - for x in d.get('folderSharingState') or []: - if not isinstance(x, dict): - continue - rows.append(nsf.NSFFolderSharingState( - folder_uid=str(x.get('folderUid', '')), - shared=bool(x.get('shared')), - count=_coerce_int(x.get('count'), 0), - )) - if rows: - storage.folder_sharing_states.put_entities(rows) - - -def _store_record_sharing_states_dict(d: Mapping[str, Any], storage: INSFStorage) -> None: - rows: List[nsf.NSFRecordSharingState] = [] - for x in d.get('recordSharingStates') or []: - if not isinstance(x, dict): - continue - rows.append(nsf.NSFRecordSharingState( - record_uid=str(x.get('recordUid', '')), - is_directly_shared=bool(x.get('isDirectlyShared')), - is_indirectly_shared=bool(x.get('isIndirectlyShared')), - is_shared=bool(x.get('isShared')), - )) - if rows: - storage.record_sharing_states.put_entities(rows) - - -def _store_optional_extras_dict( - d: Mapping[str, Any], - storage: INSFStorage, - task: Optional['NSFRebuildTask']) -> None: - nsd = [_dict_to_non_shared(x) for x in d.get('nonSharedData') or [] if isinstance(x, dict)] - if nsd: - storage.non_shared_data.put_entities(nsd) - if task: - task.add_records((r.record_uid for r in nsd)) - bw = [_dict_to_bw_record(x) for x in d.get('breachWatchRecords') or [] if isinstance(x, dict)] - if bw: - storage.breach_watch_records.put_entities(bw) - if task: - task.add_records((r.record_uid for r in bw)) - ss = [_dict_to_security_score(x) for x in d.get('securityScoreData') or [] if isinstance(x, dict)] - if ss: - storage.security_score_data.put_entities(ss) - if task: - task.add_records((r.record_uid for r in ss)) - bws = [_dict_to_bw_security(x) for x in d.get('breachWatchSecurityData') or [] if isinstance(x, dict)] - if bws: - storage.breach_watch_security_data.put_entities(bws) - if task: - task.add_records((r.record_uid for r in bws)) - chunk_payload: Dict[str, Any] = { - CHUNK_RECORD_ROTATION: d.get('recordRotationData'), - CHUNK_RAW_DAG: d.get('rawDagData'), - } - _replace_json_lists(storage, chunk_payload) - - def _replace_json_lists(storage: INSFStorage, d: Mapping[str, Any]) -> None: _replace_chunk_group(storage, CHUNK_RECORD_ROTATION, d.get(CHUNK_RECORD_ROTATION)) _replace_chunk_group(storage, CHUNK_RAW_DAG, d.get(CHUNK_RAW_DAG)) @@ -824,124 +457,6 @@ def _proto_bw_security(bws: SyncDown_pb2.BreachWatchSecurityData) -> nsf.NSFBrea ) -def _dict_to_folder(x: Dict[str, Any]) -> nsf.NSFFolder: - oi = x.get('ownerInfo') or {} - return nsf.NSFFolder( - folder_uid=str(x.get('folderUid', '')), - parent_uid=str(x.get('parentUid', '')), - data=str(x.get('data', '')), - folder_type=_coerce_int(x.get('type'), 0), - inherit_user_permissions=_coerce_int(x.get('inheritUserPermissions'), 0), - folder_key=str(x.get('folderKey', '')), - owner_account_uid=str(oi.get('accountUid', '')), - owner_username=str(oi.get('username', '')), - date_created=_coerce_int(x.get('dateCreated'), 0), - last_modified=_coerce_int(x.get('lastModified'), 0), - ) - - -def _dict_to_folder_key(x: Dict[str, Any]) -> nsf.NSFFolderKey: - return nsf.NSFFolderKey( - folder_uid=str(x.get('folderUid', '')), - parent_uid=str(x.get('parentUid', '')), - folder_key=str(x.get('folderKey', '')), - encrypted_by=_coerce_int(x.get('encryptedBy'), 0), - ) - - -def _dict_to_folder_access(x: Dict[str, Any]) -> nsf.NSFFolderAccess: - fk = x.get('folderKey') - enc, kt = '', 0 - if isinstance(fk, dict): - enc = str(fk.get('encryptedKey', '')) - kt = _coerce_int(fk.get('encryptedKeyType'), 0) - elif fk is not None and fk != '': - enc = str(fk) - return nsf.NSFFolderAccess( - folder_uid=str(x.get('folderUid', '')), - access_type_uid=str(x.get('accessTypeUid', '')), - access_type=_coerce_int(x.get('accessType'), 0), - access_role_type=_coerce_int(x.get('accessRoleType'), 0), - folder_key_encrypted=enc, - folder_key_type=kt, - inherited=bool(x.get('inherited')), - hidden=bool(x.get('hidden')), - denied_access=bool(x.get('deniedAccess')), - permissions_json=_j(x.get('permissions')), - tla_properties_json=_j(x.get('tlaProperties')), - date_created=_coerce_int(x.get('dateCreated'), 0), - last_modified=_coerce_int(x.get('lastModified'), 0), - ) - - -def _dict_to_non_shared(x: Dict[str, Any]) -> nsf.NSFNonSharedData: - return nsf.NSFNonSharedData( - record_uid=str(x.get('recordUid', '')), - data=str(x.get('data', '')), - ) - - -def _dict_to_record_access(x: Dict[str, Any]) -> nsf.NSFRecordAccess: - return nsf.NSFRecordAccess( - record_uid=str(x.get('recordUid', '')), - access_type_uid=str(x.get('accessTypeUid', '')), - access_type=_coerce_int(x.get('accessType'), 0), - access_role_type=_coerce_int(x.get('accessRoleType'), 0), - owner=bool(x.get('owner')), - inherited=bool(x.get('inherited')), - hidden=bool(x.get('hidden')), - denied_access=bool(x.get('deniedAccess')), - can_view_title=bool(x.get('canViewTitle')), - can_edit=bool(x.get('canEdit')), - can_view=bool(x.get('canView')), - can_list_access=bool(x.get('canListAccess')), - can_update_access=bool(x.get('canUpdateAccess')), - can_delete=bool(x.get('canDelete')), - can_change_ownership=bool(x.get('canChangeOwnership')), - can_request_access=bool(x.get('canRequestAccess')), - can_approve_access=bool(x.get('canApproveAccess')), - date_created=_coerce_int(x.get('dateCreated'), 0), - last_modified=_coerce_int(x.get('lastModified'), 0), - tla_properties_json=_j(x.get('tlaProperties')), - ) - - -def _dict_to_record_link(x: Dict[str, Any]) -> nsf.NSFRecordLink: - return nsf.NSFRecordLink( - parent_record_uid=str(x.get('parentRecordUid', '')), - child_record_uid=str(x.get('childRecordUid', '')), - record_key=str(x.get('recordKey', '')), - revision=_coerce_int(x.get('revision'), 0), - ) - - -def _dict_to_bw_record(x: Dict[str, Any]) -> nsf.NSFBreachWatchRecord: - return nsf.NSFBreachWatchRecord( - record_uid=str(x.get('recordUid', '')), - data=str(x.get('data', '')), - type=_coerce_int(x.get('type'), 0), - scanned_by=str(x.get('scannedBy', '')), - revision=_coerce_int(x.get('revision'), 0), - scanned_by_account_uid=str(x.get('scannedByAccountUid', '')), - ) - - -def _dict_to_security_score(x: Dict[str, Any]) -> nsf.NSFSecurityScoreData: - return nsf.NSFSecurityScoreData( - record_uid=str(x.get('recordUid', '')), - data=str(x.get('data', '')), - revision=_coerce_int(x.get('revision'), 0), - ) - - -def _dict_to_bw_security(x: Dict[str, Any]) -> nsf.NSFBreachWatchSecurityData: - return nsf.NSFBreachWatchSecurityData( - record_uid=str(x.get('recordUid', '')), - revision=_coerce_int(x.get('revision'), 0), - removed=bool(x.get('removed')), - ) - - def load_list_chunks(storage: INSFStorage, group: str) -> List[Any]: """Decode ``NSFListChunk`` rows for ``group`` back into Python values.""" out: List[Any] = [] diff --git a/keepersdk-package/src/keepersdk/vault/share_management_utils.py b/keepersdk-package/src/keepersdk/vault/share_management_utils.py index 31608ea1..13c8223a 100644 --- a/keepersdk-package/src/keepersdk/vault/share_management_utils.py +++ b/keepersdk-package/src/keepersdk/vault/share_management_utils.py @@ -341,8 +341,8 @@ def _build_record_details_request(record_uids: set) -> record_pb2.GetRecordDataW def _load_records_in_batches( vault: vault_online.VaultOnline, - record_set: set, - record_keys: dict, + record_set: Set[str], + record_keys: Dict[str, bytes], key_encrypter_uid: str, ) -> Set[str]: @@ -367,7 +367,7 @@ def _load_records_in_batches( return loaded -def _process_record_owner_key(record_data, record_uid: str, record_keys: dict): +def _process_record_owner_key(record_data: record_pb2.RecordData, record_uid: str, record_keys: Dict[str, bytes]): if record_data.recordUid and record_data.recordKey: owner_id = utils.base64_url_encode(record_data.recordUid) @@ -380,8 +380,8 @@ def _process_record_owner_key(record_data, record_uid: str, record_keys: dict): def _process_record_batch( vault: vault_online.VaultOnline, - response, - record_keys: dict, + response: record_pb2.GetRecordDataWithAccessInfoResponse, + record_keys: Dict[str, bytes], record_set: set, key_encrypter_uid: str, ) -> Set[str]: @@ -410,7 +410,7 @@ def _process_record_batch( return _persist_loaded_records(vault, batch_records, key_encrypter_uid) -def _create_record_dict(record_uid: str, record_data, record_key: bytes, version: int) -> dict: +def _create_record_dict(record_uid: str, record_data: record_pb2.RecordData, record_key: bytes, version: int) -> Dict: """Create record dictionary from API data.""" return { 'record_uid': record_uid, @@ -423,7 +423,7 @@ def _create_record_dict(record_uid: str, record_data, record_key: bytes, version } -def _decrypt_record_data(record_data, record_key: bytes, version: int) -> bytes: +def _decrypt_record_data(record_data: record_pb2.RecordData, record_key: bytes, version: int) -> bytes: data_decoded = utils.base64_url_decode(record_data.encryptedRecordData) @@ -433,7 +433,7 @@ def _decrypt_record_data(record_data, record_key: bytes, version: int) -> bytes: return crypto.decrypt_aes_v2(data_decoded, record_key) -def _process_v2_extra_data(record: dict, record_data, record_key: bytes): +def _process_v2_extra_data(record: Dict, record_data: record_pb2.RecordData, record_key: bytes): if record_data.encryptedExtraData: record['extra'] = record_data.encryptedExtraData @@ -441,7 +441,7 @@ def _process_v2_extra_data(record: dict, record_data, record_key: bytes): record['extra_unencrypted'] = crypto.decrypt_aes_v1(extra_decoded, record_key) -def _collect_typed_record_ref_uids(record: dict) -> Set[str]: +def _collect_typed_record_ref_uids(record: Dict) -> Set[str]: version = record.get('version', 0) if version < V3_VERSION or version == V4_VERSION: return set() @@ -478,7 +478,7 @@ def _encrypted_field_to_bytes(value: Union[str, bytes]) -> bytes: return b'' -def _dict_to_storage_record(record: dict) -> storage_types.StorageRecord: +def _dict_to_storage_record(record: Dict) -> storage_types.StorageRecord: sr = storage_types.StorageRecord() sr.record_uid = record['record_uid'] sr.revision = record.get('revision', 0) @@ -493,7 +493,7 @@ def _dict_to_storage_record(record: dict) -> storage_types.StorageRecord: def _ensure_record_key_link( vault: vault_online.VaultOnline, - record: dict, + record: Dict, key_encrypter_uid: str, ) -> None: record_uid = record['record_uid'] @@ -522,7 +522,7 @@ def _ensure_record_key_link( def _persist_loaded_records( vault: vault_online.VaultOnline, - records: List[dict], + records: List[Dict], key_encrypter_uid: str, ) -> Set[str]: if not records: @@ -540,7 +540,7 @@ def _persist_loaded_records( return loaded -def _process_v4_record_metadata(record: dict, record_data): +def _process_v4_record_metadata(record: Dict, record_data: record_pb2.RecordData): if record_data.fileSize > 0: record['file_size'] = record_data.fileSize @@ -548,14 +548,14 @@ def _process_v4_record_metadata(record: dict, record_data): record['thumbnail_size'] = record_data.thumbnailSize -def _process_record_owner_info(record: dict, record_data): +def _process_record_owner_info(record: Dict, record_data: record_pb2.RecordData): 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 _handle_record_versions(record: dict, record_data, version: int) -> None: +def _handle_record_versions(record: Dict, record_data: record_pb2.RecordData, version: int) -> None: record_key = record['record_key_unencrypted'] record['data_unencrypted'] = _decrypt_record_data(record_data, record_key, version) @@ -568,7 +568,7 @@ def _handle_record_versions(record: dict, record_data, version: int) -> None: _process_record_owner_info(record, record_data) -def _add_share_permissions(record: dict, record_info): +def _add_share_permissions(record: Dict, record_info: record_pb2.RecordDataWithAccessInfo): """Add share permissions to record.""" record['shares'] = { 'user_permissions': [{ @@ -613,7 +613,7 @@ def get_record_shares( raise ValueError(f"Error fetching record shares: {e}") -def _needs_share_info(uid: str, record_cache: dict, is_share_admin: bool) -> bool: +def _needs_share_info(uid: str, record_cache: Dict[str, Any], is_share_admin: bool) -> bool: """Check if a record needs share information.""" if uid in record_cache: record = record_cache[uid] diff --git a/keepersdk-package/src/keepersdk/vault/shares_management.py b/keepersdk-package/src/keepersdk/vault/shares_management.py index b145464d..5e076005 100644 --- a/keepersdk-package/src/keepersdk/vault/shares_management.py +++ b/keepersdk-package/src/keepersdk/vault/shares_management.py @@ -613,7 +613,7 @@ def _process_records(vault, rq, curr_sf, rec_uids, action, ce, cs, share_expirat if not rec_uids: return - existing_records = {x['record_uid'] for x in curr_sf.get('records', [])} + existing_records = {x.record_uid for x in curr_sf.get('records', [])} for record_uid in rec_uids: ro = folder_pb2.SharedFolderUpdateRecord() diff --git a/keepersdk-package/src/keepersdk/vault/sync_down.py b/keepersdk-package/src/keepersdk/vault/sync_down.py index c8250b16..bb49407e 100644 --- a/keepersdk-package/src/keepersdk/vault/sync_down.py +++ b/keepersdk-package/src/keepersdk/vault/sync_down.py @@ -73,9 +73,7 @@ def sync_down_request(auth: keeper_auth.KeeperAuth, if nsf_storage is not None: if nsf_task is None: nsf_task = nsf_data.NSFRebuildTask(False) - if not nsf_sync.try_apply_nsf_from_sync_down_proto(response, nsf_storage, nsf_task): - nsf_sync.try_apply_nsf_from_sync_down_json( - auth, nsf_storage, rq.continuationToken, nsf_task) + nsf_sync.try_apply_nsf_from_sync_down_proto(response, nsf_storage, nsf_task) if len(response.removedRecords) > 0: record_uids = [utils.base64_url_encode(x) for x in response.removedRecords] diff --git a/keepersdk-package/unit_tests/test_nsf_management.py b/keepersdk-package/unit_tests/test_nsf_management.py deleted file mode 100644 index be7b4d18..00000000 --- a/keepersdk-package/unit_tests/test_nsf_management.py +++ /dev/null @@ -1,72 +0,0 @@ -import unittest - -from keepersdk.vault import nsf_sync, memory_nsf_storage, nsf_management -from keepersdk.vault.nsf_data import NSFData -class _FakeVault: - def __init__(self, view: NSFData) -> None: - self.nsf_data = view - self.sync_requested = False - - def run_pending_jobs(self) -> None: - pass - - -class TestNsfManagement(unittest.TestCase): - def test_list_and_resolve_from_cache(self): - nsf = memory_nsf_storage.InMemoryNSFStorage() - nsf_sync.apply_nsf_data_dict( - nsf, - { - 'folders': [{'folderUid': 'F1', 'parentUid': '', 'data': '', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': '', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}], - 'folderRecords': [{'folderUid': 'F1', 'recordMetadata': {'recordUid': 'R1'}, - 'folderKeyEncryptionType': 0}], - 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, - 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], - 'recordData': [{'recordUid': 'R1', 'user': {'accountUid': 'a', 'username': 'u'}, - 'data': ''}], - }, - ) - view = NSFData(nsf) - vault = _FakeVault(view) - - rows = nsf_management.list_nsf_items(vault) - self.assertEqual(len(rows), 2) - self.assertEqual(nsf_management.resolve_nsf_folder_uid(vault, 'F1'), 'F1') - self.assertIsNone(nsf_management.resolve_nsf_record_uid(vault, 'missing')) - - def test_find_child_folder_and_build_removals(self): - nsf = memory_nsf_storage.InMemoryNSFStorage() - nsf_sync.apply_nsf_data_dict( - nsf, - { - 'folders': [ - {'folderUid': 'P1', 'parentUid': '', 'data': '', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': '', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}, - {'folderUid': 'C1', 'parentUid': 'P1', 'data': '', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': '', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}, - ], - 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, - 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], - 'recordData': [{'recordUid': 'R1', 'user': {'accountUid': 'a', 'username': 'u'}, - 'data': ''}], - 'folderRecords': [{'folderUid': 'P1', 'recordMetadata': {'recordUid': 'R1'}, - 'folderKeyEncryptionType': 0}], - }, - ) - view = NSFData(nsf) - vault = _FakeVault(view) - removals = nsf_management.build_nsf_record_removals( - vault, ['R1'], operation_type='owner-trash') - self.assertEqual(len(removals), 1) - self.assertEqual(removals[0]['record_uid'], 'R1') - - -if __name__ == '__main__': - unittest.main() diff --git a/keepersdk-package/unit_tests/test_nsf_sync.py b/keepersdk-package/unit_tests/test_nsf_sync.py index 78f5dea1..2ca9de69 100644 --- a/keepersdk-package/unit_tests/test_nsf_sync.py +++ b/keepersdk-package/unit_tests/test_nsf_sync.py @@ -1,123 +1,11 @@ -import json -import os -import sqlite3 import unittest from keepersdk import utils -from keepersdk.proto import SyncDown_pb2, folder_pb2 -from keepersdk.vault import nsf_data, nsf_sync, nsf_storage_types as nsf, memory_nsf_storage, sqlite_storage, storage_types +from keepersdk.proto import SyncDown_pb2 +from keepersdk.vault import nsf_sync, nsf_storage_types as nsf, memory_nsf_storage class TestNSFSync(unittest.TestCase): - def test_coerce_int_folder_usage_type_enum_name(self): - self.assertEqual(nsf_sync._coerce_int('UT_NORMAL'), int(folder_pb2.UT_NORMAL)) - self.assertEqual(nsf_sync._coerce_int(1), 1) - - def test_dict_to_folder_accepts_enum_name(self): - folder = nsf_sync._dict_to_folder({ - 'folderUid': 'F1', - 'parentUid': 'P', - 'data': 'd', - 'type': 'UT_NORMAL', - 'inheritUserPermissions': 1, - 'folderKey': 'k', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, - 'lastModified': 2, - }) - self.assertEqual(folder.folder_type, int(folder_pb2.UT_NORMAL)) - - def test_nsf_data_rebuild_after_sync(self): - conn = sqlite3.connect(':memory:') - vault = sqlite_storage.SqliteVaultStorage(lambda: conn, b'owner') - nsf = vault.nsf - view = nsf_data.NSFData(nsf) - nsf_sync.apply_nsf_data_dict( - nsf, - {'folders': [{'folderUid': 'F1', 'parentUid': 'P', 'data': 'd', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': 'k', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}]}, - ) - self.assertEqual(len(list(nsf.folders.get_all_entities())), 1) - view.rebuild_data(nsf_data.NSFRebuildTask(True)) - self.assertEqual(view.folder_count, 0) - conn.close() - - def test_apply_nsf_sync_down_sample(self): - root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) - path = os.path.join(root, 'nsf_sync_down.txt') - with open(path, 'r', encoding='utf-8') as f: - payload = json.load(f) - - conn = sqlite3.connect(':memory:') - owner = b'owner-test' - vault = sqlite_storage.SqliteVaultStorage(lambda: conn, owner) - nsf = vault.nsf - assert nsf is not None - nsf_sync.apply_nsf_from_full_sync_json(nsf, payload) - - inner = payload['nsfData'] - self.assertEqual(len(list(nsf.folders.get_all_entities())), len(inner['folders'])) - self.assertEqual(len(list(nsf.records.get_all_entities())), len(inner['records'])) - - f0 = nsf.folders.get_entity(inner['folders'][0]['folderUid']) - assert f0 is not None - self.assertEqual(f0.owner_username, inner['folders'][0]['ownerInfo']['username']) - - conn.close() - - def test_revoked_and_removed_sync_down_delete_main_tables(self): - """Revoked/removed sync-down payloads delete rows from main tables only.""" - nsf = memory_nsf_storage.InMemoryNSFStorage() - nsf_sync.apply_nsf_data_dict( - nsf, - { - 'folders': [{'folderUid': 'F1', 'parentUid': '', 'data': 'd', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': 'k', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}], - 'folderAccesses': [{'folderUid': 'F1', 'accessTypeUid': 'A1', 'accessType': 1, - 'accessRoleType': 0, 'inherited': False, 'hidden': False, - 'deniedAccess': False, 'dateCreated': 1, 'lastModified': 2}], - 'folderRecords': [{'folderUid': 'F1', 'recordMetadata': {'recordUid': 'R1'}, - 'folderKeyEncryptionType': 0}], - 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, - 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], - 'recordLinks': [{'parentRecordUid': 'P1', 'childRecordUid': 'C1', - 'recordKey': 'k', 'revision': 1}], - 'recordAccesses': [{'recordUid': 'R1', 'accessTypeUid': 'A2', 'accessType': 1, - 'accessRoleType': 0, 'owner': True, 'inherited': False, - 'hidden': False, 'deniedAccess': False, - 'canViewTitle': True, 'canEdit': True, 'canView': True, - 'canListAccess': True, 'canUpdateAccess': True, - 'canDelete': True, 'canChangeOwnership': True, - 'canRequestAccess': True, 'canApproveAccess': True, - 'dateCreated': 1, 'lastModified': 2}], - }, - ) - self.assertIsNotNone(nsf.folders.get_entity('F1')) - self.assertEqual(len(list(nsf.folder_accesses.get_links_by_subject('F1'))), 1) - self.assertEqual(len(list(nsf.folder_records.get_links_by_subject('F1'))), 1) - self.assertEqual(len(list(nsf.record_links.get_all_links())), 1) - self.assertEqual(len(list(nsf.record_accesses.get_links_by_subject('R1'))), 1) - - nsf_sync.apply_nsf_data_dict( - nsf, - { - 'revokedFolderAccesses': [{'folderUid': 'F1', 'actorUid': 'A1'}], - 'removedFolderRecords': [{'folderUid': 'F1', 'recordUid': 'R1'}], - 'removedRecordLinks': [{'parentRecordUid': 'P1', 'childRecordUid': 'C1'}], - 'revokedRecordAccesses': [{'recordUid': 'R1', 'actorUid': 'A2'}], - 'removedFolders': ['F1'], - }, - ) - self.assertIsNone(nsf.folders.get_entity('F1')) - self.assertEqual(len(list(nsf.folder_accesses.get_links_by_subject('F1'))), 1) - self.assertEqual(len(list(nsf.folder_records.get_links_by_subject('F1'))), 0) - self.assertEqual(len(list(nsf.record_links.get_all_links())), 0) - self.assertEqual(len(list(nsf.record_accesses.get_links_by_subject('R1'))), 0) - def test_removed_folder_records_proto_uses_folder_record_key_fields(self): """Proto removedFolderRecords are FolderRecordKey (snake_case fields).""" storage = memory_nsf_storage.InMemoryNSFStorage() @@ -136,67 +24,6 @@ def test_removed_folder_records_proto_uses_folder_record_key_fields(self): self.assertEqual(len(list(storage.folder_records.get_links_by_subject(folder_uid))), 0) - def test_store_record_data_before_records_row_creates_stub(self): - """recordData may arrive in an earlier sync chunk than records metadata.""" - nsf = memory_nsf_storage.InMemoryNSFStorage() - nsf_sync.apply_nsf_data_dict( - nsf, - {'recordData': [{'recordUid': 'R1', 'data': 'payload-data'}]}, - ) - row = nsf.records.get_entity('R1') - self.assertIsNotNone(row) - assert row is not None - self.assertEqual(row.data, 'payload-data') - - nsf_sync.apply_nsf_data_dict( - nsf, - {'records': [{'recordUid': 'R1', 'revision': 2, 'version': 3, 'shared': False, - 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}]}, - ) - row = nsf.records.get_entity('R1') - assert row is not None - self.assertEqual(row.data, 'payload-data') - self.assertEqual(row.revision, 2) - - def test_store_records_before_record_data_preserves_payload(self): - """recordData must apply after records exist in storage.""" - nsf = memory_nsf_storage.InMemoryNSFStorage() - nsf_sync.apply_nsf_data_dict( - nsf, - { - 'records': [{'recordUid': 'R1', 'revision': 1, 'version': 1, 'shared': False, - 'clientModifiedTime': 0, 'fileSize': 0, 'thumbnailSize': 0}], - 'recordData': [{'recordUid': 'R1', 'data': 'payload-data'}], - }, - ) - row = nsf.records.get_entity('R1') - self.assertIsNotNone(row) - assert row is not None - self.assertEqual(row.data, 'payload-data') - - def test_vault_clear_wipes_nsf_tables(self): - """``SqliteVaultStorage.clear`` clears vault and NSF rows in the same database.""" - conn = sqlite3.connect(':memory:') - owner = b'vault-owner' - vault = sqlite_storage.SqliteVaultStorage(lambda: conn, owner) - nsf = vault.nsf - - vault.user_settings.store(storage_types.UserSettings(continuation_token=b'x')) - nsf_sync.apply_nsf_data_dict( - nsf, - {'folders': [{'folderUid': 'F1', 'parentUid': 'P', 'data': 'd', 'type': 1, - 'inheritUserPermissions': 0, 'folderKey': 'k', - 'ownerInfo': {'accountUid': 'a', 'username': 'u'}, - 'dateCreated': 1, 'lastModified': 2}]}, - ) - self.assertIsNotNone(nsf.folders.get_entity('F1')) - - vault.clear() - self.assertIsNone(vault.user_settings.load()) - self.assertIsNone(nsf.folders.get_entity('F1')) - - conn.close() - if __name__ == '__main__': unittest.main() From 8b31b3e3228244bcc7502eed21c1a13339bf88fb Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 22 Jun 2026 10:07:46 +0530 Subject: [PATCH 22/23] SDK release 1.2.0 --- keepercli-package/src/keepercli/__init__.py | 2 +- keepersdk-package/src/keepersdk/__init__.py | 2 +- .../src/keepersdk/helpers/keeper_dag/__version__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/keepercli-package/src/keepercli/__init__.py b/keepercli-package/src/keepercli/__init__.py index e9c7de7d..5ad78b99 100644 --- a/keepercli-package/src/keepercli/__init__.py +++ b/keepercli-package/src/keepercli/__init__.py @@ -9,5 +9,5 @@ # Contact: commander@keepersecurity.com # -__version__ = '1.1.0' +__version__ = '1.2.0' diff --git a/keepersdk-package/src/keepersdk/__init__.py b/keepersdk-package/src/keepersdk/__init__.py index 423455dd..b6443102 100644 --- a/keepersdk-package/src/keepersdk/__init__.py +++ b/keepersdk-package/src/keepersdk/__init__.py @@ -10,6 +10,6 @@ # from . import background -__version__ = '1.1.0' +__version__ = '1.2.0' background.init() diff --git a/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py index 12ce4098..f00ed5d1 100644 --- a/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py +++ b/keepersdk-package/src/keepersdk/helpers/keeper_dag/__version__.py @@ -1 +1 @@ -__version__ = '1.1.0' # pragma: no cover +__version__ = '1.2.0' # pragma: no cover From 9e5566f3deae6356a99b7efbf804218f19cce492 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Mon, 22 Jun 2026 13:41:27 +0530 Subject: [PATCH 23/23] Fix unit tests --- keepersdk-package/setup.cfg | 2 ++ keepersdk-package/unit_tests/test_sqlite_dao.py | 4 ++++ keepersdk-package/unit_tests/test_vault_storage.py | 1 + 3 files changed, 7 insertions(+) diff --git a/keepersdk-package/setup.cfg b/keepersdk-package/setup.cfg index 196fe098..6d933c6b 100644 --- a/keepersdk-package/setup.cfg +++ b/keepersdk-package/setup.cfg @@ -32,6 +32,8 @@ install_requires = websockets>=13.1 fido2>=2.0.0; python_version>='3.10' email-validator>=2.0.0 + pydantic>=2.6.4; python_version>='3.8' + google-api-core>=2.16.0 [options.package_data] diff --git a/keepersdk-package/unit_tests/test_sqlite_dao.py b/keepersdk-package/unit_tests/test_sqlite_dao.py index ff03827b..8cecfb33 100644 --- a/keepersdk-package/unit_tests/test_sqlite_dao.py +++ b/keepersdk-package/unit_tests/test_sqlite_dao.py @@ -21,6 +21,7 @@ class Settings: class TestSqliteDao(TestCase): def test_proto(self) -> None: connection = sqlite3.Connection(':memory:') + self.addCleanup(connection.close) settings_table = sqlite_dao.TableSchema.load_schema( enterprise_pb2.Node, ['nodeId'], owner_column='enterprise_id', owner_type=int) @@ -42,6 +43,7 @@ def test_proto(self) -> None: def test_create_query(self) -> None: connection = sqlite3.Connection(':memory:') + self.addCleanup(connection.close) settings_table = sqlite_dao.TableSchema.load_schema( Settings, [], owner_column='account_uid', owner_type=str) @@ -70,6 +72,7 @@ def test_create_query(self) -> None: def test_entity_storage(self) -> None: connection = sqlite3.Connection(':memory:') + self.addCleanup(connection.close) record_table = sqlite_dao.TableSchema.load_schema( vault_storage_types.StorageRecord, 'record_uid', owner_column='account_uid', owner_type=str) record_key_table = sqlite_dao.TableSchema.load_schema( @@ -132,6 +135,7 @@ def test_entity_storage(self) -> None: def test_link_storage(self) -> None: connection = sqlite3.Connection(':memory:') + self.addCleanup(connection.close) owner_column = 'enterprise_id' collection_link_schema = sqlite_dao.TableSchema.load_schema( diff --git a/keepersdk-package/unit_tests/test_vault_storage.py b/keepersdk-package/unit_tests/test_vault_storage.py index 7a21d241..539b36f4 100644 --- a/keepersdk-package/unit_tests/test_vault_storage.py +++ b/keepersdk-package/unit_tests/test_vault_storage.py @@ -16,6 +16,7 @@ def test_memory_storage_create(self) -> None: def test_sqlite_storage_create(self) -> None: conn = sqlite3.Connection('file:///?mode=memory&cache=shared', uri=True) + self.addCleanup(conn.close) vault_data: vault_storage.IVaultStorage = \ sqlite_storage.SqliteVaultStorage(lambda: conn, utils.generate_aes_key()) self.assertIsNotNone(vault_data)