Custom SSL certificates config panel
Adds a new panel to the superuser config tool, for managing custom SSL certificates in the config bundle [Delivers #135586525]
This commit is contained in:
parent
773f271daa
commit
7e0fbeb625
14 changed files with 434 additions and 41 deletions
|
@ -4,6 +4,8 @@ import logging
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
|
|
||||||
|
import pathvalidate
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
|
|
||||||
|
@ -25,6 +27,8 @@ from data import model
|
||||||
from data.database import ServiceKeyApprovalType
|
from data.database import ServiceKeyApprovalType
|
||||||
from util.useremails import send_confirmation_email, send_recovery_email
|
from util.useremails import send_confirmation_email, send_recovery_email
|
||||||
from util.license import decode_license, LicenseDecodeError
|
from util.license import decode_license, LicenseDecodeError
|
||||||
|
from util.security.ssl import load_certificate, CertInvalidException
|
||||||
|
from util.config.validator import EXTRA_CA_DIRECTORY
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -824,6 +828,89 @@ class SuperUserServiceKeyApproval(ApiResource):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/superuser/customcerts')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
class SuperUserCustomCertificates(ApiResource):
|
||||||
|
""" Resource for managing custom certificates. """
|
||||||
|
@nickname('getCustomCertificates')
|
||||||
|
@require_fresh_login
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@verify_not_prod
|
||||||
|
def get(self):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
has_extra_certs_path = config_provider.volume_file_exists(EXTRA_CA_DIRECTORY)
|
||||||
|
extra_certs_found = config_provider.list_volume_directory(EXTRA_CA_DIRECTORY)
|
||||||
|
if extra_certs_found is None:
|
||||||
|
return {
|
||||||
|
'status': 'file' if has_extra_certs_path else 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_views = []
|
||||||
|
for extra_cert_path in extra_certs_found:
|
||||||
|
try:
|
||||||
|
cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, extra_cert_path)
|
||||||
|
with config_provider.get_volume_file(cert_full_path) as f:
|
||||||
|
certificate = load_certificate(f.read())
|
||||||
|
cert_views.append({
|
||||||
|
'path': extra_cert_path,
|
||||||
|
'names': list(certificate.names),
|
||||||
|
'expired': certificate.expired,
|
||||||
|
})
|
||||||
|
except CertInvalidException as cie:
|
||||||
|
cert_views.append({
|
||||||
|
'path': extra_cert_path,
|
||||||
|
'error': cie.message,
|
||||||
|
})
|
||||||
|
except IOError as ioe:
|
||||||
|
cert_views.append({
|
||||||
|
'path': extra_cert_path,
|
||||||
|
'error': ioe.message,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'directory',
|
||||||
|
'certs': cert_views,
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/superuser/customcerts/<certpath>')
|
||||||
|
@internal_only
|
||||||
|
@show_if(features.SUPER_USERS)
|
||||||
|
class SuperUserCustomCertificate(ApiResource):
|
||||||
|
""" Resource for managing a custom certificate. """
|
||||||
|
@nickname('uploadCustomCertificate')
|
||||||
|
@require_fresh_login
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@verify_not_prod
|
||||||
|
def post(self, certpath):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
uploaded_file = request.files['file']
|
||||||
|
if not uploaded_file:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
certpath = pathvalidate.sanitize_filename(certpath)
|
||||||
|
cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, certpath)
|
||||||
|
config_provider.save_volume_file(cert_full_path, uploaded_file)
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@nickname('deleteCustomCertificate')
|
||||||
|
@require_fresh_login
|
||||||
|
@require_scope(scopes.SUPERUSER)
|
||||||
|
@verify_not_prod
|
||||||
|
def delete(self, certpath):
|
||||||
|
if SuperUserPermission().can():
|
||||||
|
cert_full_path = os.path.join(EXTRA_CA_DIRECTORY, certpath)
|
||||||
|
config_provider.remove_volume_file(cert_full_path)
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@resource('/v1/superuser/license')
|
@resource('/v1/superuser/license')
|
||||||
@internal_only
|
@internal_only
|
||||||
@show_if(features.SUPER_USERS)
|
@show_if(features.SUPER_USERS)
|
||||||
|
|
|
@ -38,6 +38,7 @@ mixpanel
|
||||||
mock
|
mock
|
||||||
moto==0.4.25 # remove when 0.4.28+ is out
|
moto==0.4.25 # remove when 0.4.28+ is out
|
||||||
namedlist
|
namedlist
|
||||||
|
pathvalidate
|
||||||
peewee==2.8.1
|
peewee==2.8.1
|
||||||
psutil
|
psutil
|
||||||
psycopg2
|
psycopg2
|
||||||
|
|
|
@ -66,6 +66,7 @@ oslo.config==3.17.0
|
||||||
oslo.i18n==3.9.0
|
oslo.i18n==3.9.0
|
||||||
oslo.serialization==2.13.0
|
oslo.serialization==2.13.0
|
||||||
oslo.utils==3.16.0
|
oslo.utils==3.16.0
|
||||||
|
pathvalidate==0.13.0
|
||||||
pbr==1.10.0
|
pbr==1.10.0
|
||||||
peewee==2.8.1
|
peewee==2.8.1
|
||||||
Pillow==3.4.2
|
Pillow==3.4.2
|
||||||
|
|
|
@ -519,6 +519,37 @@ a:focus {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .dns-name {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .cert-status .fa {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .cert-status .green {
|
||||||
|
color: #2FC98E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .cert-status .orange {
|
||||||
|
color: #FCA657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .cert-status .red {
|
||||||
|
color: #D64456;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .file-upload-box-element .file-input-container {
|
||||||
|
padding: 0px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-certificates-field-element .file-upload-box-element .file-drop + label {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.config-list-field-element .empty {
|
.config-list-field-element .empty {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
70
static/directives/config/config-certificates-field.html
Normal file
70
static/directives/config/config-certificates-field.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<div class="config-certificates-field-element">
|
||||||
|
<div class="resource-view" resource="certificatesResource" error-message="'Could not load certificates list'">
|
||||||
|
<!-- File -->
|
||||||
|
<div class="co-alert co-alert-warning" ng-if="certInfo.status == 'file'">
|
||||||
|
<code>extra_ca_certs</code> is a single file and cannot be processed by this tool. If a valid and appended list of certificates, they will be installed on container startup.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="certInfo.status != 'file'">
|
||||||
|
<div class="description">
|
||||||
|
<p>This section lists any custom or self-signed SSL certificates that are installed in the <span class="registry-name"></span> container on startup after being read from the <code>extra_ca_certs</code> directory in the configuration volume.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="config-table" style="margin-bottom: 20px;">
|
||||||
|
<tr>
|
||||||
|
<td>Upload certificates:</td>
|
||||||
|
<td>
|
||||||
|
<div class="file-upload-box"
|
||||||
|
select-message="Select custom certificate to add to configuration. Must be in PEM format."
|
||||||
|
files-selected="handleCertsSelected(files, callback)"
|
||||||
|
reset="resetUpload"></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table class="co-table">
|
||||||
|
<thead>
|
||||||
|
<td>Certificate Filename</td>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>Names Handled</td>
|
||||||
|
<td class="options-col"></td>
|
||||||
|
</thead>
|
||||||
|
<tr ng-repeat="certificate in certInfo.certs">
|
||||||
|
<td>{{ certificate.path }}</td>
|
||||||
|
<td class="cert-status">
|
||||||
|
<div ng-if="certificate.error" class="red">
|
||||||
|
<i class="fa fa-exclamation-circle"></i>
|
||||||
|
Error: {{ certificate.error }}
|
||||||
|
</div>
|
||||||
|
<div ng-if="certificate.expired" class="orange">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
Certificate is expired
|
||||||
|
</div>
|
||||||
|
<div ng-if="!certificate.error && !certificate.expired" class="green">
|
||||||
|
<i class="fa fa-check-circle"></i>
|
||||||
|
Certificate is valid
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="empty" ng-if="!certificate.names">(None)</div>
|
||||||
|
<a class="dns-name" ng-href="http://{{ name }}" ng-repeat="name in certificate.names" ng-safenewtab>{{ name }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="options-col">
|
||||||
|
<span class="cor-options-menu">
|
||||||
|
<span class="cor-option" option-click="deleteCert(certificate.path)">
|
||||||
|
<i class="fa fa-times"></i> Delete Certificate
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="empty" ng-if="!certInfo.certs.length" style="margin-top: 20px;">
|
||||||
|
<div class="empty-primary-msg">No custom certificates found.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -13,6 +13,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom SSL certificates -->
|
||||||
|
<div class="co-panel">
|
||||||
|
<div class="co-panel-heading">
|
||||||
|
<i class="fa fa-certificate"></i> Custom SSL Certificates
|
||||||
|
</div>
|
||||||
|
<div class="co-panel-body">
|
||||||
|
<div class="config-certificates-field"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Basic Configuration -->
|
<!-- Basic Configuration -->
|
||||||
<div class="co-panel">
|
<div class="co-panel">
|
||||||
<div class="co-panel-heading">
|
<div class="co-panel-heading">
|
||||||
|
|
|
@ -1254,6 +1254,56 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.directive('configCertificatesField', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/config/config-certificates-field.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: false,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, $upload, ApiService, UserService) {
|
||||||
|
$scope.resetUpload = 0;
|
||||||
|
|
||||||
|
var loadCertificates = function() {
|
||||||
|
$scope.certificatesResource = ApiService.getCustomCertificatesAsResource().get(function(resp) {
|
||||||
|
$scope.certInfo = resp;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
UserService.updateUserIn($scope, function(user) {
|
||||||
|
if (!user.anonymous) {
|
||||||
|
loadCertificates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.handleCertsSelected = function(files, callback) {
|
||||||
|
$upload.upload({
|
||||||
|
url: '/api/v1/superuser/customcerts/' + files[0].name,
|
||||||
|
method: 'POST',
|
||||||
|
data: {'_csrf_token': window.__token},
|
||||||
|
file: files[0]
|
||||||
|
}).success(function() {
|
||||||
|
callback(true);
|
||||||
|
$scope.resetUpload++;
|
||||||
|
loadCertificates();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteCert = function(path) {
|
||||||
|
var errorDisplay = ApiService.errorDisplay('Could not delete certificate');
|
||||||
|
var params = {
|
||||||
|
'certpath': path
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.deleteCustomCertificate(null, params).then(loadCertificates, errorDisplay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
})
|
||||||
|
|
||||||
.directive('configLicenseField', function () {
|
.directive('configLicenseField', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
|
|
@ -9,7 +9,6 @@ angular.module('quay').directive('fileUploadBox', function () {
|
||||||
transclude: true,
|
transclude: true,
|
||||||
restrict: 'C',
|
restrict: 'C',
|
||||||
scope: {
|
scope: {
|
||||||
'allowMultiple': '@allowMultiple',
|
|
||||||
'selectMessage': '@selectMessage',
|
'selectMessage': '@selectMessage',
|
||||||
|
|
||||||
'filesSelected': '&filesSelected',
|
'filesSelected': '&filesSelected',
|
||||||
|
|
|
@ -51,7 +51,9 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
|
||||||
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
SuperUserOrganizationManagement, SuperUserOrganizationList,
|
||||||
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
SuperUserAggregateLogs, SuperUserServiceKeyManagement,
|
||||||
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
||||||
SuperUserTakeOwnership, SuperUserLicense)
|
SuperUserTakeOwnership, SuperUserLicense,
|
||||||
|
SuperUserCustomCertificates,
|
||||||
|
SuperUserCustomCertificate)
|
||||||
from endpoints.api.globalmessages import GlobalUserMessage, GlobalUserMessages
|
from endpoints.api.globalmessages import GlobalUserMessage, GlobalUserMessages
|
||||||
from endpoints.api.secscan import RepositoryImageSecurity
|
from endpoints.api.secscan import RepositoryImageSecurity
|
||||||
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
||||||
|
@ -4170,6 +4172,54 @@ class TestSuperUserList(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserCustomCertificates(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(SuperUserCustomCertificates)
|
||||||
|
|
||||||
|
def test_get_anonymous(self):
|
||||||
|
self._run_test('GET', 401, None, None)
|
||||||
|
|
||||||
|
def test_get_freshuser(self):
|
||||||
|
self._run_test('GET', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_get_reader(self):
|
||||||
|
self._run_test('GET', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_get_devtable(self):
|
||||||
|
self._run_test('GET', 200, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserCustomCertificate(ApiTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
ApiTestCase.setUp(self)
|
||||||
|
self._set_url(SuperUserCustomCertificate, certpath='somecert.crt')
|
||||||
|
|
||||||
|
def test_post_anonymous(self):
|
||||||
|
self._run_test('POST', 401, None, None)
|
||||||
|
|
||||||
|
def test_post_freshuser(self):
|
||||||
|
self._run_test('POST', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_post_reader(self):
|
||||||
|
self._run_test('POST', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_post_devtable(self):
|
||||||
|
self._run_test('POST', 400, 'devtable', None)
|
||||||
|
|
||||||
|
def test_delete_anonymous(self):
|
||||||
|
self._run_test('DELETE', 401, None, None)
|
||||||
|
|
||||||
|
def test_delete_freshuser(self):
|
||||||
|
self._run_test('DELETE', 403, 'freshuser', None)
|
||||||
|
|
||||||
|
def test_delete_reader(self):
|
||||||
|
self._run_test('DELETE', 403, 'reader', None)
|
||||||
|
|
||||||
|
def test_delete_devtable(self):
|
||||||
|
self._run_test('DELETE', 204, 'devtable', None)
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserLicense(ApiTestCase):
|
class TestSuperUserLicense(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.setUp(self)
|
ApiTestCase.setUp(self)
|
||||||
|
|
|
@ -66,12 +66,14 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe
|
||||||
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
RepositoryTeamPermissionList, RepositoryUserPermissionList)
|
||||||
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
|
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
|
||||||
SuperUserServiceKeyManagement, SuperUserServiceKey,
|
SuperUserServiceKeyManagement, SuperUserServiceKey,
|
||||||
SuperUserServiceKeyApproval, SuperUserTakeOwnership,)
|
SuperUserServiceKeyApproval, SuperUserTakeOwnership,
|
||||||
|
SuperUserCustomCertificates, SuperUserCustomCertificate)
|
||||||
from endpoints.api.globalmessages import (GlobalUserMessage, GlobalUserMessages,)
|
from endpoints.api.globalmessages import (GlobalUserMessage, GlobalUserMessages,)
|
||||||
from endpoints.api.secscan import RepositoryImageSecurity
|
from endpoints.api.secscan import RepositoryImageSecurity
|
||||||
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
|
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
|
||||||
SuperUserCreateInitialSuperUser)
|
SuperUserCreateInitialSuperUser)
|
||||||
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
||||||
|
from test.test_ssl_util import generate_test_cert
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -4240,6 +4242,81 @@ class TestRepositoryImageSecurity(ApiTestCase):
|
||||||
self.assertEquals(1, response['data']['Layer']['IndexedByVersion'])
|
self.assertEquals(1, response['data']['Layer']['IndexedByVersion'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuperUserCustomCertificates(ApiTestCase):
|
||||||
|
def test_custom_certificates(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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(set(['somecoolhost', 'bar', 'baz']), set(cert_info['names']))
|
||||||
|
self.assertFalse(cert_info['expired'])
|
||||||
|
|
||||||
|
# Remove the certificate.
|
||||||
|
self.deleteResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'))
|
||||||
|
|
||||||
|
# Make sure it is gone.
|
||||||
|
json = self.getJsonResponse(SuperUserCustomCertificates)
|
||||||
|
self.assertEquals(0, len(json['certs']))
|
||||||
|
|
||||||
|
def test_expired_custom_certificate(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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(set(['somecoolhost']), set(cert_info['names']))
|
||||||
|
self.assertTrue(cert_info['expired'])
|
||||||
|
|
||||||
|
def test_invalid_custom_certificate(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# Upload an invalid certificate.
|
||||||
|
self.postResponse(SuperUserCustomCertificate, params=dict(certpath='testcert'),
|
||||||
|
file=(StringIO('some contents'), 'testcert'), 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('no start line', cert_info['error'])
|
||||||
|
|
||||||
|
def test_path_sanitization(self):
|
||||||
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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'])
|
||||||
|
|
||||||
|
|
||||||
class TestSuperUserTakeOwnership(ApiTestCase):
|
class TestSuperUserTakeOwnership(ApiTestCase):
|
||||||
def test_take_ownership_superuser(self):
|
def test_take_ownership_superuser(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
|
@ -5,42 +5,45 @@ from OpenSSL import crypto
|
||||||
|
|
||||||
from util.security.ssl import load_certificate, CertInvalidException, KeyInvalidException
|
from util.security.ssl import load_certificate, CertInvalidException, KeyInvalidException
|
||||||
|
|
||||||
|
def generate_test_cert(hostname='somehostname', san_list=None, expires=1000000):
|
||||||
|
""" Generates a test SSL certificate and returns the certificate data and private key data. """
|
||||||
|
|
||||||
|
# Based on: http://blog.richardknop.com/2012/08/create-a-self-signed-x509-certificate-in-python/
|
||||||
|
# Create a key pair.
|
||||||
|
k = crypto.PKey()
|
||||||
|
k.generate_key(crypto.TYPE_RSA, 1024)
|
||||||
|
|
||||||
|
# Create a self-signed cert.
|
||||||
|
cert = crypto.X509()
|
||||||
|
cert.get_subject().CN = hostname
|
||||||
|
|
||||||
|
# Add the subjectAltNames (if necessary).
|
||||||
|
if san_list is not None:
|
||||||
|
cert.add_extensions([crypto.X509Extension("subjectAltName", False, ", ".join(san_list))])
|
||||||
|
|
||||||
|
cert.set_serial_number(1000)
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(expires)
|
||||||
|
cert.set_issuer(cert.get_subject())
|
||||||
|
|
||||||
|
cert.set_pubkey(k)
|
||||||
|
cert.sign(k, 'sha1')
|
||||||
|
|
||||||
|
# Dump the certificate and private key in PEM format.
|
||||||
|
cert_data = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||||
|
key_data = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
|
||||||
|
|
||||||
|
return (cert_data, key_data)
|
||||||
|
|
||||||
|
|
||||||
class TestSSLCertificate(unittest.TestCase):
|
class TestSSLCertificate(unittest.TestCase):
|
||||||
def _generate_cert(self, hostname='somehostname', san_list=None, expires=1000000):
|
|
||||||
# Based on: http://blog.richardknop.com/2012/08/create-a-self-signed-x509-certificate-in-python/
|
|
||||||
# Create a key pair.
|
|
||||||
k = crypto.PKey()
|
|
||||||
k.generate_key(crypto.TYPE_RSA, 1024)
|
|
||||||
|
|
||||||
# Create a self-signed cert.
|
|
||||||
cert = crypto.X509()
|
|
||||||
cert.get_subject().CN = hostname
|
|
||||||
|
|
||||||
# Add the subjectAltNames (if necessary).
|
|
||||||
if san_list is not None:
|
|
||||||
cert.add_extensions([crypto.X509Extension("subjectAltName", False, ", ".join(san_list))])
|
|
||||||
|
|
||||||
cert.set_serial_number(1000)
|
|
||||||
cert.gmtime_adj_notBefore(0)
|
|
||||||
cert.gmtime_adj_notAfter(expires)
|
|
||||||
cert.set_issuer(cert.get_subject())
|
|
||||||
|
|
||||||
cert.set_pubkey(k)
|
|
||||||
cert.sign(k, 'sha1')
|
|
||||||
|
|
||||||
# Dump the certificate and private key in PEM format.
|
|
||||||
cert_data = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
||||||
key_data = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
|
|
||||||
|
|
||||||
return (cert_data, key_data)
|
|
||||||
|
|
||||||
def test_load_certificate(self):
|
def test_load_certificate(self):
|
||||||
# Try loading an invalid certificate.
|
# Try loading an invalid certificate.
|
||||||
with self.assertRaisesRegexp(CertInvalidException, 'no start line'):
|
with self.assertRaisesRegexp(CertInvalidException, 'no start line'):
|
||||||
load_certificate('someinvalidcontents')
|
load_certificate('someinvalidcontents')
|
||||||
|
|
||||||
# Load a valid certificate.
|
# Load a valid certificate.
|
||||||
(public_key_data, _) = self._generate_cert()
|
(public_key_data, _) = generate_test_cert()
|
||||||
|
|
||||||
cert = load_certificate(public_key_data)
|
cert = load_certificate(public_key_data)
|
||||||
self.assertFalse(cert.expired)
|
self.assertFalse(cert.expired)
|
||||||
|
@ -48,13 +51,13 @@ class TestSSLCertificate(unittest.TestCase):
|
||||||
self.assertTrue(cert.matches_name('somehostname'))
|
self.assertTrue(cert.matches_name('somehostname'))
|
||||||
|
|
||||||
def test_expired_certificate(self):
|
def test_expired_certificate(self):
|
||||||
(public_key_data, _) = self._generate_cert(expires=-100)
|
(public_key_data, _) = generate_test_cert(expires=-100)
|
||||||
|
|
||||||
cert = load_certificate(public_key_data)
|
cert = load_certificate(public_key_data)
|
||||||
self.assertTrue(cert.expired)
|
self.assertTrue(cert.expired)
|
||||||
|
|
||||||
def test_hostnames(self):
|
def test_hostnames(self):
|
||||||
(public_key_data, _) = self._generate_cert(hostname='foo', san_list=['DNS:bar', 'DNS:baz'])
|
(public_key_data, _) = generate_test_cert(hostname='foo', san_list=['DNS:bar', 'DNS:baz'])
|
||||||
cert = load_certificate(public_key_data)
|
cert = load_certificate(public_key_data)
|
||||||
self.assertEquals(set(['foo', 'bar', 'baz']), cert.names)
|
self.assertEquals(set(['foo', 'bar', 'baz']), cert.names)
|
||||||
|
|
||||||
|
@ -62,12 +65,12 @@ class TestSSLCertificate(unittest.TestCase):
|
||||||
self.assertTrue(cert.matches_name(name))
|
self.assertTrue(cert.matches_name(name))
|
||||||
|
|
||||||
def test_nondns_hostnames(self):
|
def test_nondns_hostnames(self):
|
||||||
(public_key_data, _) = self._generate_cert(hostname='foo', san_list=['URI:yarg'])
|
(public_key_data, _) = generate_test_cert(hostname='foo', san_list=['URI:yarg'])
|
||||||
cert = load_certificate(public_key_data)
|
cert = load_certificate(public_key_data)
|
||||||
self.assertEquals(set(['foo']), cert.names)
|
self.assertEquals(set(['foo']), cert.names)
|
||||||
|
|
||||||
def test_validate_private_key(self):
|
def test_validate_private_key(self):
|
||||||
(public_key_data, private_key_data) = self._generate_cert()
|
(public_key_data, private_key_data) = generate_test_cert()
|
||||||
|
|
||||||
private_key = NamedTemporaryFile(delete=True)
|
private_key = NamedTemporaryFile(delete=True)
|
||||||
private_key.write(private_key_data)
|
private_key.write(private_key_data)
|
||||||
|
@ -77,7 +80,7 @@ class TestSSLCertificate(unittest.TestCase):
|
||||||
cert.validate_private_key(private_key.name)
|
cert.validate_private_key(private_key.name)
|
||||||
|
|
||||||
def test_invalid_private_key(self):
|
def test_invalid_private_key(self):
|
||||||
(public_key_data, _) = self._generate_cert()
|
(public_key_data, _) = generate_test_cert()
|
||||||
|
|
||||||
private_key = NamedTemporaryFile(delete=True)
|
private_key = NamedTemporaryFile(delete=True)
|
||||||
private_key.write('somerandomdata')
|
private_key.write('somerandomdata')
|
||||||
|
@ -88,8 +91,8 @@ class TestSSLCertificate(unittest.TestCase):
|
||||||
cert.validate_private_key(private_key.name)
|
cert.validate_private_key(private_key.name)
|
||||||
|
|
||||||
def test_mismatch_private_key(self):
|
def test_mismatch_private_key(self):
|
||||||
(public_key_data, _) = self._generate_cert()
|
(public_key_data, _) = generate_test_cert()
|
||||||
(_, private_key_data) = self._generate_cert()
|
(_, private_key_data) = generate_test_cert()
|
||||||
|
|
||||||
private_key = NamedTemporaryFile(delete=True)
|
private_key = NamedTemporaryFile(delete=True)
|
||||||
private_key.write(private_key_data)
|
private_key.write(private_key_data)
|
||||||
|
|
|
@ -68,6 +68,9 @@ class FileConfigProvider(BaseProvider):
|
||||||
if not os.path.exists(dirpath):
|
if not os.path.exists(dirpath):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not os.path.isdir(dirpath):
|
||||||
|
return None
|
||||||
|
|
||||||
return os.listdir(dirpath)
|
return os.listdir(dirpath)
|
||||||
|
|
||||||
def save_volume_file(self, filename, flask_file):
|
def save_volume_file(self, filename, flask_file):
|
||||||
|
|
|
@ -57,7 +57,7 @@ class TestConfigProvider(BaseProvider):
|
||||||
return filename in self.files
|
return filename in self.files
|
||||||
|
|
||||||
def save_volume_file(self, filename, flask_file):
|
def save_volume_file(self, filename, flask_file):
|
||||||
self.files[filename] = ''
|
self.files[filename] = flask_file.read()
|
||||||
|
|
||||||
def write_volume_file(self, filename, contents):
|
def write_volume_file(self, filename, contents):
|
||||||
self.files[filename] = contents
|
self.files[filename] = contents
|
||||||
|
@ -68,6 +68,17 @@ class TestConfigProvider(BaseProvider):
|
||||||
|
|
||||||
return io.BytesIO(self.files[filename])
|
return io.BytesIO(self.files[filename])
|
||||||
|
|
||||||
|
def remove_volume_file(self, filename):
|
||||||
|
self.files.pop(filename, None)
|
||||||
|
|
||||||
|
def list_volume_directory(self, path):
|
||||||
|
paths = []
|
||||||
|
for filename in self.files:
|
||||||
|
if filename.startswith(path):
|
||||||
|
paths.append(filename[len(path)+1:])
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
def requires_restart(self, app_config):
|
def requires_restart(self, app_config):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ ACI_CERT_FILENAMES = ['signing-public.gpg', 'signing-private.gpg']
|
||||||
LDAP_FILENAMES = [LDAP_CERT_FILENAME]
|
LDAP_FILENAMES = [LDAP_CERT_FILENAME]
|
||||||
CONFIG_FILENAMES = (SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES +
|
CONFIG_FILENAMES = (SSL_FILENAMES + DB_SSL_FILENAMES + JWT_FILENAMES + ACI_CERT_FILENAMES +
|
||||||
LDAP_FILENAMES)
|
LDAP_FILENAMES)
|
||||||
|
EXTRA_CA_DIRECTORY = 'extra_ca_certs'
|
||||||
|
|
||||||
def get_storage_providers(config):
|
def get_storage_providers(config):
|
||||||
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
|
storage_config = config.get('DISTRIBUTED_STORAGE_CONFIG', {})
|
||||||
|
|
Reference in a new issue