def credential_list(mocker): credentials = [ Credential( id='1234', revision=1, data_type='credential', enabled=True, name='Test credential', credential_pairs='akjlaklkaj==', data_key='slkjlksfjklsdjf==', cipher_version=2, metadata={}, modified_date=datetime.now(), modified_by='*****@*****.**', documentation='', ), Credential( id='5678', revision=2, data_type='credential', enabled=True, name='Test credential 2', credential_pairs='akjlaklkaj==', data_key='slkjlksfjklsdjf==', cipher_version=2, metadata={}, modified_date=datetime.now(), modified_by='*****@*****.**', documentation='', ), ] return credentials
def get_archive_credential_revisions(id): try: cred = Credential.get(id) except DoesNotExist: logging.warning('Item with id {0} does not exist.'.format(id)) return jsonify({}), 404 if (cred.data_type != 'credential' and cred.data_type != 'archive-credential'): return jsonify({}), 404 revisions = [] _range = range(1, cred.revision + 1) ids = [] for i in _range: ids.append("{0}-{1}".format(id, i)) for revision in Credential.batch_get(ids): revisions.append({ 'id': revision.id, 'name': revision.name, 'revision': revision.revision, 'enabled': revision.enabled, 'modified_date': revision.modified_date, 'modified_by': revision.modified_by, 'documentation': revision.documentation }) return jsonify({ 'revisions': sorted(revisions, key=lambda k: k['revision'], reverse=True) })
def create_credential(): data = request.get_json() 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 = _lowercase_credential_pairs(data['credential_pairs']) _check, ret = _check_credential_pair_values(credential_pairs) if not _check: return jsonify(ret), 400 for cred in Credential.data_type_date_index.query( 'credential', name__eq=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 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) cred = Credential( id='{0}-{1}'.format(id, revision), data_type='archive-credential', name=data['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() ).save(id__null=True) # Make this the current revision cred = Credential( id=id, data_type='credential', name=data['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() ) cred.save() return jsonify({ 'id': cred.id, 'name': cred.name, 'credential_pairs': json.loads(cipher.decrypt(cred.credential_pairs)), 'metadata': cred.metadata, 'revision': cred.revision, 'enabled': cred.enabled, 'modified_date': cred.modified_date, 'modified_by': cred.modified_by })
def get_archive_credential_revisions(id): try: cred = Credential.get(id) except Credential.DoesNotExist: return jsonify({}), 404 if (cred.data_type != 'credential' and cred.data_type != 'archive-credential'): return jsonify({}), 404 revisions = [] _range = range(1, cred.revision + 1) ids = [] for i in _range: ids.append("{0}-{1}".format(id, i)) for revision in Credential.batch_get(ids): revisions.append({ 'id': revision.id, 'name': revision.name, 'revision': revision.revision, 'enabled': revision.enabled, 'modified_date': revision.modified_date, 'modified_by': revision.modified_by }) return jsonify({ 'revisions': sorted( revisions, key=lambda k: k['revision'], reverse=True ) })
def create_dynamodb_tables(): i = 0 # This loop is absurd, but there's race conditions with dynamodb local while i < 5: try: if not Credential.exists(): Credential.create_table( read_capacity_units=10, write_capacity_units=10, wait=True ) if not BlindCredential.exists(): BlindCredential.create_table( read_capacity_units=10, write_capacity_units=10, wait=True ) if not Service.exists(): Service.create_table( read_capacity_units=10, write_capacity_units=10, wait=True ) break except TableError: i = i + 1 time.sleep(2)
def test_diff(mocker): mocker.patch( 'confidant.models.credential.Credential' '._get_decrypted_credential_pairs', return_value={}, ) modified_by = '*****@*****.**' modified_date_old = datetime.now modified_date_new = datetime.now old = Credential( name='test', revision=1, enabled=True, documentation='old', metadata={'hello': 'world'}, modified_by=modified_by, modified_date=modified_date_old, tags=['FINANCIALLY_SENSITIVE', 'IMPORTANT'], ) new = Credential( name='test2', revision=2, enabled=True, documentation='', metadata={'foo': 'bar'}, modified_by=modified_by, modified_date=modified_date_new, tags=['ADMIN_PRIV', 'IMPORTANT'], ) # TODO: figure out how to test decrypted_credential_pairs. Mocking # it is turning out to be difficult. expectedDiff = { 'name': { 'removed': 'test', 'added': 'test2', }, 'metadata': { 'removed': ['hello'], 'added': ['foo'], }, 'documentation': { 'removed': 'old', 'added': '', }, 'modified_by': { 'removed': modified_by, 'added': modified_by, }, 'modified_date': { 'removed': modified_date_old, 'added': modified_date_new, }, 'tags': { 'removed': ['FINANCIALLY_SENSITIVE'], 'added': ['ADMIN_PRIV'], }, } assert old.diff(new) == expectedDiff
def credentials(mocker, now): gmd_mock = mocker.Mock(return_value='test') gmd_mock.range_keyname = 'test' mocker.patch( 'confidant.scripts.restore.Credential._get_meta_data', return_value=gmd_mock, ) mocker.patch( 'confidant.scripts.restore.CredentialArchive._get_meta_data', return_value=gmd_mock, ) archive_credential = CredentialArchive( id='1234', name='test', data_type='credential', revision=2, enabled=True, modified_date=now, modified_by='*****@*****.**', ) credential = Credential.from_archive_credential(archive_credential) archive_revision1 = CredentialArchive( id='1234-1', name='test revision1', data_type='archive-credential', revision=1, enabled=True, modified_date=now, modified_by='*****@*****.**', ) revision1 = Credential.from_archive_credential(archive_revision1) archive_revision2 = Credential( id='1234-2', name='test revision2', data_type='archive-credential', revision=2, enabled=True, modified_date=now, modified_by='*****@*****.**', ) revision2 = Credential.from_archive_credential(archive_revision2) def from_archive_credential(archive_credential): if archive_credential.id == '1234': return credential elif archive_credential.id == '1234-1': return revision1 elif archive_credential.id == '1234-2': return revision2 mocker.patch.object(Credential, 'from_archive_credential', from_archive_credential) return { 'credentials': [credential], 'archive_credentials': [archive_credential], 'revisions': [revision1, revision2], 'archive_revisions': [archive_revision1, archive_revision2], }
def _get_latest_credential_revision(id, revision): i = revision + 1 while True: _id = '{0}-{1}'.format(id, i) try: Credential.get(_id) except Credential.DoesNotExist: return i i = i + 1
def _get_latest_credential_revision(id, revision): i = revision + 1 while True: _id = '{0}-{1}'.format(id, i) try: Credential.get(_id) except DoesNotExist: return i i = i + 1
def test_exempt_from_rotation(mocker): mocker.patch( 'confidant.models.credential.settings.TAGS_EXCLUDING_ROTATION', ['ADMIN_PRIV'], ) cred = Credential(tags=['ADMIN_PRIV']) assert cred.exempt_from_rotation is True cred = Credential(tags=['FINANCIALLY_SENSITIVE']) assert cred.exempt_from_rotation is False
def run(self, days, force, ids): if not settings.DYNAMODB_TABLE_ARCHIVE: logger.error('DYNAMODB_TABLE_ARCHIVE is not configured, exiting.') return 1 if days and ids: logger.error('--days and --ids options are mutually exclusive') return 1 if not days and not ids: logger.error('Either --days or --ids options are required') return 1 credentials = [] if ids: # filter strips an empty string _ids = [_id.strip() for _id in list(filter(None, ids.split(',')))] if not _ids: logger.error('Passed in --ids argument is empty') return 1 for credential in Credential.batch_get(_ids): if credential.enabled: logger.warning('Skipping enabled credential {}'.format( credential.id)) continue credentials.append(credential) else: for credential in Credential.data_type_date_index.query( 'credential'): tz = credential.modified_date.tzinfo now = datetime.now(tz) delta = now - credential.modified_date if not credential.enabled and delta.days > days: credentials.append(credential) self.archive(credentials, force=force)
def get_credential(id): try: cred = Credential.get(id) except Credential.DoesNotExist: return jsonify({}), 404 if (cred.data_type != 'credential' and cred.data_type != 'archive-credential'): return jsonify({}), 404 services = [] for service in Service.data_type_date_index.query('service'): services.append(service.id) if cred.data_type == 'credential': context = id else: context = id.split('-')[0] data_key = keymanager.decrypt_key( cred.data_key, encryption_context={'id': context} ) cipher_version = cred.cipher_version cipher = CipherManager(data_key, cipher_version) _credential_pairs = cipher.decrypt(cred.credential_pairs) _credential_pairs = json.loads(_credential_pairs) return jsonify({ 'id': id, 'name': cred.name, 'credential_pairs': _credential_pairs, 'services': services, 'revision': cred.revision, 'enabled': cred.enabled, 'modified_date': cred.modified_date, 'modified_by': cred.modified_by })
def test_next_rotation_date_never_rotated(mocker): mocker.patch( 'confidant.models.credential.settings.TAGS_EXCLUDING_ROTATION', [], ) cred = Credential(tags=['FINANCIALLY_SENSITIVE']) assert cred.next_rotation_date <= datetime.utcnow()
def get_credential(id): try: cred = Credential.get(id) except DoesNotExist: logging.warning('Item with id {0} does not exist.'.format(id)) return jsonify({}), 404 if (cred.data_type != 'credential' and cred.data_type != 'archive-credential'): return jsonify({}), 404 services = [] for service in Service.data_type_date_index.query('service'): services.append(service.id) if cred.data_type == 'credential': context = id else: context = id.split('-')[0] data_key = keymanager.decrypt_datakey(cred.data_key, encryption_context={'id': context}) cipher_version = cred.cipher_version cipher = CipherManager(data_key, cipher_version) _credential_pairs = cipher.decrypt(cred.credential_pairs) _credential_pairs = json.loads(_credential_pairs) return jsonify({ 'id': id, 'name': cred.name, 'credential_pairs': _credential_pairs, 'metadata': cred.metadata, 'services': services, 'revision': cred.revision, 'enabled': cred.enabled, 'modified_date': cred.modified_date, 'modified_by': cred.modified_by, 'documentation': cred.documentation })
def test_equals(mocker): decrypted_pairs_mock = mocker.patch( 'confidant.models.credential.Credential.decrypted_credential_pairs' ) decrypted_pairs_mock.return_value = {'test': 'me'} cred1 = Credential( name='test', enabled=True, documentation='', metadata={}, ) cred2 = Credential( name='test', enabled=True, documentation='', metadata={}, ) assert cred1.equals(cred2) is True
def test_not_equals(mocker): mocker.patch( 'confidant.models.credential.Credential' '._get_decrypted_credential_pairs', return_value={'test': 'me'}, ) cred1 = Credential( name='test', enabled=True, documentation='', metadata={}, ) cred2 = Credential( name='test2', enabled=True, documentation='', metadata={}, ) assert cred1.equals(cred2) is False
def test_not_equals_different_tags(mocker): decrypted_pairs_mock = mocker.patch( 'confidant.models.credential.Credential.decrypted_credential_pairs') decrypted_pairs_mock.return_value = {'test': 'me'} cred1 = Credential( name='test', enabled=True, documentation='', metadata={}, tags=['ADMIN_PRIV'], ) cred2 = Credential( name='test', enabled=True, documentation='', metadata={}, tags=['FINANCIALLY_SENSITIVE'], ) assert cred1.equals(cred2) is False
def test_get_revision_ids_for_credential(): credential = Credential( id='1234', revision=3, name='test', enabled=True, ) assert credentialmanager.get_revision_ids_for_credential(credential) == [ '1234-1', '1234-2', '1234-3', ]
def delete(self, deletes, force=False): _deletes = ', '.join([delete.id for delete in deletes]) if not force: logger.info( 'Would have deleted credential and revisions: {}'.format( _deletes, )) return logger.info('Deleting credential and revisions: {}'.format(_deletes, )) with Credential.batch_write() as batch: for delete in deletes: batch.delete(delete) stats.incr('archive.delete.success')
def get_archive_credential_revisions(id): if not acl_module_check( resource_type='credential', action='metadata', resource_id=id): msg = "{} does not have access to credential {} revisions".format( authnz.get_logged_in_user(), id) error_msg = {'error': msg} return jsonify(error_msg), 403 try: cred = Credential.get(id) except DoesNotExist: logging.warning('Item with id {0} does not exist.'.format(id)) return jsonify({}), 404 if (cred.data_type != 'credential' and cred.data_type != 'archive-credential'): return jsonify({}), 404 _range = range(1, cred.revision + 1) ids = [] for i in _range: ids.append("{0}-{1}".format(id, i)) revisions_response = RevisionsResponse.from_credentials( Credential.batch_get(ids)) return revisions_response_schema.dumps(revisions_response)
def test_credential_archive(mocker): mocker.patch( 'confidant.models.credential.Credential' '._get_decrypted_credential_pairs', return_value={}, ) cred = Credential( name='test', enabled=True, documentation='', metadata={}, ) archive_cred = CredentialArchive.from_credential(cred) # TODO: do a more thorough equality test here. assert cred.id == archive_cred.id
def restore(self, archive_credentials, force): for archive_credential in archive_credentials: saves = [] # restore the current record credential = Credential.from_archive_credential( archive_credential, ) saves.append(credential) # fetch and restore every revision _range = range(1, credential.revision + 1) ids = [] for i in _range: ids.append("{0}-{1}".format(credential.id, i)) archive_revisions = CredentialArchive.batch_get(ids) for archive_revision in archive_revisions: revision = Credential.from_archive_credential( archive_revision, ) saves.append(revision) try: self.save(saves, force=force) except Exception: logger.exception('Failed to batch save {}.'.format( credential.id)) stats.incr('restore.save.failure') continue
def diff_credential(id, old_revision, new_revision): if not acl_module_check( resource_type='credential', action='metadata', resource_id=id): msg = "{} does not have access to diff credential {}".format( authnz.get_logged_in_user(), id) error_msg = {'error': msg, 'reference': id} return jsonify(error_msg), 403 try: old_credential = Credential.get('{}-{}'.format(id, old_revision)) except DoesNotExist: return jsonify({'error': 'Credential not found.'}), 404 if old_credential.data_type != 'archive-credential': msg = 'id provided is not a credential.' return jsonify({'error': msg}), 400 try: new_credential = Credential.get('{}-{}'.format(id, new_revision)) except DoesNotExist: logging.warning('Item with id {0} does not exist.'.format(id)) return jsonify({}), 404 if new_credential.data_type != 'archive-credential': msg = 'id provided is not a credential.' return jsonify({'error': msg}), 400 return jsonify(old_credential.diff(new_credential))
def archive_credential(mocker): return Credential( id='123-1', revision=1, data_type='archive-credential', enabled=True, name='Archive credential', credential_pairs='akjlaklkaj==', data_key='slkjlksfjklsdjf==', cipher_version=2, metadata={}, modified_date=datetime.now(), modified_by='*****@*****.**', documentation='', tags=['OLD TAG'], )
def credential(mocker): return Credential( id='1234', revision=1, data_type='credential', enabled=True, name='Test credential', credential_pairs='akjlaklkaj==', data_key='slkjlksfjklsdjf==', cipher_version=2, metadata={}, modified_date=datetime.now(), modified_by='*****@*****.**', documentation='', last_rotation_date=datetime(2020, 1, 1, tzinfo=pytz.utc), )
def test_next_rotation_date_last_rotation_present(mocker): mocker.patch( 'confidant.models.credential.settings.TAGS_EXCLUDING_ROTATION', [], ) mocker.patch( 'confidant.models.credential.settings.MAXIMUM_ROTATION_DAYS', 100, ) mocker.patch( 'confidant.models.credential.settings.ROTATION_DAYS_CONFIG', {'FINANCIALLY_SENSITIVE': 30}, ) cred = Credential( tags=['FINANCIALLY_SENSITIVE'], last_rotation_date=datetime(2020, 1, 1), ) assert cred.next_rotation_date == datetime(2020, 1, 31)
def _get_credentials(credential_ids): credentials = [] with stats.timer('service_batch_get_credentials'): for cred in Credential.batch_get(credential_ids): data_key = keymanager.decrypt_key( cred.data_key, encryption_context={'id': cred.id}) cipher_version = cred.cipher_version cipher = CipherManager(data_key, cipher_version) _credential_pairs = cipher.decrypt(cred.credential_pairs) _credential_pairs = json.loads(_credential_pairs) credentials.append({ 'id': cred.id, 'name': cred.name, 'enabled': cred.enabled, 'revision': cred.revision, 'credential_pairs': _credential_pairs }) return credentials
def get_credential(id): if not acl_module_check( resource_type='credential', action='metadata', resource_id=id): msg = "{} does not have access to credential {}".format( authnz.get_logged_in_user(), id) error_msg = {'error': msg, 'reference': id} return jsonify(error_msg), 403 try: credential = Credential.get(id) except DoesNotExist: logging.warning('Item with id {0} does not exist.'.format(id)) return jsonify({}), 404 if (credential.data_type != 'credential' and credential.data_type != 'archive-credential'): return jsonify({}), 404 permissions = { 'metadata': True, 'get': False, 'update': acl_module_check(resource_type='credential', action='update', resource_id=id), } include_credential_pairs = False if acl_module_check(resource_type='credential', action='get', resource_id=id): permissions['get'] = True include_credential_pairs = True log_line = "{0} get credential {1}".format(authnz.get_logged_in_user(), id) logging.info(log_line) credential_response = CredentialResponse.from_credential( credential, include_credential_keys=True, include_credential_pairs=include_credential_pairs, ) credential_response.permissions = permissions return credential_response_schema.dumps(credential_response)
def _get_credentials(credential_ids): credentials = [] with stats.timer('service_batch_get_credentials'): for cred in Credential.batch_get(credential_ids): data_key = keymanager.decrypt_key( cred.data_key, encryption_context={'id': cred.id} ) cipher_version = cred.cipher_version cipher = CipherManager(data_key, cipher_version) _credential_pairs = cipher.decrypt(cred.credential_pairs) _credential_pairs = json.loads(_credential_pairs) credentials.append({ 'id': cred.id, 'name': cred.name, 'enabled': cred.enabled, 'revision': cred.revision, 'credential_pairs': _credential_pairs }) return credentials
def archive(self, credentials, force): services = list(Service.data_type_date_index.query('service')) for credential in credentials: if self.credential_in_service(credential.id, services): msg = ('Skipping archival of disabled credential {}, as it' ' is still mapped to a service.') logger.warning(msg.format(credential.id)) continue saves = [] deletes = [] # save the current record. archive_credential = CredentialArchive.from_credential( credential, ) saves.append(archive_credential) # fetch and save every revision revisions = Credential.batch_get( credentialmanager.get_revision_ids_for_credential(credential)) for revision in revisions: archive_revision = CredentialArchive.from_credential( revision, ) saves.append(archive_revision) deletes.append(revision) deletes.append(credential) try: self.save(saves, force=force) except Exception: logger.exception( 'Failed to batch save {}, skipping deletion.'.format( credential.id)) stats.incr('archive.save.failure') continue try: self.delete(deletes, force=force) except Exception: logger.exception('Failed to batch delete {}'.format( credential.id)) stats.incr('archive.delete.failure')
def save(self, saves, force=False): # Do not restore a credential if it exists in the primary table. # We do this check at the point of all saves so that we can # restore revisions, if one of them failed to restore for some # reason. _saves = [] for save in saves: if self.credential_exists(save.id): continue _saves.append(save) if not _saves: return save_msg = ', '.join([save.id for save in _saves]) if not force: logger.info( 'Would have restored credential and revisions: {}'.format( save_msg, )) return logger.info('Restoring credential and revisions: {}'.format( save_msg, )) with Credential.batch_write() as batch: for save in _saves: batch.save(save) stats.incr('restore.save.success')
import time from confidant import app from confidant.models.credential import Credential from confidant.models.service import Service from pynamodb.exceptions import TableError # Only used when using dynamodb local if app.config.get('DYNAMODB_URL'): i = 0 # This loop is absurd, but there's race conditions with dynamodb local while i < 5: try: if not Credential.exists(): Credential.create_table( read_capacity_units=10, write_capacity_units=10, wait=True ) if not Service.exists(): Service.create_table( read_capacity_units=10, write_capacity_units=10, wait=True ) break except TableError: i = i + 1 time.sleep(2)
def update_credential(id): try: _cred = Credential.get(id) except Credential.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() update = {} revision = _cred.revision + 1 update['name'] = data.get('name', _cred.name) if 'enabled' in data: if not isinstance(data['enabled'], bool): return jsonify({'error': 'Enabled must be a boolean.'}), 400 update['enabled'] = data['enabled'] else: update['enabled'] = _cred.enabled services = _get_services_for_credential(id) if 'credential_pairs' in data: # Ensure credential pair keys are lowercase credential_pairs = _lowercase_credential_pairs( data['credential_pairs'] ) if not _check_credential_pair_uniqueness(credential_pairs): ret = {'error': 'credential pairs must be key: value'} return jsonify(ret), 400 # Ensure credential pairs don't conflicts with pairs from other # services conflicts = _pair_key_conflicts_for_services( id, credential_pairs, services ) if conflicts: ret = { 'error': 'Conflicting key pairs in mapped service.', 'conflicts': conflicts } return jsonify(ret), 400 update['credential_pairs'] = json.dumps(credential_pairs) else: data_key = keymanager.decrypt_key( _cred.data_key, encryption_context={'id': id} ) cipher_version = _cred.cipher_version cipher = CipherManager(data_key, cipher_version) update['credential_pairs'] = cipher.decrypt(_cred.credential_pairs) data_key = keymanager.create_datakey(encryption_context={'id': id}) cipher = CipherManager(data_key['plaintext'], version=2) credential_pairs = cipher.encrypt(update['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=credential_pairs, enabled=update['enabled'], revision=revision, data_key=data_key['ciphertext'], cipher_version=2, modified_by=authnz.get_logged_in_user_email() ).save(id__null=True) except PutError as e: logging.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=credential_pairs, enabled=update['enabled'], revision=revision, data_key=data_key['ciphertext'], cipher_version=2, modified_by=authnz.get_logged_in_user_email() ) cred.save() except PutError as e: logging.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) return jsonify({ 'id': cred.id, 'name': cred.name, 'credential_pairs': json.loads(cipher.decrypt(cred.credential_pairs)), 'revision': cred.revision, 'enabled': cred.enabled, 'modified_date': cred.modified_date, 'modified_by': cred.modified_by })
def test_next_rotation_date_no_rotation_required(mocker): mocker.patch( 'confidant.models.credential.settings.TAGS_EXCLUDING_ROTATION', ['ADMIN_PRIV'], ) assert Credential(tags=['ADMIN_PRIV']).next_rotation_date is None
def update_credential(id): 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() update = {} revision = _get_latest_credential_revision(id, _cred.revision) update['name'] = data.get('name', _cred.name) if 'enabled' in data: if not isinstance(data['enabled'], bool): return jsonify({'error': 'Enabled must be a boolean.'}), 400 update['enabled'] = data['enabled'] else: update['enabled'] = _cred.enabled if not isinstance(data.get('metadata', {}), dict): return jsonify({'error': 'metadata must be a dict'}), 400 services = _get_services_for_credential(id) if 'credential_pairs' in data: # Ensure credential pair keys are lowercase credential_pairs = _lowercase_credential_pairs( data['credential_pairs']) _check, ret = _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 = _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 update['credential_pairs'] = json.dumps(credential_pairs) else: data_key = keymanager.decrypt_datakey(_cred.data_key, encryption_context={'id': id}) cipher_version = _cred.cipher_version cipher = CipherManager(data_key, cipher_version) update['credential_pairs'] = cipher.decrypt(_cred.credential_pairs) data_key = keymanager.create_datakey(encryption_context={'id': id}) cipher = CipherManager(data_key['plaintext'], version=2) credential_pairs = cipher.encrypt(update['credential_pairs']) update['metadata'] = data.get('metadata', _cred.metadata) update['documentation'] = data.get('documentation', _cred.documentation) # 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 # Try to save to the archive try: Credential(id='{0}-{1}'.format(id, revision), name=update['name'], data_type='archive-credential', credential_pairs=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']).save(id__null=True) except PutError as e: logging.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=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']) cred.save() except PutError as e: logging.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]) return jsonify({ 'id': cred.id, 'name': cred.name, 'credential_pairs': json.loads(cipher.decrypt(cred.credential_pairs)), 'metadata': cred.metadata, 'revision': cred.revision, 'enabled': cred.enabled, 'modified_date': cred.modified_date, 'modified_by': cred.modified_by, 'documentation': cred.documentation })
def credential_exists(self, credential_id): try: Credential.get(credential_id) return True except DoesNotExist: return False