Skip to content

Commit 3933c5c

Browse files
committed
Add Support for JSON-formatted GENERATE_PASSWORD_COMPLEXITY Enforcement Policy. KC-665
1 parent d63b689 commit 3933c5c

File tree

4 files changed

+100
-10
lines changed

4 files changed

+100
-10
lines changed

keepercommander/commands/enterprise.py

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,29 @@ def register_command_info(aliases, command_info):
282282
user_report_parser.exit = suppress_exit
283283

284284

285+
_DEFAULT_PASSWORD_COMPLEXITY = """[
286+
{
287+
"domains": ["_default_"],
288+
"length": 10,
289+
"lower-use": true,
290+
"lower-min": 2,
291+
"upper-use": true,
292+
"upper-min": 2,
293+
"digit-use": true,
294+
"digit-min": 2,
295+
"special-use": true,
296+
"special-min": 2,
297+
"special": "!@#$%^?();'\\\",.=+[]<>{}&",
298+
"passphrase-allow": true,
299+
"passphrase-length": 5,
300+
"passphrase-capitalize": false,
301+
"passphrase-number": false,
302+
"passphrase-separator": "-",
303+
"apply-privacy-screen": true
304+
}
305+
]"""
306+
307+
285308
def get_user_status_dict(user):
286309

287310
def lock_text(lock):
@@ -1988,9 +2011,15 @@ class EnterpriseRoleCommand(EnterpriseCommand):
19882011
def get_parser(self):
19892012
return enterprise_role_parser
19902013

2014+
@staticmethod
2015+
def expand_file_path(filepath): # type: (str) -> string
2016+
if not os.path.isfile(filepath):
2017+
filepath = os.path.expanduser(filepath)
2018+
return filepath
2019+
19912020
@staticmethod
19922021
def enforcement_value_from_file(filepath):
1993-
filepath = os.path.expanduser(filepath)
2022+
filepath = EnterpriseRoleCommand.expand_file_path(filepath)
19942023
if os.path.isfile(filepath):
19952024
with open(filepath, 'r', encoding='utf-8') as f:
19962025
enforcement_value = f.read()
@@ -2130,17 +2159,44 @@ def execute(self, params, **kwargs):
21302159
logging.warning('Enforcement \"%s\" does not exist', key)
21312160
continue
21322161
enforcement_value = tokens[1].strip()
2162+
if enforcement_type == 'password_complexity' and enforcement_value:
2163+
if not enforcement_value.startswith(file_prefix):
2164+
logging.warning('Enforcement "%s" can be set only from a file.', key)
2165+
logging.warning('Use the following syntax "%s:%s<FILEPATH>" to set enforcement value', key, file_prefix)
2166+
logging.warning('If file does not exist it will be created with the current or default values')
2167+
continue
21332168
if enforcement_value.startswith(file_prefix):
21342169
# Get value from file
21352170
filepath = enforcement_value[len(file_prefix):]
21362171
if filepath:
2137-
enforcement_value = self.enforcement_value_from_file(filepath)
2138-
if enforcement_value is None:
2139-
logging.warning(f'Could not load enforcement value from "{filepath}"')
2172+
filepath = self.expand_file_path(filepath)
2173+
if os.path.isfile(filepath):
2174+
enforcement_value = self.enforcement_value_from_file(filepath)
2175+
if enforcement_value is None:
2176+
logging.warning(f'Could not load enforcement value from "{filepath}"')
2177+
continue
2178+
else:
2179+
template_value = ''
2180+
if enforcement_type == 'password_complexity':
2181+
if len(matched_roles) == 1:
2182+
role_id = matched_roles[0]['role_id']
2183+
role_enforcements = params.enterprise.get('role_enforcements') or []
2184+
enforcements = next((x['enforcements'] for x in role_enforcements if x['role_id'] == role_id), None)
2185+
template_value = enforcements.get(key) if enforcements else None
2186+
if template_value:
2187+
try:
2188+
_ = json.loads(template_value)
2189+
except:
2190+
template_value = ''
2191+
if not template_value:
2192+
template_value = _DEFAULT_PASSWORD_COMPLEXITY
2193+
if template_value:
2194+
with open(filepath, 'wt') as fd:
2195+
fd.write(template_value)
2196+
logging.warning('Enforcement "%s" value has been stored to file "%s"', key, filepath)
2197+
else:
2198+
logging.warning('Enforcement "%s" is skipped. Expected format: KEY:$FILE=<FILEPATH>', key)
21402199
continue
2141-
else:
2142-
logging.warning(f'Enforcement {key} is skipped. Expected format: KEY:$FILE=<FILEPATH>')
2143-
continue
21442200
if enforcement_value:
21452201
if enforcement_type == 'long':
21462202
try:
@@ -2286,6 +2342,40 @@ def execute(self, params, **kwargs):
22862342
else:
22872343
logging.warning('Enforcement \"%s\". Role \"%s\" does not have \"TRANSFER_ACCOUNT\" privilege', key, role['data'].get('displayname', ''))
22882344
continue
2345+
elif enforcement_type =='password_complexity':
2346+
try:
2347+
complexity = json.loads(enforcement_value)
2348+
except Exception as e:
2349+
logging.warning('Error parsing "%s" enforcement JSON value: %s', key, e)
2350+
continue
2351+
if not isinstance(complexity, list):
2352+
if isinstance(complexity, dict):
2353+
complexity = [complexity]
2354+
else:
2355+
logging.warning('Enforcement "%s" should be a JSON array of complexity objects', key)
2356+
continue
2357+
2358+
dc = json.loads(_DEFAULT_PASSWORD_COMPLEXITY)
2359+
default_complexity = dc[0]
2360+
errors = []
2361+
for pc in complexity:
2362+
if not isinstance(pc, dict):
2363+
errors.append(f'Enforcement "{key}" should be a JSON array of complexity objects')
2364+
break
2365+
for c_key, c_value in pc.items():
2366+
if c_key in default_complexity:
2367+
default_type = type(default_complexity[c_key])
2368+
if not isinstance(c_value, default_type):
2369+
errors.append(f'Property "{c_key}" of complexity objects should be a {default_type.__name__}')
2370+
2371+
domains = pc.get('domains')
2372+
if not isinstance(domains, list):
2373+
errors.append(f'Enforcement complexity object should contain "domains" property: a list of domains or ["_default_"]')
2374+
break
2375+
if len(errors) > 0:
2376+
for error in errors:
2377+
logging.warning(error)
2378+
continue
22892379
else:
22902380
logging.warning('Enforcement \"%s\". Value type \"%s\" is not supported', key, enforcement_type)
22912381
continue

