Skip to content

Commit

Permalink
Relogin does not clear the previous MC context. KC-790
Browse files Browse the repository at this point in the history
  • Loading branch information
sk-keeper committed Jul 8, 2024
1 parent 5726c99 commit 667aa84
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 142 deletions.
5 changes: 3 additions & 2 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,6 @@ def is_msp(params_local):

else:
cmd, args = command_and_args_from_cmd(command_line)
params, args = check_if_running_as_mc(params, args)

if cmd:
orig_cmd = cmd
if cmd in aliases and cmd not in commands and cmd not in enterprise_commands and cmd not in msp_commands:
Expand All @@ -240,6 +238,9 @@ def is_msp(params_local):
logging.info('Canceled')
return

if cmd in enterprise_commands 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_executing_as_msp_admin():
logging.debug("OK to execute command: %s", cmd)
Expand Down
3 changes: 3 additions & 0 deletions keepercommander/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def register_enterprise_commands(commands, aliases, command_info):
from .msp import switch_to_msp_parser, SwitchToMspCommand
commands[switch_to_msp_parser.prog] = SwitchToMspCommand()
command_info[switch_to_msp_parser.prog] = switch_to_msp_parser.description
from . import enterprise_reports
enterprise_reports.register_commands(commands)
enterprise_reports.register_command_info(aliases, command_info)


def register_msp_commands(commands, aliases, command_info):
Expand Down
136 changes: 2 additions & 134 deletions keepercommander/commands/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,12 @@
report_output_parser
from .enterprise_common import EnterpriseCommand
from .enterprise_push import EnterprisePushCommand, enterprise_push_parser
from .register import ShareRecordCommand, ShareFolderCommand
from .transfer_account import EnterpriseTransferUserCommand, transfer_user_parser
from .. import api, crypto, utils, constants
from ..display import bcolors
from ..error import CommandError, KeeperApiError, Error
from ..error import CommandError, KeeperApiError
from ..params import KeeperParams
from ..proto import record_pb2, APIRequest_pb2, enterprise_pb2
from ..sox.sox_types import RecordPermissions


def register_commands(commands):
Expand All @@ -67,7 +65,6 @@ def register_commands(commands):
commands['aging-report'] = aram.AgingReportCommand()
commands['user-report'] = UserReportCommand()
commands['action-report'] = ActionReportCommand()
commands['external-shares-report'] = ExternalSharesReportCommand()
commands['audit-alert'] = audit_alerts.AuditAlerts()

compliance.register_commands(commands)
Expand All @@ -91,7 +88,7 @@ def register_command_info(aliases, command_info):
enterprise_role_parser, enterprise_team_parser, transfer_user_parser,
enterprise_push_parser, team_approve_parser, device_approve_parser,
aram.audit_log_parser, aram.audit_report_parser, aram.aging_report_parser, aram.action_report_parser,
user_report_parser, external_share_report_parser]:
user_report_parser]:
command_info[p.prog] = p.description

compliance.register_command_info(aliases, command_info)
Expand Down Expand Up @@ -284,22 +281,6 @@ def register_command_info(aliases, command_info):
user_report_parser.error = raise_parse_exception
user_report_parser.exit = suppress_exit

ext_shares_report_desc = 'Run an external shares report.'
external_share_report_parser = argparse.ArgumentParser(prog='external-shares-report', description=ext_shares_report_desc)
external_share_report_parser.add_argument('--format', dest='format', action='store', choices=['table', 'json', 'csv'],
default='table', help='output format.')
external_share_report_parser.add_argument('--output', dest='output', action='store',
help='output file name. (ignored for table format)')
external_share_report_parser.add_argument('-a', '--action', action='store', choices=['remove', 'none'], default='none',
help='action to perform on external shares, \'none\' if omitted')
external_share_report_parser.add_argument('-t', '--share-type', action='store', choices=['direct', 'shared-folder', 'all'],
default='all', help='filter report by share type, \'all\' if omitted')
# external_share_report_parser.add_argument('-e', '--email', action='store', help='filter report by share-recipient email')
external_share_report_parser.add_argument('-f', '--force', action='store_true', help='apply action w/o confirmation')
external_share_report_parser.add_argument('-r', '--refresh-data', action='store_true', help='retrieve fresh data')
external_share_report_parser.error = raise_parse_exception
external_share_report_parser.exit = suppress_exit


