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 logging
import os import os
import string import string
import subprocess
import pathvalidate import pathvalidate
@ -894,9 +895,27 @@ class SuperUserCustomCertificate(ApiResource):
if not uploaded_file: if not uploaded_file:
abort(400) abort(400)
# Save the certificate.
certpath = pathvalidate.sanitize_filename(certpath) certpath = pathvalidate.sanitize_filename(certpath)
if not certpath.endswith('.crt'):
abort(400)
cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, certpath) cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, certpath)
config_provider.save_volume_file(cert_full_path, uploaded_file) 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 return '', 204
abort(403) abort(403)

View file

@ -12,6 +12,7 @@
<p> <p>
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services. Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
</p> </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> </div>
<table class="config-table" style="margin-bottom: 20px;"> <table class="config-table" style="margin-bottom: 20px;">
@ -19,9 +20,10 @@
<td>Upload certificates:</td> <td>Upload certificates:</td>
<td> <td>
<div class="file-upload-box" <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)" files-selected="handleCertsSelected(files, callback)"
reset="resetUpload"></div> reset="resetUpload"
extensions="['.crt']"></div>
</td> </td>
</tr> </tr>
</table> </table>
@ -33,7 +35,7 @@
<td>Names Handled</td> <td>Names Handled</td>
<td class="options-col"></td> <td class="options-col"></td>
</thead> </thead>
<tr ng-repeat="certificate in certInfo.certs"> <tr ng-repeat="certificate in certInfo.certs" ng-if="!certsUploading">
<td>{{ certificate.path }}</td> <td>{{ certificate.path }}</td>
<td class="cert-status"> <td class="cert-status">
<div ng-if="certificate.error" class="red"> <div ng-if="certificate.error" class="red">
@ -62,7 +64,11 @@
</td> </td>
</tr> </tr>
</table> </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 class="empty-primary-msg">No custom certificates found.</div>
</div> </div>
</div> </div>

View file

@ -2,7 +2,9 @@
<div class="file-input-container"> <div class="file-input-container">
<div ng-show="state != 'uploading'"> <div ng-show="state != 'uploading'">
<form id="file-drop-form-{{ boxId }}"> <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"> <label for="file-drop-{{ boxId }}" ng-class="state">
<span class="chosen-file"> <span class="chosen-file">
<span ng-if="selectedFiles.length"> <span ng-if="selectedFiles.length">

View file

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

View file

@ -15,6 +15,8 @@ angular.module('quay').directive('fileUploadBox', function () {
'filesCleared': '&filesCleared', 'filesCleared': '&filesCleared',
'filesValidated': '&filesValidated', 'filesValidated': '&filesValidated',
'extensions': '<extensions',
'reset': '=?reset' 'reset': '=?reset'
}, },
controller: function($rootScope, $scope, $element, ApiService) { 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) { $scope.$watch('reset', function(reset) {
if (reset) { if (reset) {
$scope.state = 'clear'; $scope.state = 'clear';

View file

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