Enforce san cert hostname limit on cert create
Add settings to cert_info_storage Add settings admin endpoints Add limit check log to taskflow and provider layers Change-Id: Ie0db8f962363590a70bc387ca9387e2fb22829b1
This commit is contained in:
parent
534b928bd4
commit
33b104b11f
|
@ -344,6 +344,37 @@ class DefaultSSLCertificateController(base.SSLCertificateController):
|
|||
|
||||
return res
|
||||
|
||||
def get_san_cert_hostname_limit(self):
|
||||
if 'akamai' in self._driver.providers:
|
||||
akamai_driver = self._driver.providers['akamai'].obj
|
||||
res = akamai_driver.cert_info_storage.get_san_cert_hostname_limit()
|
||||
res = {'san_cert_hostname_limit': res}
|
||||
else:
|
||||
# if not using akamai driver just return an empty list
|
||||
res = {'san_cert_hostname_limit': 0}
|
||||
|
||||
return res
|
||||
|
||||
def set_san_cert_hostname_limit(self, request_json):
|
||||
if 'akamai' in self._driver.providers:
|
||||
try:
|
||||
new_limit = request_json['san_cert_hostname_limit']
|
||||
except Exception as exc:
|
||||
LOG.error("Error attempting to update san settings {0}".format(
|
||||
exc
|
||||
))
|
||||
raise ValueError('Unknown setting!')
|
||||
|
||||
akamai_driver = self._driver.providers['akamai'].obj
|
||||
res = akamai_driver.cert_info_storage.set_san_cert_hostname_limit(
|
||||
new_limit
|
||||
)
|
||||
else:
|
||||
# if not using akamai driver just return an empty list
|
||||
res = 0
|
||||
|
||||
return res
|
||||
|
||||
def get_certs_by_status(self, status):
|
||||
certs_by_status = self.storage.get_certs_by_status(status)
|
||||
|
||||
|
|
|
@ -130,7 +130,8 @@ class CassandraSanInfoStorage(base.BaseAkamaiSanInfoStorage):
|
|||
|
||||
stmt = query.SimpleStatement(
|
||||
GET_PROVIDER_INFO,
|
||||
consistency_level=self.consistency_level)
|
||||
consistency_level=self.consistency_level
|
||||
)
|
||||
results = self.session.execute(stmt, args)
|
||||
complete_results = list(results)
|
||||
if len(complete_results) != 1:
|
||||
|
@ -143,6 +144,39 @@ class CassandraSanInfoStorage(base.BaseAkamaiSanInfoStorage):
|
|||
def _get_akamai_san_certs_info(self):
|
||||
return json.loads(self._get_akamai_provider_info()['info']['san_info'])
|
||||
|
||||
def _get_akamai_san_certs_settings(self):
|
||||
try:
|
||||
return json.loads(
|
||||
self._get_akamai_provider_info()['info']['settings']
|
||||
)
|
||||
except KeyError as ke:
|
||||
LOG.error(
|
||||
'Error retrieving cert info storage settings. {0}'.format(ke)
|
||||
)
|
||||
# settings doesn't exist in the table
|
||||
self._seed_san_info_settings()
|
||||
|
||||
return json.loads(self._get_akamai_provider_info()['info']['settings'])
|
||||
|
||||
def _seed_san_info_settings(self):
|
||||
provider_info = dict(self._get_akamai_provider_info()['info'])
|
||||
provider_info['settings'] = json.dumps(
|
||||
{
|
||||
'san_cert_hostname_limit': 80
|
||||
}
|
||||
)
|
||||
|
||||
stmt = query.SimpleStatement(
|
||||
UPDATE_PROVIDER_INFO,
|
||||
consistency_level=self.consistency_level)
|
||||
|
||||
args = {
|
||||
'provider_name': 'akamai',
|
||||
'info': provider_info
|
||||
}
|
||||
|
||||
self.session.execute(stmt, args)
|
||||
|
||||
def list_all_san_cert_names(self):
|
||||
return self._get_akamai_san_certs_info().keys()
|
||||
|
||||
|
@ -279,3 +313,38 @@ class CassandraSanInfoStorage(base.BaseAkamaiSanInfoStorage):
|
|||
}
|
||||
|
||||
self.session.execute(stmt, args)
|
||||
|
||||
def get_san_cert_hostname_limit(self):
|
||||
"""Get the san cert hostname limit setting.
|
||||
|
||||
:returns the hostname limit if the limit exists else None.
|
||||
"""
|
||||
|
||||
return self._get_akamai_san_certs_settings().get(
|
||||
'san_cert_hostname_limit'
|
||||
)
|
||||
|
||||
def set_san_cert_hostname_limit(self, new_hostname_limit):
|
||||
settings = self._get_akamai_san_certs_settings()
|
||||
|
||||
if settings is None:
|
||||
raise ValueError('No san cert settings found.')
|
||||
|
||||
settings['san_cert_hostname_limit'] = new_hostname_limit
|
||||
|
||||
# Change the previous san info in the overall provider_info dictionary
|
||||
provider_info = dict(self._get_akamai_provider_info()['info'])
|
||||
provider_info['settings'] = json.dumps(settings)
|
||||
|
||||
stmt = query.SimpleStatement(
|
||||
UPDATE_PROVIDER_INFO,
|
||||
consistency_level=self.consistency_level
|
||||
)
|
||||
|
||||
args = {
|
||||
'provider_name': 'akamai',
|
||||
'info': provider_info
|
||||
}
|
||||
|
||||
self.session.execute(stmt, args)
|
||||
return self.get_san_cert_hostname_limit()
|
||||
|
|
|
@ -88,6 +88,10 @@ class CertificateController(base.CertificateBase):
|
|||
)
|
||||
})
|
||||
|
||||
san_cert_hostname_limit = (
|
||||
self.cert_info_storage.get_san_cert_hostname_limit()
|
||||
)
|
||||
|
||||
for san_cert_name in self.san_cert_cnames:
|
||||
enabled = (
|
||||
self.cert_info_storage.get_enabled_status(
|
||||
|
@ -96,6 +100,25 @@ class CertificateController(base.CertificateBase):
|
|||
)
|
||||
if not enabled:
|
||||
continue
|
||||
|
||||
# if the limit provided as an arg to this function is None
|
||||
# default san_cert_hostname_limit to the value provided in
|
||||
# the config file.
|
||||
san_cert_hostname_limit = (
|
||||
san_cert_hostname_limit or
|
||||
self.driver.san_cert_hostname_limit
|
||||
)
|
||||
|
||||
# Check san_cert to enforce number of hosts hasn't
|
||||
# reached the limit. If the current san_cert is at max
|
||||
# capacity continue to the next san_cert
|
||||
san_hosts = utils.get_ssl_number_of_hosts(
|
||||
san_cert_name +
|
||||
self.driver.akamai_https_access_url_suffix
|
||||
)
|
||||
if san_hosts >= san_cert_hostname_limit:
|
||||
continue
|
||||
|
||||
last_sps_id = (
|
||||
self.cert_info_storage.get_cert_last_spsid(
|
||||
san_cert_name
|
||||
|
|
|
@ -216,36 +216,43 @@ class AkamaiRetryListController(base.Controller, hooks.HookController):
|
|||
res, deleted = (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
update_san_retry_list(queue_data))
|
||||
# queue is the new queue, and deleted is deleted items
|
||||
return {"queue": res, "deleted": deleted}
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
# queue is the new queue, and deleted is deleted items
|
||||
return {"queue": res, "deleted": deleted}
|
||||
|
||||
|
||||
class AkamaiSanCertConfigController(base.Controller, hooks.HookController):
|
||||
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
|
||||
|
||||
@pecan.expose('json')
|
||||
@decorators.validate(
|
||||
san_cert_name=rule.Rule(
|
||||
helpers.is_valid_domain_by_name(),
|
||||
query=rule.Rule(
|
||||
helpers.is_valid_domain_by_name_or_akamai_setting(),
|
||||
helpers.abort_with_message))
|
||||
def get_one(self, san_cert_name):
|
||||
def get_one(self, query):
|
||||
|
||||
try:
|
||||
res = (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
get_san_cert_configuration(san_cert_name))
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
return res
|
||||
if query == 'san_cert_hostname_limit':
|
||||
try:
|
||||
return (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
get_san_cert_hostname_limit()
|
||||
)
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
else:
|
||||
try:
|
||||
return (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
get_san_cert_configuration(query)
|
||||
)
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
@pecan.expose('json')
|
||||
@decorators.validate(
|
||||
san_cert_name=rule.Rule(
|
||||
helpers.is_valid_domain_by_name(),
|
||||
query=rule.Rule(
|
||||
helpers.is_valid_domain_by_name_or_akamai_setting(),
|
||||
helpers.abort_with_message),
|
||||
request=rule.Rule(
|
||||
helpers.json_matches_service_schema(
|
||||
|
@ -253,17 +260,25 @@ class AkamaiSanCertConfigController(base.Controller, hooks.HookController):
|
|||
"config", "POST")),
|
||||
helpers.abort_with_message,
|
||||
stoplight_helpers.pecan_getter))
|
||||
def post(self, san_cert_name):
|
||||
config_json = json.loads(pecan.request.body.decode('utf-8'))
|
||||
def post(self, query):
|
||||
request_json = json.loads(pecan.request.body.decode('utf-8'))
|
||||
|
||||
try:
|
||||
res = (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
update_san_cert_configuration(san_cert_name, config_json))
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
if query == 'san_cert_hostname_limit':
|
||||
try:
|
||||
self._driver.manager.ssl_certificate_controller. \
|
||||
set_san_cert_hostname_limit(request_json)
|
||||
|
||||
return res
|
||||
return pecan.Response(None, 202)
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
else:
|
||||
try:
|
||||
res = (
|
||||
self._driver.manager.ssl_certificate_controller.
|
||||
update_san_cert_configuration(query, request_json))
|
||||
return res
|
||||
except Exception as e:
|
||||
pecan.abort(400, str(e))
|
||||
|
||||
|
||||
class AkamaiSSLCertificateController(base.Controller, hooks.HookController):
|
||||
|
|
|
@ -189,6 +189,37 @@ def is_valid_project_id(project_id):
|
|||
'{0}'.format(project_id))
|
||||
|
||||
|
||||
@decorators.validation_function
|
||||
def is_valid_akamai_setting(setting):
|
||||
if setting not in ['san_cert_hostname_limit']:
|
||||
raise exceptions.ValidationFailed(
|
||||
'Invalid akamai setting : {0}'.format(setting)
|
||||
)
|
||||
|
||||
|
||||
@decorators.validation_function
|
||||
def is_valid_domain_by_name_or_akamai_setting(query):
|
||||
valid_domain = True
|
||||
domain_exc = None
|
||||
valid_setting = True
|
||||
setting_exc = None
|
||||
|
||||
try:
|
||||
is_valid_domain_by_name(query)
|
||||
except Exception as exc:
|
||||
valid_domain = False
|
||||
domain_exc = exc
|
||||
|
||||
try:
|
||||
is_valid_akamai_setting(query)
|
||||
except Exception as exc:
|
||||
valid_setting = False
|
||||
setting_exc = exc
|
||||
|
||||
if valid_domain is False and valid_setting is False:
|
||||
raise exceptions.ValidationFailed(str(domain_exc) + str(setting_exc))
|
||||
|
||||
|
||||
def is_root_domain(domain):
|
||||
domain_name = domain.get('domain')
|
||||
|
||||
|
|
|
@ -100,6 +100,11 @@ class SSLCertificateSchema(schema_base.SchemaBase):
|
|||
},
|
||||
'enabled': {
|
||||
'type': 'boolean'
|
||||
},
|
||||
'san_cert_hostname_limit': {
|
||||
'type': 'integer',
|
||||
'minimum': 1,
|
||||
'maximum': 200,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
# Copyright (c) 2016 Rackspace, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from tests.functional.transport.pecan import base
|
||||
|
||||
|
||||
class AdminControllerTest(base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(AdminControllerTest, self).setUp()
|
||||
|
||||
self.project_id = str(uuid.uuid1())
|
||||
|
||||
def tearDown(self):
|
||||
super(AdminControllerTest, self).tearDown()
|
||||
|
||||
def test_put_settings_positive(self):
|
||||
settings_json = {
|
||||
'san_cert_hostname_limit': 10
|
||||
}
|
||||
|
||||
# create with good data
|
||||
response = self.app.post(
|
||||
'/v1.0/admin/provider/akamai/ssl_certificate/'
|
||||
'config/san_cert_hostname_limit',
|
||||
params=json.dumps(settings_json),
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
}
|
||||
)
|
||||
self.assertEqual(202, response.status_code)
|
||||
|
||||
def test_put_akamai_settings_negative(self):
|
||||
settings_json = {
|
||||
'san_cert_hostname_limit': 10
|
||||
}
|
||||
|
||||
# create with bad endpoint which fails validation with 400 error
|
||||
response = self.app.post(
|
||||
'/v1.0/admin/provider/akamai/ssl_certificate/'
|
||||
'config/unknown_setting',
|
||||
params=json.dumps(settings_json),
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True
|
||||
)
|
||||
self.assertEqual(400, response.status_code)
|
||||
|
||||
def test_get_settings_positive(self):
|
||||
response = self.app.get(
|
||||
'/v1.0/admin/provider/akamai/ssl_certificate/'
|
||||
'config/san_cert_hostname_limit',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
}
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_get_akamai_settings_negative(self):
|
||||
response = self.app.get(
|
||||
'/v1.0/admin/provider/akamai/ssl_certificate/'
|
||||
'config/unknown_setting',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'X-Project-ID': self.project_id
|
||||
},
|
||||
expect_errors=True
|
||||
)
|
||||
self.assertEqual(400, response.status_code)
|
|
@ -114,7 +114,7 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
|
|||
with testtools.ExpectedException(ValueError):
|
||||
self.scc.get_san_cert_configuration("non-existant")
|
||||
|
||||
def test_update_san_cert_configuration_positive(self):
|
||||
def test_set_san_cert_hostname_limit_positive(self):
|
||||
resp = mock.Mock()
|
||||
resp.status_code = 200
|
||||
resp.json.return_value = {
|
||||
|
@ -136,6 +136,29 @@ class DefaultSSLCertificateControllerTests(base.TestCase):
|
|||
)
|
||||
)
|
||||
|
||||
def test_update_san_cert_configuration_positive(self):
|
||||
|
||||
self.scc.set_san_cert_hostname_limit(
|
||||
{"san_cert_hostname_limit": '1234'}
|
||||
)
|
||||
|
||||
cert_info_storage = self.mock_providers['akamai'].obj.cert_info_storage
|
||||
|
||||
cert_info_storage.set_san_cert_hostname_limit.\
|
||||
assert_called_once_with('1234')
|
||||
|
||||
def test_update_san_cert_configuration_negative(self):
|
||||
|
||||
with testtools.ExpectedException(ValueError):
|
||||
self.scc.set_san_cert_hostname_limit(
|
||||
{"invalid_setting_name": '1234'}
|
||||
)
|
||||
|
||||
cert_info_storage = self.mock_providers['akamai'].obj.cert_info_storage
|
||||
|
||||
self.assertFalse(
|
||||
cert_info_storage.set_san_cert_hostname_limit.called)
|
||||
|
||||
def test_update_san_cert_configuration_no_sps_id(self):
|
||||
api_client = self.mock_providers['akamai'].obj.sps_api_client
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ class TestCassandraCertInfoStorage(base.TestCase):
|
|||
|
||||
self.conf = cfg.ConfigOpts()
|
||||
|
||||
self.default_limit = 80
|
||||
self.get_returned_value = [{'info': {
|
||||
'san_info':
|
||||
'{"secure2.san1.test-cdn.com": '
|
||||
|
@ -52,7 +53,9 @@ class TestCassandraCertInfoStorage(base.TestCase):
|
|||
'"secure1.san1.test-cdn.com": '
|
||||
'{"ipVersion": "ipv4", "issuer": "symentec", '
|
||||
'"slot_deployment_klass": "esslType", '
|
||||
'"jobId": "1432", "spsId": 1423}}'}}]
|
||||
'"jobId": "1432", "spsId": 1423}}',
|
||||
'settings': '{"san_cert_hostname_limit": 80}'
|
||||
}}]
|
||||
|
||||
@mock.patch.object(
|
||||
cassandra_storage,
|
||||
|
@ -188,3 +191,28 @@ class TestCassandraCertInfoStorage(base.TestCase):
|
|||
cert_name, {'spsId': new_spsId}
|
||||
)
|
||||
mock_execute.assert_called()
|
||||
|
||||
def test_set_san_cert_hostname_limit(self):
|
||||
self.cassandra_storage = cassandra_storage.CassandraSanInfoStorage(
|
||||
self.conf
|
||||
)
|
||||
|
||||
mock_execute = self.cassandra_storage.session.execute
|
||||
mock_execute.return_value = self.get_returned_value
|
||||
|
||||
self.cassandra_storage.set_san_cert_hostname_limit(99)
|
||||
|
||||
mock_execute.assert_called()
|
||||
|
||||
def test_get_san_cert_hostname_limit(self):
|
||||
self.cassandra_storage = cassandra_storage.CassandraSanInfoStorage(
|
||||
self.conf
|
||||
)
|
||||
|
||||
mock_execute = self.cassandra_storage.session.execute
|
||||
mock_execute.return_value = self.get_returned_value
|
||||
|
||||
res = self.cassandra_storage.get_san_cert_hostname_limit()
|
||||
|
||||
mock_execute.assert_called()
|
||||
self.assertEqual(res, self.default_limit)
|
||||
|
|
|
@ -36,13 +36,20 @@ class TestCertificates(base.TestCase):
|
|||
'example.net'
|
||||
)
|
||||
|
||||
background_job_controller_patcher = mock.patch(
|
||||
san_by_host_patcher = mock.patch(
|
||||
'poppy.provider.akamai.utils.get_sans_by_host'
|
||||
)
|
||||
self.mock_get_sans_by_host = background_job_controller_patcher.start()
|
||||
self.addCleanup(background_job_controller_patcher.stop)
|
||||
self.mock_get_sans_by_host = san_by_host_patcher.start()
|
||||
self.addCleanup(san_by_host_patcher.stop)
|
||||
|
||||
ssl_number_of_hosts_patcher = mock.patch(
|
||||
'poppy.provider.akamai.utils.get_ssl_number_of_hosts'
|
||||
)
|
||||
self.mock_get_ssl_number_of_hosts = ssl_number_of_hosts_patcher.start()
|
||||
self.addCleanup(ssl_number_of_hosts_patcher.stop)
|
||||
|
||||
self.mock_get_sans_by_host.return_value = []
|
||||
self.mock_get_ssl_number_of_hosts.return_value = 10
|
||||
|
||||
self.controller = certificates.CertificateController(self.driver)
|
||||
|
||||
|
@ -77,6 +84,9 @@ class TestCertificates(base.TestCase):
|
|||
'slot-deployment.class': 'esslType'
|
||||
}
|
||||
|
||||
controller.cert_info_storage.get_san_cert_hostname_limit. \
|
||||
return_value = 80
|
||||
|
||||
cert_info = controller.cert_info_storage.get_cert_info(
|
||||
"secure.san1.poppycdn.com")
|
||||
cert_info['add.sans'] = "www.abc.com"
|
||||
|
@ -149,6 +159,9 @@ class TestCertificates(base.TestCase):
|
|||
controller.cert_info_storage.get_cert_last_spsid(
|
||||
"secure.san1.poppycdn.com"))
|
||||
|
||||
controller.cert_info_storage.get_san_cert_hostname_limit. \
|
||||
return_value = 80
|
||||
|
||||
controller.cert_info_storage.get_cert_info.return_value = {
|
||||
'cnameHostname': "secure.san1.poppycdn.com",
|
||||
'jobId': "secure.san1.poppycdn.com",
|
||||
|
@ -246,6 +259,9 @@ class TestCertificates(base.TestCase):
|
|||
controller.cert_info_storage.get_cert_last_spsid(
|
||||
"secure.san1.poppycdn.com"))
|
||||
|
||||
controller.cert_info_storage.get_san_cert_hostname_limit. \
|
||||
return_value = 80
|
||||
|
||||
controller.cert_info_storage.get_cert_info.return_value = {
|
||||
'cnameHostname': "secure.san1.poppycdn.com",
|
||||
'jobId': "secure.san1.poppycdn.com",
|
||||
|
@ -303,6 +319,9 @@ class TestCertificates(base.TestCase):
|
|||
controller.cert_info_storage.get_cert_last_spsid(
|
||||
"secure.san1.poppycdn.com"))
|
||||
|
||||
controller.cert_info_storage.get_san_cert_hostname_limit. \
|
||||
return_value = 80
|
||||
|
||||
controller.cert_info_storage.get_cert_info.return_value = {
|
||||
'cnameHostname': "secure.san1.poppycdn.com",
|
||||
'jobId': "secure.san1.poppycdn.com",
|
||||
|
|
Loading…
Reference in New Issue