From ebdf1110f985dae00ef19a04c118d4acd4bcaf14 Mon Sep 17 00:00:00 2001 From: Marko Malenic Date: Thu, 16 Jan 2025 14:32:24 +1100 Subject: [PATCH] fix(token-service): generated password should contain required characters --- .../token_service/cognitor/__init__.py | 42 +++++++++++++++---- .../token_service/cognitor/tests.py | 20 +++++++-- .../token_service/rotate_service_user.py | 2 +- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/lib/workload/stateful/stacks/token-service/token_service/cognitor/__init__.py b/lib/workload/stateful/stacks/token-service/token_service/cognitor/__init__.py index e65f4f153..dab8d3982 100644 --- a/lib/workload/stateful/stacks/token-service/token_service/cognitor/__init__.py +++ b/lib/workload/stateful/stacks/token-service/token_service/cognitor/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -import random +import secrets import string from dataclasses import dataclass +from typing import List import boto3 @@ -13,24 +14,49 @@ class ServiceUserDto: email: str +SPECIAL_PASSWORD_CHAR_SET = "_-!@#%&." + + class CognitoTokenService: def __init__(self, user_pool_id: str, user_pool_app_client_id: str): self.client = boto3.client('cognito-idp') self.user_pool_id: str = user_pool_id self.user_pool_app_client_id: str = user_pool_app_client_id - - @staticmethod - def generate_password(length: int = 32) -> str: + self.password_char_sets: List[str] = [ + string.ascii_lowercase, + string.ascii_uppercase, + string.digits, + SPECIAL_PASSWORD_CHAR_SET, + ] + + def generate_password(self, length: int = 32) -> str: """ Must meet the Cognito password requirements policy https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html """ if length < 8: - raise ValueError('Length must be at least 8 characters or more better') - return ''.join( - random.SystemRandom().choice(string.ascii_letters + string.digits + '_-!@#%&.') for _ in range(length) - ) + raise ValueError("Length must be at least 8 characters or more better") + + password = "" + while not self.is_password_valid(password): + password = "".join( + secrets.SystemRandom().choice("".join(self.password_char_sets)) + for _ in range(length) + ) + + return password + + def is_password_valid(self, password: str) -> bool: + """ + Check if the password meets the Cognito password requirements policy + https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-policies.html + """ + for char_set in self.password_char_sets: + if not any(c in char_set for c in password): + return False + + return True def list_users(self, **kwargs) -> dict: if 'UserPoolId' in kwargs.keys(): diff --git a/lib/workload/stateful/stacks/token-service/token_service/cognitor/tests.py b/lib/workload/stateful/stacks/token-service/token_service/cognitor/tests.py index d558ebb8d..786c575e6 100644 --- a/lib/workload/stateful/stacks/token-service/token_service/cognitor/tests.py +++ b/lib/workload/stateful/stacks/token-service/token_service/cognitor/tests.py @@ -5,7 +5,7 @@ import botocore from botocore.stub import Stubber -from . import CognitoTokenService, ServiceUserDto +from . import CognitoTokenService, ServiceUserDto, SPECIAL_PASSWORD_CHAR_SET logger = logging.getLogger() logger.setLevel(logging.INFO) @@ -22,7 +22,7 @@ def setUp(self): self.jjb_dto = ServiceUserDto( username='jjb', email='', - password=CognitoTokenService.generate_password() + password=self.srv.generate_password() ) self.mock_client = botocore.session.get_session().create_client('cognito-idp') @@ -39,11 +39,25 @@ def test_generate_password(self): """ python -m unittest token_service.cognitor.tests.CognitorUnitTest.test_generate_password """ - passwd = CognitoTokenService.generate_password() + passwd = self.srv.generate_password() self.assertIsNotNone(passwd) self.assertEqual(len(passwd), 32) + self.assertTrue(any(c.islower() for c in passwd)) + self.assertTrue(any(c.isupper() for c in passwd)) + self.assertTrue(any(c.isdigit() for c in passwd)) + self.assertTrue(any(c in SPECIAL_PASSWORD_CHAR_SET for c in passwd)) # print(passwd) + def test_is_password_valid(self): + """ + python -m unittest token_service.cognitor.tests.CognitorUnitTest.test_is_password_valid + """ + self.assertTrue(self.srv.is_password_valid("abAB12!@")) # pragma: allowlist secret + self.assertFalse(self.srv.is_password_valid("abAB1234")) # pragma: allowlist secret + self.assertFalse(self.srv.is_password_valid("abAB#&!@")) # pragma: allowlist secret + self.assertFalse(self.srv.is_password_valid("ABCD12!@")) # pragma: allowlist secret + self.assertFalse(self.srv.is_password_valid("abcd12!@")) # pragma: allowlist secret + def test_list_users(self): """ python -m unittest token_service.cognitor.tests.CognitorUnitTest.test_list_users diff --git a/lib/workload/stateful/stacks/token-service/token_service/rotate_service_user.py b/lib/workload/stateful/stacks/token-service/token_service/rotate_service_user.py index 8dcff78c3..809a8190e 100644 --- a/lib/workload/stateful/stacks/token-service/token_service/rotate_service_user.py +++ b/lib/workload/stateful/stacks/token-service/token_service/rotate_service_user.py @@ -124,7 +124,7 @@ def create_secret(service_client, arn, token): logger.info("createSecret: Successfully retrieved secret for %s." % arn) except service_client.exceptions.ResourceNotFoundException: # Generate a random password - current_dict['password'] = CognitoTokenService.generate_password() + current_dict['password'] = token_srv.generate_password() # Put the secret service_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=json.dumps(current_dict), VersionStages=['AWSPENDING']) logger.info("createSecret: Successfully put secret for ARN %s and version %s." % (arn, token))