diff --git a/keepercommander/breachwatch.py b/keepercommander/breachwatch.py index 7e670900c..553050be9 100644 --- a/keepercommander/breachwatch.py +++ b/keepercommander/breachwatch.py @@ -14,10 +14,11 @@ from urllib.parse import urlparse, urlunparse from typing import Iterator, Tuple, Optional, List, Callable, Dict, Iterable, Union +from .commands.helpers.enterprise import user_has_privilege, is_addon_enabled from .constants import KEEPER_PUBLIC_HOSTS from . import api, crypto, utils, rest_api, vault from .proto import breachwatch_pb2, client_pb2, APIRequest_pb2 -from .error import KeeperApiError +from .error import KeeperApiError, CommandError from .params import KeeperParams from .vault import KeeperRecord @@ -418,3 +419,17 @@ def scan_and_update_security_data(params, record_uid, bw_obj=None, force_update= if set_reused_pws: BreachWatch.save_reused_pw_count(params) api.sync_down(params) + + @staticmethod + def validate_reporting(cmd, params): + msg_no_priv = 'You do not have the required privilege to run a BreachWatch report' + msg_no_addon = ('BreachWatch is not enabled for this enterprise. ' + 'Please visit https://www.keepersecurity.com/breachwatch.html for more information.') + + privilege = 'run_reports' + addon = 'enterprise_breach_watch' + error_msg = msg_no_priv if not user_has_privilege(params, privilege) \ + else msg_no_addon if not is_addon_enabled(params, addon) \ + else None + if error_msg: + raise CommandError(cmd, error_msg) diff --git a/keepercommander/cli.py b/keepercommander/cli.py index c3608477c..88d9af254 100644 --- a/keepercommander/cli.py +++ b/keepercommander/cli.py @@ -32,7 +32,7 @@ register_commands, register_enterprise_commands, register_msp_commands, aliases, commands, command_info, enterprise_commands, msp_commands ) -from .commands.base import dump_report_data, CliCommand +from .commands.base import dump_report_data, CliCommand, GroupCommand from .commands import msp from .constants import OS_WHICH_CMD, KEEPER_PUBLIC_HOSTS from .error import CommandError, Error @@ -112,6 +112,19 @@ def check_if_running_as_mc(params, args): return params, args +def is_enterprise_command(name, command, args): # type: (str, CliCommand, str) -> bool + if name in enterprise_commands: + return True + elif isinstance(command, GroupCommand): + args = args.split(' ') + verb = next(iter(args), None) + subcommand = command.subcommands.get(verb) + from keepercommander.commands.enterprise_common import EnterpriseCommand + return isinstance(subcommand, EnterpriseCommand) + else: + return False + + def command_and_args_from_cmd(command_line): args = '' pos = command_line.find(' ') @@ -238,10 +251,10 @@ def is_msp(params_local): logging.info('Canceled') return - if cmd in enterprise_commands or cmd in msp_commands: + if is_enterprise_command(cmd, command, args) or cmd in msp_commands: params, args = check_if_running_as_mc(params, args) - if cmd in enterprise_commands and not params.enterprise: + if is_enterprise_command(cmd, command, args) and not params.enterprise: if is_executing_as_msp_admin(): logging.debug("OK to execute command: %s", cmd) else: diff --git a/keepercommander/commands/breachwatch.py b/keepercommander/commands/breachwatch.py index 68ba1b6d8..fb90fb204 100644 --- a/keepercommander/commands/breachwatch.py +++ b/keepercommander/commands/breachwatch.py @@ -15,6 +15,7 @@ import logging from typing import Optional, Any, Dict +from .enterprise_common import EnterpriseCommand from .security_audit import SecurityAuditReportCommand from .. import api, crypto, utils, vault, vault_extensions from .base import GroupCommand, Command, dump_report_data @@ -73,7 +74,7 @@ def __init__(self): self.default_verb = 'list' def validate(self, params): # type: (KeeperParams) -> None - if not params.breach_watch: + if not params.breach_watch and not params.msp_tree_key: raise CommandError('breachwatch', 'BreachWatch is not active. Please visit the Web Vault at https://keepersecurity.com/vault') @@ -279,10 +280,11 @@ def execute(self, params, **kwargs): # type: (KeeperParams, any) -> any logging.info(f'{utils.base64_url_encode(status.recordUid)}: {status.status} {status.reason}') -class BreachWatchReportCommand(Command): +class BreachWatchReportCommand(EnterpriseCommand): def get_parser(self): return breachwatch_report_parser def execute(self, params, **kwargs): + BreachWatch.validate_reporting('breachwatch report', params) cmd = SecurityAuditReportCommand() return cmd.execute(params, **{'breachwatch':True, **kwargs}) diff --git a/keepercommander/commands/helpers/enterprise.py b/keepercommander/commands/helpers/enterprise.py new file mode 100644 index 000000000..75c45a099 --- /dev/null +++ b/keepercommander/commands/helpers/enterprise.py @@ -0,0 +1,37 @@ +from keepercommander.params import KeeperParams + + +def is_addon_enabled(params, addon_name): # type: (KeeperParams, Dict[str, ]) -> Boolean + def is_enabled(addon): + return addon.get('enabled') or addon.get('included_in_product') + + enterprise = params.enterprise or {} + licenses = enterprise.get('licenses') + if not isinstance(licenses, list): + return False + if next(iter(licenses), {}).get('lic_status') == 'business_trial': + return True + addons = [a for l in licenses for a in l.get('add_ons', []) if a.get('name') == addon_name] + return any(a for a in addons if is_enabled(a)) + + +def user_has_privilege(params, privilege): # type: (KeeperParams, str) -> bool + # Running as MSP admin, user has all available privileges in this context + if params.msp_tree_key: + return True + + enterprise = params.enterprise + + # Not an admin account (user has no admin privileges) + if not enterprise: + return False + + # Check role-derived privileges + username = params.user + users = enterprise.get('users') + e_user_id = next(iter([u.get('enterprise_user_id') for u in users if u.get('username') == username])) + role_users = enterprise.get('role_users') + r_ids = [ru.get('role_id') for ru in role_users if ru.get('enterprise_user_id') == e_user_id] + r_privileges = enterprise.get('role_privileges') + p_key = 'privilege' + return any(rp for rp in r_privileges if rp.get('role_id') in r_ids and rp.get(p_key) == privilege) diff --git a/keepercommander/commands/security_audit.py b/keepercommander/commands/security_audit.py index 2b1da0390..afabb4a6b 100644 --- a/keepercommander/commands/security_audit.py +++ b/keepercommander/commands/security_audit.py @@ -7,6 +7,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from keepercommander import api, crypto, utils +from keepercommander.breachwatch import BreachWatch from keepercommander.commands.base import GroupCommand, raise_parse_exception, suppress_exit, field_to_title, \ dump_report_data from keepercommander.commands.enterprise_common import EnterpriseCommand @@ -163,10 +164,9 @@ def execute(self, params, **kwargs): logging.info(security_audit_report_description) return - if kwargs.get('breachwatch') and not params.breach_watch: - msg = ('Ignoring "--breachwatch" option because BreachWatch is not active. ' - 'Please visit the Web Vault at https://keepersecurity.com/vault') - logging.warning(msg) + show_breachwatch = kwargs.get('breachwatch') + if show_breachwatch: + BreachWatch.validate_reporting('security-audit-report', params) def get_node_id(name_or_id): nodes = params.enterprise.get('nodes') or [] @@ -308,7 +308,6 @@ def update_vault_errors(username, error): if save_report: self.save_updated_security_reports(params, updated_security_reports) - show_breachwatch = kwargs.get('breachwatch') and params.breach_watch fields = ('email', 'name', 'at_risk', 'passed', 'ignored') if show_breachwatch else \ ('email', 'name', 'weak', 'medium', 'strong', 'reused', 'unique', 'securityScore', 'twoFactorChannel', 'node') diff --git a/keepercommander/sox/__init__.py b/keepercommander/sox/__init__.py index e837ff496..b7f972147 100644 --- a/keepercommander/sox/__init__.py +++ b/keepercommander/sox/__init__.py @@ -6,6 +6,7 @@ from typing import Dict, Tuple from .. import api, crypto, utils +from ..commands.helpers.enterprise import user_has_privilege, is_addon_enabled from ..error import CommandError, Error from ..params import KeeperParams from ..proto import enterprise_pb2 @@ -18,40 +19,22 @@ def validate_data_access(params, cmd=''): - if not is_compliance_reporting_enabled(params): - msg = 'Compliance reports add-on required to perform this action. ' \ - 'Please contact your administrator to enable this feature.' - raise CommandError(cmd, msg) + privilege = 'run_compliance_reports' + addon = 'compliance_report' + msg_no_priv = 'You do not have the required privilege to run a Compliance Report.' + msg_no_addon = ('Compliance reports add-on is required to perform this action. ' + 'Please contact your administrator to enable this feature.') + error_msg = msg_no_priv if not user_has_privilege(params, privilege) \ + else msg_no_addon if not is_addon_enabled(params, addon) \ + else None + if error_msg: + raise CommandError(cmd, error_msg) def is_compliance_reporting_enabled(params): - enterprise = params.enterprise - if not enterprise: - return False - e_licenses = enterprise.get('licenses') - if not isinstance(e_licenses, list): - return False - if len(e_licenses) == 0: - return False - if e_licenses[0].get('lic_status') == 'business_trial': - return True - addon = next((a for l in e_licenses for a in l.get('add_ons', []) - if a.get('name') == 'compliance_report' and (a.get('enabled') or a.get('included_in_product'))), None) - if addon is None: - return False - - if not params.msp_tree_key: - role_privilege = 'run_compliance_reports' - username = params.user - users = enterprise.get('users') - e_user_id = next(iter([u.get('enterprise_user_id') for u in users if u.get('username') == username])) - role_users = enterprise.get('role_users') - r_ids = [ru.get('role_id') for ru in role_users if ru.get('enterprise_user_id') == e_user_id] - r_privileges = enterprise.get('role_privileges') - p_key = 'privilege' - return any([rp for rp in r_privileges if rp.get('role_id') in r_ids and rp.get(p_key) == role_privilege]) - else: - return True + privilege = 'run_compliance_reports' + addon = 'compliance_report' + return user_has_privilege(params, privilege) and is_addon_enabled(params, addon) def encrypt_data(params, data): # type: (KeeperParams, str) -> bytes diff --git a/keepercommander/utils.py b/keepercommander/utils.py index 1278dc76c..65fa62a6f 100644 --- a/keepercommander/utils.py +++ b/keepercommander/utils.py @@ -321,6 +321,7 @@ def size_to_str(size): # type: (int) -> str size = size / 1024 return f'{size:,.2f} Gb' + def parse_totp_uri(uri): # type: (str) -> Dict[str, Union[str, int, None]] def parse_int(val): return val and int(val)