def get_user_status_dict(user):

Expand Down Expand Up @@ -3310,119 +3291,6 @@ def get_user_status(user):
return status


class ExternalSharesReportCommand(EnterpriseCommand):
def __init__(self):
super(ExternalSharesReportCommand, self).__init__()
self.sox_data = None

def get_sox_data(self, params, refresh_data):
if not self.sox_data or refresh_data:
from keepercommander.sox import get_compliance_data
node_id = params.enterprise['nodes'][0].get('node_id', 0)
enterprise_id = node_id >> 32
now_ts = datetime.datetime.now().timestamp()
self.sox_data = get_compliance_data(params, node_id, enterprise_id, True, now_ts, False, True)
return self.sox_data

def get_parser(self):
return external_share_report_parser

def execute(self, params, **kwargs):
output = kwargs.get('output')
output_fmt = kwargs.get('format', 'table')
action = kwargs.get('action', 'none')
force_action = kwargs.get('force')
share_type = kwargs.get('share_type', 'all')
refresh_data = kwargs.get('refresh_data')
sd = self.get_sox_data(params, refresh_data)

# Non-enterprise users
external_users = {uid: user for uid, user in sd.get_users().items() if (user.user_uid >> 32) == 0}
ext_uuids = set(external_users.keys())

def get_direct_shares():
records = sd.get_records()
ext_recs = [r for r in records.values() if r.shared and ext_uuids.intersection(r.user_permissions.keys())]
rec_shares = {r.record_uid: ext_uuids.intersection(r.user_permissions.keys()) for r in ext_recs}
return rec_shares

def get_sf_shares():
folders = sd.get_shared_folders()
ext_sfs = [sf for sf in folders.values() if ext_uuids.intersection(sf.users)]
sf_shares = {sf.folder_uid: ext_uuids.intersection(sf.users) for sf in ext_sfs}
return sf_shares

def confirm_remove_shares():
logging.info(bcolors.FAIL + bcolors.BOLD + '\nALERT!' + bcolors.ENDC)
logging.info('You are about to delete the following shares:')
generate_report('simple')
answer = user_choice('\nDo you wish to proceed?', 'yn', 'n')
if answer.lower() in {'y', 'yes'}:
remove_shares()
else:
logging.info('Action aborted.')

def remove_shares():
if share_type in ('direct', 'all'):
cmd = ShareRecordCommand()
for rec_uid, user_uids in get_direct_shares().items():
emails = [external_users.get(user_uid).email for user_uid in user_uids]
try:
cmd.execute(params, email=emails, action='revoke', record=rec_uid)
except Error:
pass
if share_type in ('shared-folder', 'all'):
cmd = ShareFolderCommand()
for sf_uid, user_uids in get_sf_shares().items():
emails = [external_users.get(user_uid).email for user_uid in user_uids]
try:
cmd.execute(params, user=emails, action='remove', folder=sf_uid)
except Error:
pass

def apply_action():
if action == 'remove':
remove_shares() if force_action else confirm_remove_shares()

def fill_rows(rows, shares, share_category):
direct_shares = share_category.lower() == 'direct'
for sf_or_rec_uid, targets in shares.items():
if direct_shares:
rec = sd.get_records().get(sf_or_rec_uid)
name = (rec.data or {}).get('title')
perm_lookup = rec.user_permissions
else:
# TODO : populate shared-folder 1) name and 2) permissions (from get_record_details endpoint in KA)
name = ''
for target_id in targets:
target = external_users.get(target_id).email
perms = RecordPermissions.to_permissions_str(perm_lookup.get(target_id)) if direct_shares \
else ''
row = [sf_or_rec_uid, name, share_category, target, perms]
rows.append(row)
return rows

