Better custom cert handling in the superuser tool

We now only allow certificates ending in .crt to be uploaded and we automatically install the certificate once it has been validated
This commit is contained in:
Joseph Schorr 2017-03-24 17:00:51 -04:00
parent da8032fe61
commit e509eb4cba
6 changed files with 58 additions and 18 deletions

View file

@ -3,6 +3,7 @@
import logging
import os
import string
import subprocess
import pathvalidate
@ -894,9 +895,27 @@ class SuperUserCustomCertificate(ApiResource):
if not uploaded_file:
abort(400)
# Save the certificate.
certpath = pathvalidate.sanitize_filename(certpath)
if not certpath.endswith('.crt'):
abort(400)
cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, certpath)
config_provider.save_volume_file(cert_full_path, uploaded_file)
# Validate the certificate.
try:
with config_provider.get_volume_file(cert_full_path) as f:
load_certificate(f.read())
# Call the update script to install the certificate immediately.
if not app.config['TESTING']:
subprocess.check_call(['/conf/init/certs_install.sh'])
except CertInvalidException:
pass
except IOError:
pass
return '', 204
abort(403)

View file

@ -12,6 +12,7 @@
<p>
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
</p>
<p>Please <strong>make sure</strong> that all custom names used for downstream services (such as Clair) are listed in the certificates below.</p>
</div>
<table class="config-table" style="margin-bottom: 20px;">
@ -19,9 +20,10 @@
<td>Upload certificates:</td>
<td>
<div class="file-upload-box"
select-message="Select custom certificate to add to configuration. Must be in PEM format."
select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'"
files-selected="handleCertsSelected(files, callback)"
reset="resetUpload"></div>
reset="resetUpload"
extensions="['.crt']"></div>
</td>
</tr>
</table>
@ -33,7 +35,7 @@
<td>Names Handled</td>
<td class="options-col"></td>
</thead>
<tr ng-repeat="certificate in certInfo.certs">
<tr ng-repeat="certificate in certInfo.certs" ng-if="!certsUploading">
<td>{{ certificate.path }}</td>
<td class="cert-status">
<div ng-if="certificate.error" class="red">
@ -62,7 +64,11 @@
</td>
</tr>
</table>
<div class="empty" ng-if="!certInfo.certs.length" style="margin-top: 20px;">
<div ng-if="certsUploading" style="margin-top: 20px; text-align: center;">
<div class="cor-loader-inline"></div>
Uploading, validating and updating certificate(s)
</div>
<div class="empty" ng-if="!certInfo.certs.length && !certsUploading" style="margin-top: 20px;">
<div class="empty-primary-msg">No custom certificates found.</div>
</div>
</div>

View file

@ -2,7 +2,9 @@
<div class="file-input-container">
<div ng-show="state != 'uploading'">
<form id="file-drop-form-{{ boxId }}">
<input id="file-drop-{{ boxId }}" name="file-drop-{{ boxId }}" class="file-drop" type="file" files-changed="handleFilesChanged(files)">
<input id="file-drop-{{ boxId }}" name="file-drop-{{ boxId }}" class="file-drop"
type="file" files-changed="handleFilesChanged(files)"
accept="{{ getAccepts(extensions) }}">
<label for="file-drop-{{ boxId }}" ng-class="state">
<span class="chosen-file">
<span ng-if="selectedFiles.length">

View file

@ -1322,10 +1322,12 @@ angular.module("core-config-setup", ['angularFileUpload'])
},
controller: function($scope, $element, $upload, ApiService, UserService) {
$scope.resetUpload = 0;
$scope.certsUploading = false;
var loadCertificates = function() {
$scope.certificatesResource = ApiService.getCustomCertificatesAsResource().get(function(resp) {
$scope.certInfo = resp;
$scope.certsUploading = false;
});
};
@ -1336,6 +1338,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
});
$scope.handleCertsSelected = function(files, callback) {
$scope.certsUploading = true;
$upload.upload({
url: '/api/v1/superuser/customcerts/' + files[0].name,
method: 'POST',

View file

@ -15,6 +15,8 @@ angular.module('quay').directive('fileUploadBox', function () {
'filesCleared': '&filesCleared',
'filesValidated': '&filesValidated',
'extensions': '<extensions',
'reset': '=?reset'
},
controller: function($rootScope, $scope, $element, ApiService) {
@ -150,6 +152,14 @@ angular.module('quay').directive('fileUploadBox', function () {
}
};
$scope.getAccepts = function(extensions) {
if (!extensions || !extensions.length) {
return '*';
}
return extensions.join(',');
};
$scope.$watch('reset', function(reset) {
if (reset) {
$scope.state = 'clear';

View file

@ -4457,21 +4457,21 @@ class TestSuperUserCustomCertificates(ApiTestCase):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', san_list=['DNS:bar', 'DNS:baz'])
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'),
file=(StringIO(cert_contents), 'testcert'), expected_code=204)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert', cert_info['path'])
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost', 'bar', 'baz']), set(cert_info['names']))
self.assertFalse(cert_info['expired'])
# Remove the certificate.
self.deleteResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'))
self.deleteResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'))
# Make sure it is gone.
json = self.getJsonResponse(SuperUserCustomCertificates)
@ -4482,15 +4482,15 @@ class TestSuperUserCustomCertificates(ApiTestCase):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'),
file=(StringIO(cert_contents), 'testcert'), expected_code=204)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO(cert_contents), 'testcert.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert', cert_info['path'])
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals(set(['somecoolhost']), set(cert_info['names']))
self.assertTrue(cert_info['expired'])
@ -4499,15 +4499,15 @@ class TestSuperUserCustomCertificates(ApiTestCase):
self.login(ADMIN_ACCESS_USER)
# Upload an invalid certificate.
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'),
file=(StringIO('some contents'), 'testcert'), expected_code=204)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert.crt'),
file=(StringIO('some contents'), 'testcert.crt'), expected_code=204)
# Make sure it is present but invalid.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('testcert', cert_info['path'])
self.assertEquals('testcert.crt', cert_info['path'])
self.assertEquals('no start line', cert_info['error'])
def test_path_sanitization(self):
@ -4515,15 +4515,15 @@ class TestSuperUserCustomCertificates(ApiTestCase):
# Upload a certificate.
cert_contents, _ = generate_test_cert(hostname='somecoolhost', expires=-10)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert/../foobar'),
file=(StringIO(cert_contents), 'testcert/../foobar'), expected_code=204)
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert/../foobar.crt'),
file=(StringIO(cert_contents), 'testcert/../foobar.crt'), expected_code=204)
# Make sure it is present.
json = self.getJsonResponse(SuperUserCustomCertificates)
self.assertEquals(1, len(json['certs']))
cert_info = json['certs'][0]
self.assertEquals('foobar', cert_info['path'])
self.assertEquals('foobar.crt', cert_info['path'])
class TestSuperUserTakeOwnership(ApiTestCase):