Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a credential revert endpoint for credential history #213

Merged
merged 1 commit into from
Dec 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions confidant/models/credential.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from datetime import datetime

from pynamodb.models import Model
Expand All @@ -14,6 +15,8 @@
from confidant.app import app
from confidant.models.session_cls import DDBSession
from confidant.models.connection_cls import DDBConnection
from confidant.services import keymanager
from confidant.services.ciphermanager import CipherManager


class DataTypeDateIndex(GlobalSecondaryIndex):
Expand Down Expand Up @@ -48,3 +51,32 @@ class Meta:
modified_date = UTCDateTimeAttribute(default=datetime.now)
modified_by = UnicodeAttribute()
documentation = UnicodeAttribute(null=True)

def equals(self, other_cred):
if self.name != other_cred.name:
return False
if self.decrypted_credential_pairs != other_cred.decrypted_credential_pairs: # noqa:E501
return False
if self.metadata != other_cred.metadata:
return False
if self.enabled != other_cred.enabled:
return False
if self.documentation != other_cred.documentation:
return False
return True

@property
def decrypted_credential_pairs(self):
if self.data_type == 'credential':
context = self.id
else:
context = self.id.split('-')[0]
data_key = keymanager.decrypt_datakey(
self.data_key,
encryption_context={'id': context}
)
cipher_version = self.cipher_version
cipher = CipherManager(data_key, cipher_version)
_credential_pairs = cipher.decrypt(self.credential_pairs)
_credential_pairs = json.loads(_credential_pairs)
return _credential_pairs
1 change: 1 addition & 0 deletions confidant/public/modules/common/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ARCHIVE_SERVICES: 'v1/archive/services',
ARCHIVE_SERVICE_REVISIONS: 'v1/archive/services/:id/:revision',
CREDENTIAL: 'v1/credentials/:id',
CREDENTIAL_REVISION: 'v1/credentials/:id/:revision',
CREDENTIAL_SERVICES: 'v1/credentials/:id/services',
CREDENTIALS: 'v1/credentials',
ARCHIVE_CREDENTIALS: 'v1/archive/credentials',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,7 @@

$scope.revertToDiffRevision = function() {
var deferred = $q.defer();
if (angular.equals($scope.diffCredential.name, $scope.currentCredential.name) &&
angular.equals($scope.diffCredential.credential_pairs, $scope.currentCredential.credential_pairs) &&
angular.equals($scope.diffCredential.metadata, $scope.currentCredential.metadata) &&
angular.equals($scope.diffCredential.enabled, $scope.currentCredential.enabled)) {
$scope.saveError = 'Can not revert to revision ' + $scope.diffCredential.revision + '. No difference between it and current revision.';
deferred.reject();
return deferred.promise;
}
Credential.update({'id': $scope.credentialId}, $scope.diffCredential).$promise.then(function(newCredential) {
Credential.revert({'id': $scope.credentialId, revision: $scope.diffCredential.revision}).$promise.then(function(newCredential) {
deferred.resolve();
ResourceArchiveService.updateResourceArchive();
$location.path('/history/credential/' + newCredential.id + '-' + newCredential.revision);
Expand Down
5 changes: 3 additions & 2 deletions confidant/public/modules/resources/services/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
}])

.factory('credentials.credential', ['$resource', 'CONFIDANT_URLS', function($resource, CONFIDANT_URLS) {
return $resource(CONFIDANT_URLS.CREDENTIAL, {id: '@id'}, {
update: {method: 'PUT', isArray: false}
return $resource(CONFIDANT_URLS.CREDENTIAL, {id: '@id', revision: '@revision'}, {
update: {method: 'PUT', isArray: false},
revert: {method: 'PUT', isArray: false, url: CONFIDANT_URLS.CREDENTIAL_REVISION}
});
}])

Expand Down
155 changes: 122 additions & 33 deletions confidant/routes/v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,35 +361,35 @@ def map_service_credentials(id):
})


@app.route('/v1/services/<id>/<revision>', methods=['PUT'])
@app.route('/v1/services/<id>/<to_revision>', methods=['PUT'])
@authnz.require_auth
@authnz.require_csrf_token
@maintenance.check_maintenance_mode
def revert_service_to_revision(id, revision):
def revert_service_to_revision(id, to_revision):
try:
current_service = Service.get(id)
if current_service.data_type != 'service':
msg = 'id provided is not a service.'
return jsonify({'error': msg}), 400
new_revision = servicemanager.get_latest_service_revision(
id,
current_service.revision
)
except DoesNotExist:
logging.warning(
'Item with id {0} does not exist.'.format(id)
)
return jsonify({}), 404
if current_service.data_type != 'service':
msg = 'id provided is not a service.'
return jsonify({'error': msg}), 400
new_revision = servicemanager.get_latest_service_revision(
id,
current_service.revision
)
try:
revert_service = Service.get('{}-{}'.format(id, revision))
if revert_service.data_type != 'archive-service':
msg = 'id provided is not a service.'
return jsonify({'error': msg}), 400
revert_service = Service.get('{}-{}'.format(id, to_revision))
except DoesNotExist:
logging.warning(
'Item with id {0} does not exist.'.format(id)
)
return jsonify({}), 404
if revert_service.data_type != 'archive-service':
msg = 'id provided is not a service.'
return jsonify({'error': msg}), 400
if revert_service.equals(current_service):
ret = {
'error': 'No difference between old and new service.'
Expand Down Expand Up @@ -498,22 +498,10 @@ def get_credential(id):
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,
'credential_pairs': cred.decrypted_credential_pairs,
'metadata': cred.metadata,
'services': services,
'revision': cred.revision,
Expand Down Expand Up @@ -717,13 +705,7 @@ def update_credential(id):
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)
update['credential_pairs'] = _cred.decrypted_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'])
Expand Down Expand Up @@ -789,6 +771,113 @@ def update_credential(id):
})