def generate_report(report_type='standard'):
headers = ['uid', 'name', 'type', 'shared_to', 'permissions']
rep_fmt = output_fmt if report_type == 'standard' else 'table'
rep_out = output if report_type == 'standard' else None
title = 'External Shares Report' if report_type == 'standard' else None
if rep_fmt != 'json':
headers = [field_to_title(field) for field in headers]
table = []
if share_type in ('direct', 'all'):
table = fill_rows(table, get_direct_shares(), 'Direct')
if share_type in ('shared-folder', 'all'):
table = fill_rows(table, get_sf_shares(), 'Shared Folder')

return dump_report_data(table, headers, title=title, fmt=rep_fmt, filename=rep_out)

if action != 'none':
apply_action()
else:
return generate_report()


class TeamApproveCommand(EnterpriseCommand):
def get_parser(self):
return team_approve_parser
Expand Down
145 changes: 145 additions & 0 deletions keepercommander/commands/enterprise_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import argparse
import datetime
import logging

from . import enterprise_common, register
from ..sox.sox_types import RecordPermissions
from ..error import Error
from ..display import bcolors
from . import base


def register_commands(commands):
commands['external-shares-report'] = ExternalSharesReportCommand()


def register_command_info(aliases, command_info):
aliases['esr'] = 'external-shares-report'

for p in [external_share_report_parser]:
command_info[p.prog] = p.description


ext_shares_report_desc = 'Run an external shares report.'
external_share_report_parser = argparse.ArgumentParser(prog='external-shares-report', description=ext_shares_report_desc,
parents=[base.report_output_parser])
external_share_report_parser.add_argument('-a', '--action', action='store', choices=['remove', 'none'], default='none',
help='action to perform on external shares, \'none\' if omitted')
external_share_report_parser.add_argument('-t', '--share-type', action='store', choices=['direct', 'shared-folder', 'all'],
default='all', help='filter report by share type, \'all\' if omitted')
# external_share_report_parser.add_argument('-e', '--email', action='store', help='filter report by share-recipient email')
external_share_report_parser.add_argument('-f', '--force', action='store_true', help='apply action w/o confirmation')
external_share_report_parser.add_argument('-r', '--refresh-data', action='store_true', help='retrieve fresh data')


class ExternalSharesReportCommand(enterprise_common.EnterpriseCommand):
def __init__(self):
super(ExternalSharesReportCommand, self).__init__()
self.sox_data = None

def get_sox_data(self, params, refresh_data):
if not self.sox_data or refresh_data:
from keepercommander.sox import get_compliance_data
node_id = params.enterprise['nodes'][0].get('node_id', 0)
enterprise_id = node_id >> 32
now_ts = datetime.datetime.now().timestamp()
self.sox_data = get_compliance_data(params, node_id, enterprise_id, True, now_ts, False, True)
return self.sox_data

def get_parser(self):
return external_share_report_parser

def execute(self, params, **kwargs):
output = kwargs.get('output')
output_fmt = kwargs.get('format', 'table')
action = kwargs.get('action', 'none')
force_action = kwargs.get('force')
share_type = kwargs.get('share_type', 'all')
refresh_data = kwargs.get('refresh_data')
sd = self.get_sox_data(params, refresh_data)

# Non-enterprise users
external_users = {uid: user for uid, user in sd.get_users().items() if (user.user_uid >> 32) == 0}
ext_uuids = set(external_users.keys())

def get_direct_shares():
records = sd.get_records()
ext_recs = [r for r in records.values() if r.shared and ext_uuids.intersection(r.user_permissions.keys())]
rec_shares = {r.record_uid: ext_uuids.intersection(r.user_permissions.keys()) for r in ext_recs}
return rec_shares

def get_sf_shares():
folders = sd.get_shared_folders()
ext_sfs = [sf for sf in folders.values() if ext_uuids.intersection(sf.users)]
sf_shares = {sf.folder_uid: ext_uuids.intersection(sf.users) for sf in ext_sfs}
return sf_shares

