Lots of smaller fixes:

- Add the rotation_duration to the keys API
- Have the key service UI use the new rotation_duration field
- Fix notification deletion lookup path
- Add proper support for the new notification in the UI
- Only delete expired keys after 7 days (configurable)
- Fix angular digest loop
- Fix unit tests
- Regenerate initdb
This commit is contained in:
Joseph Schorr 2016-04-12 19:17:19 -04:00 committed by Jimmy Zelinskie
parent 2805dad64f
commit 522cf68c5d
12 changed files with 86 additions and 20 deletions

View file

@ -304,3 +304,7 @@ class DefaultConfig(object):
# The timeout for service key approval. # The timeout for service key approval.
UNAPPROVED_SERVICE_KEY_TTL_SEC = 60 * 60 * 24 # One day UNAPPROVED_SERVICE_KEY_TTL_SEC = 60 * 60 * 24 # One day
# How long to wait before GCing an expired service key.
EXPIRED_SERVICE_KEY_TTL_SEC = 60 * 60 * 24 * 7 # One week

View file

@ -79,7 +79,7 @@ def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=F
def delete_all_notifications_by_path_prefix(prefix): def delete_all_notifications_by_path_prefix(prefix):
(Notification (Notification
.delete() .delete()
.where(Notification.lookup_path % prefix + '%') .where(Notification.lookup_path ** (prefix + '%'))
.execute()) .execute())

View file

