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:
parent
da8032fe61
commit
e509eb4cba
6 changed files with 58 additions and 18 deletions
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Reference in a new issue