@app.route('/v1/credentials/<id>/<to_revision>', methods=['PUT'])
@authnz.require_auth
@authnz.require_csrf_token
@maintenance.check_maintenance_mode
def revert_credential_to_revision(id, to_revision):
try:
current_credential = Credential.get(id)
except DoesNotExist:
return jsonify({'error': 'Credential not found.'}), 404
if current_credential.data_type != 'credential':
msg = 'id provided is not a credential.'
return jsonify({'error': msg}), 400
new_revision = credentialmanager.get_latest_credential_revision(
id,
current_credential.revision
)
try:
revert_credential = Credential.get('{}-{}'.format(id, to_revision))
except DoesNotExist:
logging.warning(
'Item with id {0} does not exist.'.format(id)
)
return jsonify({}), 404
if revert_credential.data_type != 'archive-credential':
msg = 'id provided is not a credential.'
return jsonify({'error': msg}), 400
if revert_credential.equals(current_credential):
ret = {
'error': 'No difference between old and new credential.'
}
return jsonify(ret), 400
services = servicemanager.get_services_for_credential(id)
if revert_credential.credential_pairs:
_credential_pairs = revert_credential.decrypted_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
# Try to save to the archive
try:
Credential(
id='{0}-{1}'.format(id, new_revision),
name=revert_credential.name,
data_type='archive-credential',
credential_pairs=revert_credential.credential_pairs,
metadata=revert_credential.metadata,
enabled=revert_credential.enabled,
revision=new_revision,
data_key=revert_credential.data_key,
cipher_version=revert_credential.cipher_version,
modified_by=authnz.get_logged_in_user(),
documentation=revert_credential.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=revert_credential.name,
data_type='credential',
credential_pairs=revert_credential.credential_pairs,
metadata=revert_credential.metadata,
enabled=revert_credential.enabled,
revision=new_revision,
data_key=revert_credential.data_key,
cipher_version=revert_credential.cipher_version,
modified_by=authnz.get_logged_in_user(),
documentation=revert_credential.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': cred.decrypted_credential_pairs,
'metadata': cred.metadata,
'revision': cred.revision,
'enabled': cred.enabled,
'modified_date': cred.modified_date,
'modified_by': cred.modified_by,
'documentation': cred.documentation
})


@app.route('/v1/blind_credentials', methods=['GET'])
@authnz.require_auth
def get_blind_credential_list():
Expand Down