"""Manager for a Machine's M2M Auth0 Client Credentials.""" from pydantic import ValidationError from m2mconfig.connectors.assume_role_secrets_manager import ( AssumeRoleSecretsManager, SecretScheduledForDeletionError, ) from m2mconfig.schemas import ClientCredentials from m2mconfig.utils import get_auth0_audience SECRET_NAME = "M2M_AUTH0_CLIENT_CREDENTIALS" class ClientCredentialsManagerException(Exception): """Generic ClientCredentialsManager exception.""" pass class ClientCredentialsDoNotExist(ClientCredentialsManagerException): """Indicate the secret name does not exist in AWS Secrets Manager. When this occurs, engineer should verify the secret has been created in the correct AWS Account under the correct environment and secret name. """ pass class ClientCredentialsInvalidFormat(ClientCredentialsManagerException): """Indicate the secret does not contain valid ClientCredentials. When this occurs, m2mconfig tool should generate a new pair of Auth0 Client id/secret values, and use ClientCredentialsManager to set valid ClientCredentials. """ pass class ClientCredentialsSecretScheduledForDeletion(ClientCredentialsManagerException): """Indicate the secret is scheduled for deletion. When this occurs, m2mconfig tool should ignore this entry and move on in the process. """ pass class ClientCredentialsManager: """Manager for a machine's ClientCredentials. ClientCredentialsManger is responsible for: * where the machine's ClientCredentials are stored * what ClientCredentials must contain to be stored/valid * what ClientCredentials looks like when stored """ def __init__( self, environment: str, machine_name: str, secrets_manager: AssumeRoleSecretsManager, ) -> None: """Initialize ClientCredentialsManager.""" self._environment = environment self._machine_name = machine_name self._secrets_manager = secrets_manager def _get_secret_name(self) -> str: """Get the ClientCredentials secret name based on the environment and machine_name.""" return f"{self._environment}/{self._machine_name}/{SECRET_NAME}" def _get(self) -> ClientCredentials: """Get the client credentials.""" secret_name = self._get_secret_name() if not self.exists(): raise ClientCredentialsDoNotExist(f"{secret_name} does not exist") try: secret = self._secrets_manager.get_secret_string(secret_name) except SecretScheduledForDeletionError as client_error: raise ClientCredentialsSecretScheduledForDeletion( f"Secret name, {secret_name}, is scheduled for deletion." ) from client_error try: return ClientCredentials.model_validate_json(secret) except ValidationError as err: raise ClientCredentialsInvalidFormat( f"{secret_name} could not be parsed." ) from err def exists(self) -> bool: """Return if the ClientCredentials storage location exists.""" secret_name = self._get_secret_name() return self._secrets_manager.does_secret_exist(secret_name) def is_valid(self) -> bool: """Return if the ClientCredentials is stored correctly.""" try: self._get() except ClientCredentialsInvalidFormat: return False return True def set(self, client_id: str, client_secret: str) -> ClientCredentials: """Set and return ClientCredentials.""" secret_name = self._get_secret_name() if not self.exists(): raise ClientCredentialsDoNotExist(f"{secret_name} does not exist") credentials = ClientCredentials( client_id=client_id, client_secret=client_secret, audience=get_auth0_audience(self._environment), ) result = self._secrets_manager.save_secret_string( secret_name, credentials.model_dump_json(), ) if not result: raise ClientCredentialsManagerException( f"Could not save secret {secret_name}" ) return credentials