refactor approval service key to not need approver

This commit is contained in:
Sam Chow 2018-08-13 11:40:50 -04:00
parent 7edf679670
commit cc9bedbeb9
11 changed files with 331 additions and 13 deletions

View file

@ -155,7 +155,7 @@ def test_mixing_keys_e2e(initialized_db):
# Approve the key and try again. # Approve the key and try again.
admin_user = model.user.get_user('devtable') admin_user = model.user.get_user('devtable')
model.service_keys.approve_service_key(key.kid, admin_user, ServiceKeyApprovalType.SUPERUSER) model.service_keys.approve_service_key(key.kid, ServiceKeyApprovalType.SUPERUSER, approver=admin_user)
valid_token = _token(token_data, key_id='newkey', private_key=private_key) valid_token = _token(token_data, key_id='newkey', private_key=private_key)

View file

@ -3,15 +3,17 @@ import pathvalidate
import os import os
import subprocess import subprocess
from flask import request, jsonify from flask import request, jsonify, make_response
from data.database import ServiceKeyApprovalType
from data.model import ServiceKeyDoesNotExist
from util.config.validator import EXTRA_CA_DIRECTORY from util.config.validator import EXTRA_CA_DIRECTORY
from config_app.config_endpoints.exception import InvalidRequest from config_app.config_endpoints.exception import InvalidRequest
from config_app.config_endpoints.api import resource, ApiResource, nickname from config_app.config_endpoints.api import resource, ApiResource, nickname
from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model from config_app.config_endpoints.api.superuser_models_pre_oci import pre_oci_model
from config_app.config_util.ssl import load_certificate, CertInvalidException from config_app.config_util.ssl import load_certificate, CertInvalidException
from config_app.c_app import config_provider, INIT_SCRIPTS_LOCATION from config_app.c_app import app, config_provider, INIT_SCRIPTS_LOCATION
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -146,4 +148,44 @@ class SuperUserServiceKeyManagement(ApiResource):
'keys': [key.to_dict() for key in keys], 'keys': [key.to_dict() for key in keys],
}) })
@resource('/v1/superuser/approvedkeys/<kid>')
class SuperUserServiceKeyApproval(ApiResource):
""" Resource for approving service keys. """
schemas = {
'ApproveServiceKey': {
'id': 'ApproveServiceKey',
'type': 'object',
'description': 'Information for approving service keys',
'properties': {
'notes': {
'type': 'string',
'description': 'Optional approval notes',
},
},
},
}
@nickname('approveServiceKey')
@validate_json_request('ApproveServiceKey')
def post(self, kid):
notes = request.get_json().get('notes', '')
approver = app.config.get('SUPER_USERS', [])[0] # get the first superuser created in the config tool
try:
key = pre_oci_model.approve_service_key(kid, ServiceKeyApprovalType.SUPERUSER, notes=notes)
# Log the approval of the service key.
key_log_metadata = {
'kid': kid,
'service': key.service,
'name': key.name,
'expiration_date': key.expiration_date,
}
log_action('service_key_approve', None, key_log_metadata)
except ServiceKeyDoesNotExist:
raise NotFound()
except ServiceKeyAlreadyApproved:
pass
return make_response('', 201)

View file

@ -17,6 +17,13 @@ def _create_key(key):
return ServiceKey(key.name, key.kid, key.service, key.jwk, key.metadata, key.created_date, key.expiration_date, return ServiceKey(key.name, key.kid, key.service, key.jwk, key.metadata, key.created_date, key.expiration_date,
key.rotation_duration, approval) key.rotation_duration, approval)
class ServiceKeyDoesNotExist(Exception):
pass
class ServiceKeyAlreadyApproved(Exception):
pass
class PreOCIModel(SuperuserDataInterface): class PreOCIModel(SuperuserDataInterface):
""" """
@ -27,5 +34,14 @@ class PreOCIModel(SuperuserDataInterface):
keys = model.service_keys.list_all_keys() keys = model.service_keys.list_all_keys()
return [_create_key(key) for key in keys] return [_create_key(key) for key in keys]
def approve_service_key(self, kid, approval_type, notes=''):
try:
key = model.service_keys.approve_service_key(kid, approval_type, notes=notes)
return _create_key(key)
except model.ServiceKeyDoesNotExist:
raise ServiceKeyDoesNotExist
except model.ServiceKeyAlreadyApproved:
raise ServiceKeyAlreadyApproved
pre_oci_model = PreOCIModel() pre_oci_model = PreOCIModel()

