Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion keepercommander/command_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

# Record Type Commands
'Record Type Commands': {
'record-type-info', 'record-type', 'convert'
'record-type-info', 'record-type', 'convert', 'convert-all'
},

# Import and Exporting Data
Expand Down
44 changes: 26 additions & 18 deletions keepercommander/commands/aram.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,10 @@
aging_report_parser.exit = suppress_exit

action_report_parser = argparse.ArgumentParser(prog='action-report', description='Run an action based on user activity', parents=[report_output_parser])
action_report_target_statuses = ['no-logon', 'no-update', 'locked', 'invited', 'no-recovery']
action_report_target_statuses = ['all','no-logon', 'no-update', 'locked', 'invited', 'no-recovery']
action_report_parser.add_argument('--target', '-t', dest='target_user_status', action='store',
choices=action_report_target_statuses, default='no-logon',
help='user status to report on')
choices=action_report_target_statuses, default='all',
help='user status to report on')
action_report_parser.add_argument('--days-since', '-d', dest='days_since', action='store', type=int,
help='number of days since event of interest (e.g., login, record add/update, lock)')
action_report_columns = {'name', 'status', 'transfer_status', 'node', 'team_count', 'teams', 'role_count', 'roles',
Expand All @@ -156,16 +156,20 @@
action_report_parser.add_argument('--columns', dest='columns', action='store', type=str,
help=columns_help)
action_report_parser.add_argument('--apply-action', '-a', dest='apply_action', action='store',
choices=['lock', 'delete', 'transfer', 'none'], default='none',
choices=['lock', 'delete', 'transfer', 'move', 'none'], default='none',
help='admin action to apply to each user in the report')
target_user_help = 'username/email of account to transfer users to when --apply-action=transfer is specified'
action_report_parser.add_argument('--target-user', action='store', help=target_user_help)
target_node_help = 'Node name/ID to move users to when --apply-action=move is specified'
action_report_parser.add_argument('--target-node', action='store', help=target_node_help)
action_report_parser.add_argument('--dry-run', '-n', dest='dry_run', default=False, action='store_true',
help='flag to enable dry-run mode')
force_action_help = 'skip confirmation prompt when applying irreversible admin actions (e.g., delete, transfer)'
action_report_parser.add_argument('--force', '-f', action='store_true', help=force_action_help)
node_filter_help = 'filter users by node (node name or ID)'
action_report_parser.add_argument('--node', dest='node', action='store', help=node_filter_help)
recursive_help = 'Search in node and subnodes when --node is specified'
action_report_parser.add_argument('--recursive', action='store_true', help=recursive_help)

syslog_templates = None # type: Optional[List[str]]

Expand Down Expand Up @@ -2264,14 +2268,14 @@ def transfer_accounts(from_users, to_user, dryrun=False):

def apply_admin_action(targets, status='no-update', action='none', dryrun=False):
# type: (List[Dict[str, Any]], Optional[str], Optional[str], Optional[bool]) -> str
default_allowed = {'none'}
default_allowed = {'none', 'move'}
status_actions = {
'no-logon': {*default_allowed, 'lock'},
'no-update': {*default_allowed},
'locked': {*default_allowed, 'delete', 'transfer'},
'invited': {*default_allowed, 'delete'},
'no-recovery': default_allowed,
'blocked': {*default_allowed, 'delete'}
'all': default_allowed
}

actions_allowed = status_actions.get(status)
Expand All @@ -2285,12 +2289,15 @@ def apply_admin_action(targets, status='no-update', action='none', dryrun=False)
action_handlers = {
'none': partial(run_cmd, targets, None, None, dryrun),
'lock': partial(run_cmd, targets,
lambda: exec_fn(params, email=emails, lock=True, force=True, return_results=True),
'lock', dryrun),
lambda: exec_fn(params, email=emails, lock=True, force=True, return_results=True),
'lock', dryrun),
'delete': partial(run_cmd, targets,
lambda: exec_fn(params, email=emails, delete=True, force=True, return_results=True),
'delete', dryrun),
'transfer': partial(transfer_accounts, targets, kwargs.get('target_user'), dryrun)
lambda: exec_fn(params, email=emails, delete=True, force=True, return_results=True),
'delete', dryrun),
'transfer': partial(transfer_accounts, targets, kwargs.get('target_user'), dryrun),
'move': partial(run_cmd, targets,
lambda: exec_fn(params, email=emails, node=kwargs.get('target_node'), force=True, return_results=True),
'move', dryrun)
}

if action in ('delete', 'transfer') and not dryrun and not kwargs.get('force') and targets:
Expand Down Expand Up @@ -2356,11 +2363,11 @@ def get_report_data_and_headers(targets, output_fmt, columns=None, lock_times=No
logging.warning(f'More than one node "{node_name}" found. Use Node ID.')
return

target_node_id = nodes[0]['node_id']
node_id = nodes[0]['node_id']

# Validate target_node_id
if not isinstance(target_node_id, int) or target_node_id <= 0:
logging.warning(f'Invalid node ID: {target_node_id}')
# Validate node_id
if not isinstance(node_id, int) or node_id <= 0:
logging.warning(f'Invalid node ID: {node_id}')
return

# Build parent-child lookup dictionary once to avoid deep recursion
Expand All @@ -2386,7 +2393,7 @@ def get_descendant_nodes(node_id):
queue.append(child_id)
return descendants

target_nodes = get_descendant_nodes(target_node_id)
target_nodes = get_descendant_nodes(node_id) if kwargs.get('recursive') else [node_id]
filtered_user_ids = {user['enterprise_user_id'] for user in params.enterprise.get('users', [])
if user.get('node_id') in target_nodes}

Expand All @@ -2397,9 +2404,10 @@ def get_descendant_nodes(node_id):
target_status = kwargs.get('target_user_status', 'no-logon')
days = kwargs.get('days_since')
if days is None:
days = 90 if target_status == 'locked' else 30
days = 90 if target_status == 'locked' else 0 if target_status == 'all' else 30

args_by_status = {
'all': [active+locked+invited,days,[]],
'no-logon': [active, days, ['login', 'login_console', 'chat_login', 'accept_invitation']],
'no-update': [active, days, ['record_add', 'record_update']],
'locked': [locked, days, ['lock_user'], 'to_username'],
Expand All @@ -2413,7 +2421,7 @@ def get_descendant_nodes(node_id):
logging.warning(f'Invalid target_user_status \'{target_status}\': value must be one of {valid_targets}')
return

target_users = get_no_action_users(*args)
target_users = args[0]
usernames = {user['username'] for user in target_users}

columns_arg = kwargs.get('columns')
Expand Down
Loading
Loading