keepercommander/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ class PrivilegeScope(enum.IntEnum):
173173
("MASK_CUSTOM_FIELDS", 103, "BOOLEAN", "VAULT_FEATURES"),
174174
("MASK_NOTES", 104, "BOOLEAN", "VAULT_FEATURES"),
175175
("MASK_PASSWORDS_WHILE_EDITING", 105, "BOOLEAN", "VAULT_FEATURES"),
176-
("GENERATED_PASSWORD_COMPLEXITY", 106, "STRING", "VAULT_FEATURES"),
176+
("GENERATED_PASSWORD_COMPLEXITY", 106, "PASSWORD_COMPLEXITY", "VAULT_FEATURES"),
177177
("GENERATED_SECURITY_QUESTION_COMPLEXITY", 109, "STRING", "VAULT_FEATURES"),
178178
("DAYS_BEFORE_DELETED_RECORDS_CLEARED_PERM", 107, "LONG", "VAULT_FEATURES"),
179179
("DAYS_BEFORE_DELETED_RECORDS_AUTO_CLEARED", 108, "LONG", "VAULT_FEATURES"),

keepercommander/enterprise.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def load(self, params): # type: (KeeperParams) -> None
181181
if self._continuationToken:
182182
rq.continuationToken = self._continuationToken
183183
rs = api.communicate_rest(params, rq, 'enterprise/get_enterprise_data_for_user',
184-
rs_type=proto.EnterpriseDataResponse)
184+
rs_type=proto.EnterpriseDataResponse, payload_version=2)
185185

186186
if rs.cacheStatus == proto.CLEAR:
187187
for d in self._data_types.values():

keepercommander/loginv3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,7 @@ def set_user_setting(params: KeeperParams, name: str, value: str):
10401040
@staticmethod
10411041
def accountSummary(params: KeeperParams):
10421042
rq = AccountSummary_pb2.AccountSummaryRequest()
1043-
rq.summaryVersion = 1
1043+
rq.summaryVersion = 3
10441044
return api.communicate_rest(params, rq, 'login/account_summary', rs_type=AccountSummary_pb2.AccountSummaryElements)
10451045

10461046
@staticmethod

0 commit comments

Comments
 (0)