View file

@ -46,6 +46,7 @@ export class KubeDeployModalComponent {
this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`; this.errorMessage = `Could cycle the deployments with the new configuration. Error: ${err.toString()}`;
}) })
}).catch(err => { }).catch(err => {
console.log(err)
this.state = 'error'; this.state = 'error';
this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`; this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`;
}) })

View file

@ -0,0 +1,137 @@
<div class="request-service-key-dialog-element">
<!-- Modal message dialog -->
<div class="co-dialog modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!working">&times;</button>
<h4 class="modal-title">Create key for service {{ requestKeyInfo.service }}</h4>
</div>
<div class="modal-body" ng-show="working">
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!working">
<!-- Step 0 -->
<div ng-show="step == 0">
<table class="co-option-table">
<tr>
<td><input type="radio" id="automaticKey" ng-model="requestKind" value="automatic"></td>
<td>
<label for="automaticKey">Have the service provide a key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the single instance is setup now.</div>
</td>
</tr>
<tr>
<td><input type="radio" id="presharedKey" ng-model="requestKind" value="preshared"></td>
<td>
<label for="presharedKey">Generate shared key</label>
<div class="help-text">Recommended for <code>{{ requestKeyInfo.service }}</code> installations where the instances are dynamically started.</div>
</td>
</tr>
</table>
</div>
<!-- Step 1 (automatic) -->
<div ng-show="step == 1 && requestKind == 'automatic'" style="text-align: center">
<div style="margin-top: 20px;">
Please start the <code>{{ requestKeyInfo.service }}</code> service now, configured for <a href="https://github.com/coreos/jwtproxy#autogenerated-private-key" ng-safenewtab>autogenerated private key</a>. The key approval process will continue automatically once the service connects to Quay.
</div>
<div style="margin-top: 20px;">
Waiting for service to connect
</div>
<div style="margin-top: 10px; margin-bottom: 20px;">
<div class="cor-loader-inline"></div>
</div>
</div>
<!-- Step 2 (automatic) -->
<div ng-show="step == 2 && requestKind == 'automatic'" style="text-align: center">
A key for service <code>{{ requestKeyInfo.service }}</code> has been automatically generated, approved and saved in the service's keystore.
</div>
<!-- Step 1 (generate) -->
<div ng-show="step == 1 && requestKind == 'preshared'">
<form name="createForm" ng-submit="createPresharedKey()">
<table class="co-form-table">
<tr>
<td><label for="create-key-name">Key Name:</label></td>
<td>
<input class="form-control" name="create-key-name" type="text" ng-model="preshared.name" placeholder="Friendly Key Name">
<span class="co-help-text">
A friendly name for the key for later reference.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-expiration">Expiration date (optional):</label></td>
<td>
<span class="datetime-picker" datetime="preshared.expiration"></span>
<span class="co-help-text">
The date and time that the key expires. If left blank, the key will never expire.
</span>
</td>
</tr>
<tr>
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
<td>
<markdown-input content="preshared.notes"
can-write="true"
(content-changed)="updateNotes($event.content)"
field-title="notes"></markdown-input>
<span class="co-help-text">
Optional notes for additional human-readable information about why the key was created.
</span>
</td>
</tr>
</table>
</form>
</div>
<!-- Step 2 (generate) -->
<div ng-show="step == 2 && requestKind == 'preshared'">
<div class="co-alert co-alert-warning">
The following key has been generated for service <code>{{ requestKeyInfo.service }}</code>.
<br><br>
Please copy the key's ID and copy/download the key's private contents and place it in the directory with the service's configuration.
<br><br>
<strong>Once this dialog is closed this private key will not be accessible anywhere else!</strong>
</div>
<label>Key ID:</label>
<div class="copy-box" value="createdKey.kid"></div>
<label>Private Key (PEM):</label>
<textarea class="key-display form-control" onclick="this.focus();this.select()" readonly>{{ createdKey.private_key }}</textarea>
</div>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-show="step == 1 && requestKind == 'preshared'"
ng-disabled="createForm.$invalid"
ng-click="createPresharedKey()">
Generate Key
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'preshared'"
ng-click="showGenerate()">
Continue
</button>
<button type="button" class="btn btn-primary" ng-show="step == 0 && requestKind == 'automatic'"
ng-click="startApproval()">
Start Approval
</button>
<button type="button" class="btn btn-primary" ng-click="downloadPrivateKey(createdKey)" ng-if="createdKey && isDownloadSupported()">
<i class="fa fa-download"></i> Download Private Key
</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step == 2">Close</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step != 2">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
</div>

View file

@ -0,0 +1,124 @@
/**
* An element which displays a dialog for requesting or creating a service key.
*/
angular.module('quay').directive('requestServiceKeyDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/request-service-key-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'requestKeyInfo': '=requestKeyInfo',
'keyCreated': '&keyCreated'
},
controller: function($scope, $element, $timeout, ApiService) {
var handleNewKey = function(key) {
var data = {
'notes': 'Approved during setup of service ' + key.service
};
var params = {
'kid': key.kid
};
ApiService.approveServiceKey(data, params).then(function(resp) {
$scope.keyCreated({'key': key});
$scope.step = 2;
}, ApiService.errorDisplay('Could not approve service key'));
};
var checkKeys = function() {
var isShown = ($element.find('.modal').data('bs.modal') || {}).isShown;
if (!isShown) {
return;
}
// TODO: filter by service.
ApiService.listServiceKeys().then(function(resp) {
var keys = resp['keys'];
for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
if (key.service == $scope.requestKeyInfo.service && !key.approval && key.rotation_duration) {
handleNewKey(key);
return;
}
}
$timeout(checkKeys, 1000);
}, ApiService.errorDisplay('Could not list service keys'));
};
$scope.show = function() {
$scope.working = false;
$scope.step = 0;
$scope.requestKind = null;
$scope.preshared = {
'name': $scope.requestKeyInfo.service + ' Service Key',
'notes': 'Created during setup for service `' + $scope.requestKeyInfo.service + '`'
};
$element.find('.modal').modal({});
};
$scope.hide = function() {
$scope.loading = false;
$element.find('.modal').modal('hide');
};
$scope.showGenerate = function() {
$scope.step = 1;
};
$scope.startApproval = function() {
$scope.step = 1;
checkKeys();
};
$scope.isDownloadSupported = function() {
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Doesn't work properly in Safari, sadly.
return false;
}
try { return !!new Blob(); } catch(e) {}
return false;
};
$scope.downloadPrivateKey = function(key) {
var blob = new Blob([key.private_key]);
FileSaver.saveAs(blob, key.service + '.pem');
};
$scope.createPresharedKey = function() {
$scope.working = true;
var data = {
'name': $scope.preshared.name,
'service': $scope.requestKeyInfo.service,
'expiration': $scope.preshared.expiration || null,
'notes': $scope.preshared.notes
};
ApiService.createServiceKey(data).then(function(resp) {
$scope.working = false;
$scope.step = 2;
$scope.createdKey = resp;
$scope.keyCreated({'key': resp});
}, ApiService.errorDisplay('Could not create service key'));
};
$scope.updateNotes = function(content) {
$scope.preshared.notes = content;
};
$scope.$watch('requestKeyInfo', function(info) {
if (info && info.service) {
$scope.show();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -145,7 +145,7 @@ def set_key_expiration(kid, expiration_date):
service_key.save() service_key.save()
def approve_service_key(kid, approver, approval_type, notes=''): def approve_service_key(kid, approval_type, approver=None, notes=''):
try: try:
with db_transaction(): with db_transaction():
key = db_for_update(ServiceKey.select().where(ServiceKey.kid == kid)).get() key = db_for_update(ServiceKey.select().where(ServiceKey.kid == kid)).get()

View file

@ -118,7 +118,7 @@ class PreOCIModel(SuperuserDataInterface):
def approve_service_key(self, kid, approver, approval_type, notes=''): def approve_service_key(self, kid, approver, approval_type, notes=''):
try: try:
key = model.service_keys.approve_service_key(kid, approver, approval_type, notes=notes) key = model.service_keys.approve_service_key(kid, approval_type, approver=approver, notes=notes)
return _create_key(key) return _create_key(key)
except model.ServiceKeyDoesNotExist: except model.ServiceKeyDoesNotExist:
raise ServiceKeyDoesNotExist raise ServiceKeyDoesNotExist

View file

@ -161,8 +161,7 @@ def __generate_service_key(kid, name, user, timestamp, approval_type, expiration
rotation_duration=rotation_duration) 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, approval_type, notes='The **test** approval')
notes='The **test** approval')
key_metadata = { key_metadata = {
'kid': kid, 'kid': kid,
@ -820,7 +819,7 @@ def populate_database(minimal=False, with_storage=False):
key = model.service_keys.create_service_key('test_service_key', 'test_service_key', 'quay', key = model.service_keys.create_service_key('test_service_key', 'test_service_key', 'quay',
_TEST_JWK, {}, None) _TEST_JWK, {}, None)
model.service_keys.approve_service_key(key.kid, new_user_1, ServiceKeyApprovalType.SUPERUSER, model.service_keys.approve_service_key(key.kid, ServiceKeyApprovalType.SUPERUSER,
notes='Test service key for local/test registry testing') notes='Test service key for local/test registry testing')
# Add an app specific token. # Add an app specific token.

View file

@ -559,7 +559,7 @@ class KeyServerTestCase(EndpointTestCase):
}, data=jwk, expected_code=403) }, data=jwk, expected_code=403)
# Approve the key. # Approve the key.
model.service_keys.approve_service_key('kid420', 1, ServiceKeyApprovalType.SUPERUSER) model.service_keys.approve_service_key('kid420', ServiceKeyApprovalType.SUPERUSER, approver=1)
# Rotate that new key # Rotate that new key
with assert_action_logged('service_key_rotate'): with assert_action_logged('service_key_rotate'):
@ -598,7 +598,7 @@ class KeyServerTestCase(EndpointTestCase):
def test_attempt_delete_service_key_with_expired_key(self): def test_attempt_delete_service_key_with_expired_key(self):
# Generate two keys, approving the first. # Generate two keys, approving the first.
private_key, _ = model.service_keys.generate_service_key('sample_service', None, kid='first') private_key, _ = model.service_keys.generate_service_key('sample_service', None, kid='first')
model.service_keys.approve_service_key('first', 1, ServiceKeyApprovalType.SUPERUSER) model.service_keys.approve_service_key('first', ServiceKeyApprovalType.SUPERUSER, approver=1)
model.service_keys.generate_service_key('sample_service', None, kid='second') model.service_keys.generate_service_key('sample_service', None, kid='second')
# Mint a JWT with our test payload # Mint a JWT with our test payload
@ -661,7 +661,7 @@ class KeyServerTestCase(EndpointTestCase):
expected_code=403, service='sample_service', kid='kid321') expected_code=403, service='sample_service', kid='kid321')
# Approve the second key. # Approve the second key.
model.service_keys.approve_service_key('kid123', 1, ServiceKeyApprovalType.SUPERUSER) model.service_keys.approve_service_key('kid123', ServiceKeyApprovalType.SUPERUSER, approver=1)
# Using the credentials of our approved key, delete our unapproved key # Using the credentials of our approved key, delete our unapproved key
with assert_action_logged('service_key_delete'): with assert_action_logged('service_key_delete'):

View file

@ -18,8 +18,7 @@ def generate_key(service, name, expiration_date=None, notes=None):
metadata=metadata, metadata=metadata,
name=name) name=name)
# Auto-approve the service key. # Auto-approve the service key.
model.service_keys.approve_service_key(key.kid, None, ServiceKeyApprovalType.AUTOMATIC, model.service_keys.approve_service_key(key.kid, ServiceKeyApprovalType.AUTOMATIC, notes=notes or '')
notes=notes or '')
# Log the creation and auto-approval of the service key. # Log the creation and auto-approval of the service key.
key_log_metadata = { key_log_metadata = {