@ -15,6 +15,11 @@ def _expired_keys_clause(service):
return ((ServiceKey.service == service) & return ((ServiceKey.service == service) &
(ServiceKey.expiration_date <= datetime.utcnow())) (ServiceKey.expiration_date <= datetime.utcnow()))
def _stale_expired_keys_clause(service):
expired_ttl = timedelta(seconds=config.app_config['EXPIRED_SERVICE_KEY_TTL_SEC'])
return ((ServiceKey.service == service) &
(ServiceKey.expiration_date <= (datetime.utcnow() - expired_ttl)))
def _stale_unapproved_keys_clause(service): def _stale_unapproved_keys_clause(service):
unapproved_ttl = timedelta(seconds=config.app_config['UNAPPROVED_SERVICE_KEY_TTL_SEC']) unapproved_ttl = timedelta(seconds=config.app_config['UNAPPROVED_SERVICE_KEY_TTL_SEC'])
@ -24,7 +29,7 @@ def _stale_unapproved_keys_clause(service):
def _gc_expired(service): def _gc_expired(service):
ServiceKey.delete().where(_expired_keys_clause(service) | ServiceKey.delete().where(_stale_expired_keys_clause(service) |
_stale_unapproved_keys_clause(service)).execute() _stale_unapproved_keys_clause(service)).execute()
@ -53,17 +58,18 @@ def create_service_key(name, kid, service, jwk, metadata, expiration_date, rotat
_notify_superusers(key) _notify_superusers(key)
_gc_expired(service) _gc_expired(service)
return key return key
def generate_service_key(service, expiration_date, kid=None, name='', metadata=None): def generate_service_key(service, expiration_date, kid=None, name='', metadata=None,
rotation_duration=None):
private_key = RSA.generate(2048) private_key = RSA.generate(2048)
jwk = RSAKey(key=private_key.publickey()).serialize() jwk = RSAKey(key=private_key.publickey()).serialize()
if kid is None: if kid is None:
kid = canonical_kid(jwk) kid = canonical_kid(jwk)
key = create_service_key(name, kid, service, jwk, metadata or {}, expiration_date) key = create_service_key(name, kid, service, jwk, metadata or {}, expiration_date,
rotation_duration=rotation_duration)
return (private_key, key) return (private_key, key)

View file

@ -484,6 +484,7 @@ def key_view(key):
'metadata': key.metadata, 'metadata': key.metadata,
'created_date': key.created_date, 'created_date': key.created_date,
'expiration_date': key.expiration_date, 'expiration_date': key.expiration_date,
'rotation_duration': key.rotation_duration,
'approval': approval_view(key.approval) if key.approval is not None else None, 'approval': approval_view(key.approval) if key.approval is not None else None,
} }
@ -562,6 +563,9 @@ class SuperUserServiceKeyManagement(ApiResource):
except ValueError: except ValueError:
abort(400) abort(400)
if expiration_date <= datetime.now():
abort(400)
# Create the metadata for the key. # Create the metadata for the key.
user = get_authenticated_user() user = get_authenticated_user()
metadata = body.get('metadata', {}) metadata = body.get('metadata', {})
@ -572,8 +576,8 @@ class SuperUserServiceKeyManagement(ApiResource):
}) })
# Generate a key with a private key that we *never save*. # Generate a key with a private key that we *never save*.
(private_key, key) = model.service_keys.generate_service_key(body['service'], metadata, (private_key, key) = model.service_keys.generate_service_key(body['service'], expiration_date,
expiration_date, metadata=metadata,
name=body.get('name', '')) name=body.get('name', ''))
# Auto-approve the service key. # Auto-approve the service key.
model.service_keys.approve_service_key(key.kid, user, ServiceKeyApprovalType.SUPERUSER, model.service_keys.approve_service_key(key.kid, user, ServiceKeyApprovalType.SUPERUSER,
@ -670,6 +674,9 @@ class SuperUserServiceKey(ApiResource):
except ValueError: except ValueError:
abort(400) abort(400)
if expiration_date <= datetime.now():
abort(400)
key_log_metadata.update({ key_log_metadata.update({
'old_expiration_date': key.expiration_date, 'old_expiration_date': key.expiration_date,
'expiration_date': expiration_date, 'expiration_date': expiration_date,

View file

@ -156,9 +156,10 @@ def __create_subtree(with_storage, repo, structure, creator_username, parent, ta
def __generate_service_key(kid, name, user, timestamp, approval_type, expiration=None, def __generate_service_key(kid, name, user, timestamp, approval_type, expiration=None,
metadata=None, service='sample_service'): metadata=None, service='sample_service', rotation_duration=None):
_, key = model.service_keys.generate_service_key(service, expiration, kid=kid, _, key = model.service_keys.generate_service_key(service, expiration, kid=kid,
name=name, metadata=metadata) name=name, metadata=metadata,
rotation_duration=rotation_duration)
if approval_type is not None: if approval_type is not None:
model.service_keys.approve_service_key(key.kid, user, approval_type, model.service_keys.approve_service_key(key.kid, user, approval_type,
@ -660,7 +661,8 @@ def populate_database(minimal=False, with_storage=False):
__generate_service_key('kid4', 'autorotatingkey', new_user_1, six_ago, __generate_service_key('kid4', 'autorotatingkey', new_user_1, six_ago,
ServiceKeyApprovalType.KEY_ROTATION, today + timedelta(1), ServiceKeyApprovalType.KEY_ROTATION, today + timedelta(1),
dict(rotation_ttl=timedelta(hours=12).total_seconds())) rotation_duration=timedelta(hours=12).total_seconds())
__generate_service_key('kid5', 'key for another service', new_user_1, today, __generate_service_key('kid5', 'key for another service', new_user_1, today,
ServiceKeyApprovalType.SUPERUSER, today + timedelta(14), ServiceKeyApprovalType.SUPERUSER, today + timedelta(14),
service='different_sample_service') service='different_sample_service')

View file

@ -64,11 +64,11 @@
<span am-time-ago="key.created_date"></span> <span am-time-ago="key.created_date"></span>
</td> </td>
<td> <td>
<span class="rotation" bo-if="key.expiration_date && getExpirationInfo(key).rotateDate"> <span class="rotation" bo-if="key.expiration_date && getExpirationInfo(key).willRotate">
<i class="fa" ng-class="getExpirationInfo(key).icon"></i> <i class="fa" ng-class="getExpirationInfo(key).icon"></i>
Automatically rotated <span am-time-ago="getExpirationInfo(key).rotateDate"></span> Automatically rotated <span am-time-ago="getRotationDate(key)"></span>
</span> </span>
<span bo-if="key.expiration_date && !getExpirationInfo(key).rotateDate"> <span bo-if="key.expiration_date && !getExpirationInfo(key).willRotate">
<span ng-class="getExpirationInfo(key).className"> <span ng-class="getExpirationInfo(key).className">
<a ng-click="showChangeExpiration(key)"> <a ng-click="showChangeExpiration(key)">
<i class="fa" ng-class="getExpirationInfo(key).icon"></i> <i class="fa" ng-class="getExpirationInfo(key).icon"></i>

View file

@ -18,7 +18,8 @@ angular.module('quay').directive('datetimePicker', function () {
$element.find('input').datetimepicker({ $element.find('input').datetimepicker({
'format': 'LLL', 'format': 'LLL',
'sideBySide': true, 'sideBySide': true,
'showClear': true 'showClear': true,
'minDate': new Date()
}); });
$element.find('input').on("dp.change", function (e) { $element.find('input').on("dp.change", function (e) {

View file

@ -32,7 +32,9 @@ angular.module('quay').directive('serviceKeysManager', function () {
var keys = $scope.keys.map(function(key) { var keys = $scope.keys.map(function(key) {
var expiration_datetime = -Number.MAX_VALUE; var expiration_datetime = -Number.MAX_VALUE;
if (key.expiration_date) { if (key.rotation_duration) {
expiration_datetime = -(Number.MAX_VALUE/2);
} else if (key.expiration_date) {
expiration_datetime = new Date(key.expiration_date).valueOf() * (-1); expiration_datetime = new Date(key.expiration_date).valueOf() * (-1);
} }
@ -66,15 +68,19 @@ angular.module('quay').directive('serviceKeysManager', function () {
key.expanded = !key.expanded; key.expanded = !key.expanded;
}; };
$scope.getRotationDate = function(key) {
return moment(key.created_date).add(key.rotation_duration, 's').format('LLL');
};
$scope.getExpirationInfo = function(key) { $scope.getExpirationInfo = function(key) {
if (!key.expiration_date) { if (!key.expiration_date) {
return ''; return '';
} }
if (key.metadata.rotation_ttl) { if (key.rotation_duration) {
var rotate_date = moment(key.created_date).add(key.metadata.rotation_ttl, 's') var rotate_date = moment(key.created_date).add(key.rotation_duration, 's')
if (moment().isBefore(rotate_date)) { if (moment().isBefore(rotate_date)) {
return {'className': 'rotation', 'icon': 'fa-refresh', 'rotateDate': rotate_date}; return {'className': 'rotation', 'icon': 'fa-refresh', 'willRotate': true};
} }
} }

View file

@ -131,6 +131,42 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
return '/repository/' + metadata.repository + '?tab=tags'; return '/repository/' + metadata.repository + '?tab=tags';
}, },
'dismissable': true 'dismissable': true
},
'service_key_submitted': {
'level': 'primary',
'message': 'Service key {kid} for service {service} requests approval<br><br>Key was created on {created_date}',
'actions': [
{
'title': 'Approve Key',
'kind': 'primary',
'handler': function(notification) {
var params = {
'kid': notification.metadata.kid
};
ApiService.approveServiceKey({}, params).then(function(resp) {
notificationService.update();
window.location = '/superuser/?tab=servicekeys';
}, ApiService.errorDisplay('Could not approve service key'));
}
},
{
'title': 'Delete Key',
'kind': 'default',
'handler': function(notification) {
var params = {
'kid': notification.metadata.kid
};
ApiService.deleteServiceKey(null, params).then(function(resp) {
notificationService.update();
}, ApiService.errorDisplay('Could not delete service key'));
}
}
],
'page': function(metadata) {
return '/superuser/?tab=servicekeys';
},
} }
}; };

View file

@ -44,6 +44,10 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f
return metadata.kid.substr(0, 12); return metadata.kid.substr(0, 12);
}, },
'created_date': function(value) {
return moment.unix(value).format('LLL');
},
'expiration_date': function(value) { 'expiration_date': function(value) {
return moment.unix(value).format('LLL'); return moment.unix(value).format('LLL');
}, },

Binary file not shown.

View file

@ -3567,7 +3567,7 @@ class TestSuperUserKeyManagement(ApiTestCase):
existing_modify = model.log.LogEntry.select().where(LogEntry.kind == kind).count() existing_modify = model.log.LogEntry.select().where(LogEntry.kind == kind).count()
json = self.getJsonResponse(SuperUserServiceKeyManagement) json = self.getJsonResponse(SuperUserServiceKeyManagement)
self.assertEquals(4, len(json['keys'])) key_count = len(json['keys'])
key = json['keys'][0] key = json['keys'][0]
self.assertTrue('name' in key) self.assertTrue('name' in key)
@ -3622,7 +3622,7 @@ class TestSuperUserKeyManagement(ApiTestCase):
self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404) self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404)
json = self.getJsonResponse(SuperUserServiceKeyManagement) json = self.getJsonResponse(SuperUserServiceKeyManagement)
self.assertEquals(3, len(json['keys'])) self.assertEquals(key_count - 1, len(json['keys']))
# Ensure a log was added for the deletion. # Ensure a log was added for the deletion.
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_delete') kind = LogEntryKind.get(LogEntryKind.name == 'service_key_delete')