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:
Isaac Mungai 2016-07-19 11:40:53 -04:00
parent 534b928bd4
commit 33b104b11f
10 changed files with 363 additions and 31 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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')

View File

@ -100,6 +100,11 @@ class SSLCertificateSchema(schema_base.SchemaBase):
},
'enabled': {
'type': 'boolean'
},
'san_cert_hostname_limit': {
'type': 'integer',
'minimum': 1,
'maximum': 200,
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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",