def confirm_remove_shares():
logging.info(bcolors.FAIL + bcolors.BOLD + '\nALERT!' + bcolors.ENDC)
logging.info('You are about to delete the following shares:')
generate_report('simple')
answer = base.user_choice('\nDo you wish to proceed?', 'yn', 'n')
if answer.lower() in {'y', 'yes'}:
remove_shares()
else:
logging.info('Action aborted.')

def remove_shares():
if share_type in ('direct', 'all'):
cmd = register.ShareRecordCommand()
for rec_uid, user_uids in get_direct_shares().items():
emails = [external_users.get(user_uid).email for user_uid in user_uids]
try:
cmd.execute(params, email=emails, action='revoke', record=rec_uid)
except Error:
pass
if share_type in ('shared-folder', 'all'):
cmd = register.ShareFolderCommand()
for sf_uid, user_uids in get_sf_shares().items():
emails = [external_users.get(user_uid).email for user_uid in user_uids]
try:
cmd.execute(params, user=emails, action='remove', folder=sf_uid)
except Error:
pass

def apply_action():
if action == 'remove':
remove_shares() if force_action else confirm_remove_shares()

def fill_rows(rows, shares, share_category):
direct_shares = share_category.lower() == 'direct'
for sf_or_rec_uid, targets in shares.items():
if direct_shares:
rec = sd.get_records().get(sf_or_rec_uid)
name = (rec.data or {}).get('title')
perm_lookup = rec.user_permissions
else:
# TODO : populate shared-folder 1) name and 2) permissions (from get_record_details endpoint in KA)
name = ''
for target_id in targets:
target = external_users.get(target_id).email
perms = RecordPermissions.to_permissions_str(perm_lookup.get(target_id)) if direct_shares \
else ''
row = [sf_or_rec_uid, name, share_category, target, perms]
rows.append(row)
return rows

def generate_report(report_type='standard'):
headers = ['uid', 'name', 'type', 'shared_to', 'permissions']
rep_fmt = output_fmt if report_type == 'standard' else 'table'
rep_out = output if report_type == 'standard' else None
title = 'External Shares Report' if report_type == 'standard' else None
if rep_fmt != 'json':
headers = [base.field_to_title(field) for field in headers]
table = []
if share_type in ('direct', 'all'):
table = fill_rows(table, get_direct_shares(), 'Direct')
if share_type in ('shared-folder', 'all'):
table = fill_rows(table, get_sf_shares(), 'Shared Folder')

return base.dump_report_data(table, headers, title=title, fmt=rep_fmt, filename=rep_out)

if action != 'none':
apply_action()
else:
return generate_report()
2 changes: 1 addition & 1 deletion keepercommander/commands/msp.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ def get_parser(self):
return switch_to_msp_parser

def execute(self, params, **kwargs):
global current_mc_id
global current_mc_id, msp_params
if current_mc_id is None:
raise CommandError('switch-to-msp', "Already MSP")

Expand Down
3 changes: 1 addition & 2 deletions keepercommander/commands/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
from ..subfolder import BaseFolderNode, SharedFolderNode, SharedFolderFolderNode, try_resolve_path, get_folder_path, \
get_folder_uids, get_contained_record_uids
from ..loginv3 import LoginV3API
from ..utils import confirm


def register_commands(commands):
Expand Down Expand Up @@ -1935,7 +1934,7 @@ def confirm_removal(cols):
indices = (idx + 1 for idx in range(len(dupe_info)))
prompt_report = prompt_title + '\n' + tabulate(dupe_info, col_headers, showindex=indices)
prompt_msg = prompt_report + '\n\nDo you wish to proceed?'
return confirm(prompt_msg)
return utils.confirm(prompt_msg)

if kwargs.get('force') or confirm_removal(col_headers):
logging_fn(f'Deleting {len(dupe_uids)} records...')
Expand Down
Loading

0 comments on commit 667aa84

Please sign in to comment.