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:
parent
2805dad64f
commit
522cf68c5d
12 changed files with 86 additions and 20 deletions
|
@ -304,3 +304,7 @@ class DefaultConfig(object):
|
|||
|
||||
# The timeout for service key approval.
|
||||
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
(Notification
|
||||
.delete()
|
||||
.where(Notification.lookup_path % prefix + '%')
|
||||
.where(Notification.lookup_path ** (prefix + '%'))
|
||||
.execute())
|
||||
|
||||
|
||||
|
|
|
@ -15,6 +15,11 @@ def _expired_keys_clause(service):
|
|||
return ((ServiceKey.service == service) &
|
||||
(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):
|
||||
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):
|
||||
ServiceKey.delete().where(_expired_keys_clause(service) |
|
||||
ServiceKey.delete().where(_stale_expired_keys_clause(service) |
|
||||
_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)
|
||||
_gc_expired(service)
|
||||
|
||||
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)
|
||||
jwk = RSAKey(key=private_key.publickey()).serialize()
|
||||
if kid is None:
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -484,6 +484,7 @@ def key_view(key):
|
|||
'metadata': key.metadata,
|
||||
'created_date': key.created_date,
|
||||
'expiration_date': key.expiration_date,
|
||||
'rotation_duration': key.rotation_duration,
|
||||
'approval': approval_view(key.approval) if key.approval is not None else None,
|
||||
}
|
||||
|
||||
|
@ -562,6 +563,9 @@ class SuperUserServiceKeyManagement(ApiResource):
|
|||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
if expiration_date <= datetime.now():
|
||||
abort(400)
|
||||
|
||||
# Create the metadata for the key.
|
||||
user = get_authenticated_user()
|
||||
metadata = body.get('metadata', {})
|
||||
|
@ -572,8 +576,8 @@ class SuperUserServiceKeyManagement(ApiResource):
|
|||
})
|
||||
|
||||
# Generate a key with a private key that we *never save*.
|
||||
(private_key, key) = model.service_keys.generate_service_key(body['service'], metadata,
|
||||
expiration_date,
|
||||
(private_key, key) = model.service_keys.generate_service_key(body['service'], expiration_date,
|
||||
metadata=metadata,
|
||||
name=body.get('name', ''))
|
||||
# Auto-approve the service key.
|
||||
model.service_keys.approve_service_key(key.kid, user, ServiceKeyApprovalType.SUPERUSER,
|
||||
|
@ -670,6 +674,9 @@ class SuperUserServiceKey(ApiResource):
|
|||
except ValueError:
|
||||
abort(400)
|
||||
|
||||
if expiration_date <= datetime.now():
|
||||
abort(400)
|
||||
|
||||
key_log_metadata.update({
|
||||
'old_expiration_date': key.expiration_date,
|
||||
'expiration_date': expiration_date,
|
||||
|
|
|
@ -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,
|
||||
metadata=None, service='sample_service'):
|
||||
metadata=None, service='sample_service', rotation_duration=None):
|
||||
_, 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:
|
||||
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,
|
||||
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,
|
||||
ServiceKeyApprovalType.SUPERUSER, today + timedelta(14),
|
||||
service='different_sample_service')
|
||||
|
|
|
@ -64,11 +64,11 @@
|
|||
<span am-time-ago="key.created_date"></span>
|
||||
</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>
|
||||
Automatically rotated <span am-time-ago="getExpirationInfo(key).rotateDate"></span>
|
||||
Automatically rotated <span am-time-ago="getRotationDate(key)"></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">
|
||||
<a ng-click="showChangeExpiration(key)">
|
||||
<i class="fa" ng-class="getExpirationInfo(key).icon"></i>
|
||||
|
|
|
@ -18,7 +18,8 @@ angular.module('quay').directive('datetimePicker', function () {
|
|||
$element.find('input').datetimepicker({
|
||||
'format': 'LLL',
|
||||
'sideBySide': true,
|
||||
'showClear': true
|
||||
'showClear': true,
|
||||
'minDate': new Date()
|
||||
});
|
||||
|
||||
$element.find('input').on("dp.change", function (e) {
|
||||
|
|
|
@ -32,7 +32,9 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
|||
|
||||
var keys = $scope.keys.map(function(key) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -66,15 +68,19 @@ angular.module('quay').directive('serviceKeysManager', function () {
|
|||
key.expanded = !key.expanded;
|
||||
};
|
||||
|
||||
$scope.getRotationDate = function(key) {
|
||||
return moment(key.created_date).add(key.rotation_duration, 's').format('LLL');
|
||||
};
|
||||
|
||||
$scope.getExpirationInfo = function(key) {
|
||||
if (!key.expiration_date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (key.metadata.rotation_ttl) {
|
||||
var rotate_date = moment(key.created_date).add(key.metadata.rotation_ttl, 's')
|
||||
if (key.rotation_duration) {
|
||||
var rotate_date = moment(key.created_date).add(key.rotation_duration, 's')
|
||||
if (moment().isBefore(rotate_date)) {
|
||||
return {'className': 'rotation', 'icon': 'fa-refresh', 'rotateDate': rotate_date};
|
||||
return {'className': 'rotation', 'icon': 'fa-refresh', 'willRotate': true};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -131,6 +131,42 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
|||
return '/repository/' + metadata.repository + '?tab=tags';
|
||||
},
|
||||
'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';
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -44,6 +44,10 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f
|
|||
return metadata.kid.substr(0, 12);
|
||||
},
|
||||
|
||||
'created_date': function(value) {
|
||||
return moment.unix(value).format('LLL');
|
||||
},
|
||||
|
||||
'expiration_date': function(value) {
|
||||
return moment.unix(value).format('LLL');
|
||||
},
|
||||
|
|
Binary file not shown.
|
@ -3567,7 +3567,7 @@ class TestSuperUserKeyManagement(ApiTestCase):
|
|||
existing_modify = model.log.LogEntry.select().where(LogEntry.kind == kind).count()
|
||||
|
||||
json = self.getJsonResponse(SuperUserServiceKeyManagement)
|
||||
self.assertEquals(4, len(json['keys']))
|
||||
key_count = len(json['keys'])
|
||||
|
||||
key = json['keys'][0]
|
||||
self.assertTrue('name' in key)
|
||||
|
@ -3622,7 +3622,7 @@ class TestSuperUserKeyManagement(ApiTestCase):
|
|||
self.getResponse(SuperUserServiceKey, params=dict(kid=key['kid']), expected_code=404)
|
||||
|
||||
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.
|
||||
kind = LogEntryKind.get(LogEntryKind.name == 'service_key_delete')
|
||||
|
|
Reference in a new issue