Skip to content
Open

Toby #12

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
163 changes: 158 additions & 5 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ class PAMCreateRecordRotationCommand(Command):
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',
Expand All @@ -303,6 +305,14 @@ def get_parser(self):
return PAMCreateRecordRotationCommand.parser

def execute(self, params, **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.
"""

record_uids = set() # type: Set[str]

folder_uids = set()
Expand Down Expand Up @@ -496,6 +506,73 @@ def config_resource(_dag, target_record, target_config_uid, silent=None):
_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 = params.record_rotation_cache.get(target_record.record_uid)
schedule_only = kwargs.get('schedule_only')

# Handle schedule-only operations first to avoid unnecessary resource validation
if schedule_only:
if kwargs.get('folder_name') and (not current_record_rotation or current_record_rotation.get('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.get('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.get('schedule')
record_schedule_data = json.loads(cs) if cs else []
except:
record_schedule_data = []
pwd_complexity_rule_list_encrypted = utils.base64_url_decode(current_record_rotation.get('pwd_complexity', ''))
record_resource_uid = current_record_rotation.get('resourceUid')
disabled = current_record_rotation.get('disabled', False)

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])

# Check if we have NOOP rotation for schedule-only operations
noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE'
if target_record and not noop_rotation: # check from record data
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.get('revision', 0)
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(params, encrypted_session_token, encrypted_transmission_key, target_iam_aad_config_uid)
if not _dag or not _dag.linking_dag.has_graph:
Expand All @@ -519,7 +596,6 @@ def config_iam_aad_user(_dag, target_record, target_iam_aad_config_uid):
_dag.link_user_to_config(target_record.record_uid)


current_record_rotation = params.record_rotation_cache.get(target_record.record_uid)

# 1. PAM Configuration UID
record_config_uid = _dag.record.record_uid
Expand Down Expand Up @@ -633,7 +709,74 @@ def config_iam_aad_user(_dag, target_record, target_iam_aad_config_uid):
r_requests.append(rq)

def config_user(_dag, target_record, target_resource_uid, target_config_uid=None, silent=None):
# NOOP rotation
current_record_rotation = params.record_rotation_cache.get(target_record.record_uid)
schedule_only = kwargs.get('schedule_only')

# Handle schedule-only operations first to avoid unnecessary resource validation
if schedule_only:
if kwargs.get('folder_name') and (not current_record_rotation or current_record_rotation.get('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.get('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.get('schedule')
record_schedule_data = json.loads(cs) if cs else []
except:
record_schedule_data = []
pwd_complexity_rule_list_encrypted = utils.base64_url_decode(current_record_rotation.get('pwd_complexity', ''))
record_resource_uid = current_record_rotation.get('resourceUid')
disabled = current_record_rotation.get('disabled', False)

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])

# Check if we have NOOP rotation for schedule-only operations
noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE'
if target_record and not noop_rotation: # check from record data
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.get('revision', 0)
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 (for non-schedule-only operations)
noop_rotation = str(kwargs.get('noop', False) or False).upper() == 'TRUE'
if target_record and not noop_rotation: # check from record data
noop_field = target_record.get_typed_field('text', 'NOOP')
Expand Down Expand Up @@ -690,9 +833,19 @@ def config_user(_dag, target_record, target_resource_uid, target_config_uid=None
# Get the resource configuration from DAG
resource_uids = _dag.get_all_owners(target_record.record_uid)
if len(resource_uids) > 1:
raise CommandError('', f'{bcolors.FAIL}Record "{target_record.record_uid}" is '
f'associated with multiple resources so you must supply '
f'{bcolors.OKBLUE}"--resource/-rs RESOURCE".{bcolors.ENDC}')
# When processing folders, skip records with multiple resources
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 CommandError('', f'{bcolors.FAIL}Record "{target_record.record_uid}" is '
f'associated with multiple resources so you must supply '
f'{bcolors.OKBLUE}"--resource/-rs RESOURCE".{bcolors.ENDC}')
elif len(resource_uids) == 0:
raise CommandError('',
f'{bcolors.FAIL}Record "{target_record.record_uid}" is not associated with'
Expand Down