From cb10bb5fee59673d313444279de297ed2838de28 Mon Sep 17 00:00:00 2001 From: Alejandro Roiz Walss <26120069+alejandroroiz@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:03:05 -0600 Subject: [PATCH] Profiling credentials (#424) Add a timer stat to update and create credentials --- VERSION | 2 +- confidant/routes/credentials.py | 448 ++++++++++++++++---------------- 2 files changed, 228 insertions(+), 222 deletions(-) diff --git a/VERSION b/VERSION index db0785f2..54358db7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.5.3 +6.5.4 diff --git a/confidant/routes/credentials.py b/confidant/routes/credentials.py index e186f870..f03293b3 100644 --- a/confidant/routes/credentials.py +++ b/confidant/routes/credentials.py @@ -25,7 +25,7 @@ webhook, ) from confidant.services.ciphermanager import CipherManager -from confidant.utils import maintenance, misc +from confidant.utils import maintenance, misc, stats from confidant.utils.dynamodb import decode_last_evaluated_key logger = logging.getLogger(__name__) @@ -592,92 +592,95 @@ def create_credential(): correct format, or a required field was not provided. :statuscode 403: Client does not have access to create credentials. ''' - if not acl_module_check(resource_type='credential', action='create'): - msg = "{} does not have access to create credentials".format( - authnz.get_logged_in_user() + with stats.timer('create_credential'): + if not acl_module_check(resource_type='credential', action='create'): + msg = "{} does not have access to create credentials".format( + authnz.get_logged_in_user() + ) + error_msg = {'error': msg} + return jsonify(error_msg), 403 + + data = request.get_json() + if not data.get('documentation') \ + and settings.get('ENFORCE_DOCUMENTATION'): + return jsonify({'error': 'documentation is a required field'}), 400 + if not data.get('credential_pairs'): + error = {'error': 'credential_pairs is a required field'} + return jsonify(error), 400 + if not isinstance(data.get('metadata', {}), dict): + return jsonify({'error': 'metadata must be a dict'}), 400 + # Ensure credential pair keys are lowercase + credential_pairs = credentialmanager.lowercase_credential_pairs( + data['credential_pairs'] ) - error_msg = {'error': msg} - return jsonify(error_msg), 403 + _check, ret = credentialmanager.check_credential_pair_values( + credential_pairs + ) + if not _check: + return jsonify(ret), 400 + for cred in Credential.data_type_date_index.query( + 'credential', filter_condition=Credential.name == data['name']): + # Conflict, the name already exists + msg = 'Name already exists. See id: {0}'.format(cred.id) + return jsonify({'error': msg, 'reference': cred.id}), 409 + # Generate an initial stable ID to allow name changes + id = str(uuid.uuid4()).replace('-', '') + # Try to save to the archive + revision = 1 + for key, value in credential_pairs.items(): + value = escape(value) + credential_pairs[key] = value + credential_pairs = json.dumps(credential_pairs) + data_key = keymanager.create_datakey(encryption_context={'id': id}) + cipher = CipherManager(data_key['plaintext'], version=2) + credential_pairs = cipher.encrypt(credential_pairs) + last_rotation_date = misc.utcnow() - data = request.get_json() - if not data.get('documentation') and settings.get('ENFORCE_DOCUMENTATION'): - return jsonify({'error': 'documentation is a required field'}), 400 - if not data.get('credential_pairs'): - return jsonify({'error': 'credential_pairs is a required field'}), 400 - if not isinstance(data.get('metadata', {}), dict): - return jsonify({'error': 'metadata must be a dict'}), 400 - # Ensure credential pair keys are lowercase - credential_pairs = credentialmanager.lowercase_credential_pairs( - data['credential_pairs'] - ) - _check, ret = credentialmanager.check_credential_pair_values( - credential_pairs - ) - if not _check: - return jsonify(ret), 400 - for cred in Credential.data_type_date_index.query( - 'credential', filter_condition=Credential.name == data['name']): - # Conflict, the name already exists - msg = 'Name already exists. See id: {0}'.format(cred.id) - return jsonify({'error': msg, 'reference': cred.id}), 409 - # Generate an initial stable ID to allow name changes - id = str(uuid.uuid4()).replace('-', '') - # Try to save to the archive - revision = 1 - for key, value in credential_pairs.items(): - value = escape(value) - credential_pairs[key] = value - credential_pairs = json.dumps(credential_pairs) - data_key = keymanager.create_datakey(encryption_context={'id': id}) - cipher = CipherManager(data_key['plaintext'], version=2) - credential_pairs = cipher.encrypt(credential_pairs) - last_rotation_date = misc.utcnow() - - sanitized_name = escape(data['name']) - cred = Credential( - id='{0}-{1}'.format(id, revision), - data_type='archive-credential', - name=sanitized_name, - credential_pairs=credential_pairs, - metadata=data.get('metadata'), - revision=revision, - enabled=data.get('enabled'), - data_key=data_key['ciphertext'], - cipher_version=2, - modified_by=authnz.get_logged_in_user(), - documentation=data.get('documentation'), - tags=data.get('tags', []), - last_rotation_date=last_rotation_date, - ).save() - # Make this the current revision - cred = Credential( - id=id, - data_type='credential', - name=sanitized_name, - credential_pairs=credential_pairs, - metadata=data.get('metadata'), - revision=revision, - enabled=data.get('enabled'), - data_key=data_key['ciphertext'], - cipher_version=2, - modified_by=authnz.get_logged_in_user(), - documentation=data.get('documentation'), - tags=data.get('tags', []), - last_rotation_date=last_rotation_date, - ) - cred.save() - permissions = { - 'metadata': True, - 'get': True, - 'update': True, - } - credential_response = CredentialResponse.from_credential( - cred, - include_credential_keys=True, - include_credential_pairs=True, - ) - credential_response.permissions = permissions - return credential_response_schema.dumps(credential_response) + sanitized_name = escape(data['name']) + cred = Credential( + id='{0}-{1}'.format(id, revision), + data_type='archive-credential', + name=sanitized_name, + credential_pairs=credential_pairs, + metadata=data.get('metadata'), + revision=revision, + enabled=data.get('enabled'), + data_key=data_key['ciphertext'], + cipher_version=2, + modified_by=authnz.get_logged_in_user(), + documentation=data.get('documentation'), + tags=data.get('tags', []), + last_rotation_date=last_rotation_date, + ).save() + # Make this the current revision + cred = Credential( + id=id, + data_type='credential', + name=sanitized_name, + credential_pairs=credential_pairs, + metadata=data.get('metadata'), + revision=revision, + enabled=data.get('enabled'), + data_key=data_key['ciphertext'], + cipher_version=2, + modified_by=authnz.get_logged_in_user(), + documentation=data.get('documentation'), + tags=data.get('tags', []), + last_rotation_date=last_rotation_date, + ) + cred.save() + permissions = { + 'metadata': True, + 'get': True, + 'update': True, + } + credential_response = CredentialResponse.from_credential( + cred, + include_credential_keys=True, + include_credential_pairs=True, + ) + credential_response.permissions = permissions + return credential_response_schema.dumps(credential_response) @blueprint.route('/v1/credentials//services', methods=['GET']) @@ -790,146 +793,149 @@ def update_credential(id): :statuscode 403: Client does not have access to update the provided credential ID. ''' - if not acl_module_check(resource_type='credential', - action='update', - resource_id=id): - msg = "{} does not have access to update credential {}".format( - authnz.get_logged_in_user(), - id - ) - error_msg = {'error': msg, 'reference': id} - return jsonify(error_msg), 403 + with stats.timer('update_credential'): + if not acl_module_check(resource_type='credential', + action='update', + resource_id=id): + msg = "{} does not have access to update credential {}".format( + authnz.get_logged_in_user(), + id + ) + error_msg = {'error': msg, 'reference': id} + return jsonify(error_msg), 403 - try: - _cred = Credential.get(id) - except DoesNotExist: - return jsonify({'error': 'Credential not found.'}), 404 - if _cred.data_type != 'credential': - msg = 'id provided is not a credential.' - return jsonify({'error': msg}), 400 - - data = request.get_json() - if not isinstance(data.get('metadata', {}), dict): - return jsonify({'error': 'metadata must be a dict'}), 400 - - update = { - 'name': data.get('name', _cred.name), - 'last_rotation_date': _cred.last_rotation_date, - 'credential_pairs': _cred.credential_pairs, - 'enabled': _cred.enabled, - 'metadata': data.get('metadata', _cred.metadata), - 'documentation': data.get('documentation', _cred.documentation), - 'tags': data.get('tags', _cred.tags), - } - # Enforce documentation, EXCEPT if we are restoring an old revision - if (not update['documentation'] and - settings.get('ENFORCE_DOCUMENTATION') and - not data.get('revision')): - return jsonify({'error': 'documentation is a required field'}), 400 - if 'enabled' in data: - if not isinstance(data['enabled'], bool): - return jsonify({'error': 'Enabled must be a boolean.'}), 400 - update['enabled'] = data['enabled'] - - services = servicemanager.get_services_for_credential(id) - revision = credentialmanager.get_latest_credential_revision( - id, - _cred.revision - ) - if 'credential_pairs' in data: - # Ensure credential pair keys are lowercase - credential_pairs = credentialmanager.lowercase_credential_pairs( - data['credential_pairs'] - ) - _check, ret = credentialmanager.check_credential_pair_values( - credential_pairs - ) - if not _check: - return jsonify(ret), 400 - # Ensure credential pairs don't conflicts with pairs from other - # services - conflicts = servicemanager.pair_key_conflicts_for_services( + try: + _cred = Credential.get(id) + except DoesNotExist: + return jsonify({'error': 'Credential not found.'}), 404 + if _cred.data_type != 'credential': + msg = 'id provided is not a credential.' + return jsonify({'error': msg}), 400 + + data = request.get_json() + if not isinstance(data.get('metadata', {}), dict): + return jsonify({'error': 'metadata must be a dict'}), 400 + + update = { + 'name': data.get('name', _cred.name), + 'last_rotation_date': _cred.last_rotation_date, + 'credential_pairs': _cred.credential_pairs, + 'enabled': _cred.enabled, + 'metadata': data.get('metadata', _cred.metadata), + 'documentation': data.get('documentation', _cred.documentation), + 'tags': data.get('tags', _cred.tags), + } + # Enforce documentation, EXCEPT if we are restoring an old revision + if (not update['documentation'] and + settings.get('ENFORCE_DOCUMENTATION') and + not data.get('revision')): + return jsonify({'error': 'documentation is a required field'}), 400 + if 'enabled' in data: + if not isinstance(data['enabled'], bool): + return jsonify({'error': 'Enabled must be a boolean.'}), 400 + update['enabled'] = data['enabled'] + + services = servicemanager.get_services_for_credential(id) + revision = credentialmanager.get_latest_credential_revision( id, - list(credential_pairs.keys()), - services - ) - if conflicts: - ret = { - 'error': 'Conflicting key pairs in mapped service.', - 'conflicts': conflicts - } - return jsonify(ret), 400 - - # If the credential pair passed in the update is different from the - # decrypted credential pair of the most recent revision, assume that - # this is a new credential pair and update last_rotation_date - if credential_pairs != _cred.decrypted_credential_pairs: - update['last_rotation_date'] = misc.utcnow() - data_key = keymanager.create_datakey(encryption_context={'id': id}) - cipher = CipherManager(data_key['plaintext'], version=2) - update['credential_pairs'] = cipher.encrypt( - json.dumps(credential_pairs) + _cred.revision ) - - # Try to save to the archive - try: - Credential( - id='{0}-{1}'.format(id, revision), - name=update['name'], - data_type='archive-credential', - credential_pairs=update['credential_pairs'], - metadata=update['metadata'], - enabled=update['enabled'], - revision=revision, - data_key=data_key['ciphertext'], - cipher_version=2, - modified_by=authnz.get_logged_in_user(), - documentation=update['documentation'], - tags=update['tags'], - last_rotation_date=update['last_rotation_date'], - ).save() - except PutError as e: - logger.error(e) - return jsonify({'error': 'Failed to add credential to archive.'}), 500 - try: - cred = Credential( - id=id, - name=update['name'], - data_type='credential', - credential_pairs=update['credential_pairs'], - metadata=update['metadata'], - enabled=update['enabled'], - revision=revision, - data_key=data_key['ciphertext'], - cipher_version=2, - modified_by=authnz.get_logged_in_user(), - documentation=update['documentation'], - tags=update['tags'], - last_rotation_date=update['last_rotation_date'], + if 'credential_pairs' in data: + # Ensure credential pair keys are lowercase + credential_pairs = credentialmanager.lowercase_credential_pairs( + data['credential_pairs'] + ) + _check, ret = credentialmanager.check_credential_pair_values( + credential_pairs + ) + if not _check: + return jsonify(ret), 400 + # Ensure credential pairs don't conflicts with pairs from other + # services + conflicts = servicemanager.pair_key_conflicts_for_services( + id, + list(credential_pairs.keys()), + services + ) + if conflicts: + ret = { + 'error': 'Conflicting key pairs in mapped service.', + 'conflicts': conflicts + } + return jsonify(ret), 400 + + # If the credential pair passed in the update is different from the + # decrypted credential pair of the most recent revision, assume that + # this is a new credential pair and update last_rotation_date + if credential_pairs != _cred.decrypted_credential_pairs: + update['last_rotation_date'] = misc.utcnow() + data_key = keymanager.create_datakey(encryption_context={'id': id}) + cipher = CipherManager(data_key['plaintext'], version=2) + update['credential_pairs'] = cipher.encrypt( + json.dumps(credential_pairs) + ) + + # Try to save to the archive + try: + Credential( + id='{0}-{1}'.format(id, revision), + name=update['name'], + data_type='archive-credential', + credential_pairs=update['credential_pairs'], + metadata=update['metadata'], + enabled=update['enabled'], + revision=revision, + data_key=data_key['ciphertext'], + cipher_version=2, + modified_by=authnz.get_logged_in_user(), + documentation=update['documentation'], + tags=update['tags'], + last_rotation_date=update['last_rotation_date'], + ).save() + except PutError as e: + logger.error(e) + error = {'error': 'Failed to add credential to archive.'} + return jsonify(error), 500 + try: + cred = Credential( + id=id, + name=update['name'], + data_type='credential', + credential_pairs=update['credential_pairs'], + metadata=update['metadata'], + enabled=update['enabled'], + revision=revision, + data_key=data_key['ciphertext'], + cipher_version=2, + modified_by=authnz.get_logged_in_user(), + documentation=update['documentation'], + tags=update['tags'], + last_rotation_date=update['last_rotation_date'], + ) + cred.save() + except PutError as e: + logger.error(e) + error = {'error': 'Failed to update active credential.'} + return jsonify(error), 500 + + if services: + service_names = [x.id for x in services] + msg = 'Updated credential "{0}" ({1}); Revision {2}' + msg = msg.format(cred.name, cred.id, cred.revision) + graphite.send_event(service_names, msg) + webhook.send_event('credential_update', service_names, [cred.id]) + permissions = { + 'metadata': True, + 'get': True, + 'update': True, + } + credential_response = CredentialResponse.from_credential( + cred, + include_credential_keys=True, + include_credential_pairs=True, ) - cred.save() - except PutError as e: - logger.error(e) - return jsonify({'error': 'Failed to update active credential.'}), 500 - - if services: - service_names = [x.id for x in services] - msg = 'Updated credential "{0}" ({1}); Revision {2}' - msg = msg.format(cred.name, cred.id, cred.revision) - graphite.send_event(service_names, msg) - webhook.send_event('credential_update', service_names, [cred.id]) - permissions = { - 'metadata': True, - 'get': True, - 'update': True, - } - credential_response = CredentialResponse.from_credential( - cred, - include_credential_keys=True, - include_credential_pairs=True, - ) - credential_response.permissions = permissions - return credential_response_schema.dumps(credential_response) + credential_response.permissions = permissions + return credential_response_schema.dumps(credential_response) @blueprint.route('/v1/credentials//', methods=['PUT'])