Add an API to rotate a cluster CA certificate
This will give admins a way to revoke access to an existing cluster once a user has been granted access. Bumped the API microversion to 1.5 for the new endpoint. Deprecated policy certificate:get in favor of certificate:get_ca for clarity and consistency. Depends-On: Ie960464e45445e195e75b91e8d65a4046eb21e93 Implements: blueprint revoke-cluster-cert Change-Id: Ief28bef3a79f212acf4166e443a96e5419fbb757
This commit is contained in:
parent
06b97cc7d7
commit
a65ef7d3c3
@ -118,3 +118,29 @@ Response Example
|
||||
|
||||
.. literalinclude:: samples/certificates-ca-sign-resp.json
|
||||
:language: javascript
|
||||
|
||||
Rotate the CA certificate for a bay/cluster
|
||||
===========================================
|
||||
|
||||
.. rest_method:: PATCH /v1/certificates/{bay_uuid/cluster_uuid}
|
||||
|
||||
Rotate the CA certificate for a bay/cluster and invalidate all user
|
||||
certificates.
|
||||
|
||||
Response Codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 202
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 400
|
||||
|
||||
Request
|
||||
-------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- cluster: cluster_id
|
||||
|
@ -37,6 +37,7 @@
|
||||
|
||||
"certificate:create": "rule:admin_or_user",
|
||||
"certificate:get": "rule:admin_or_user",
|
||||
"certificate:rotate_ca": "rule:admin_or_owner",
|
||||
|
||||
"magnum-service:get_all": "rule:admin_api",
|
||||
"stats:get_all": "rule:admin_or_owner"
|
||||
|
@ -166,3 +166,15 @@ class CertificateController(base.Controller):
|
||||
new_cert = pecan.request.rpcapi.sign_certificate(cluster,
|
||||
cert_obj)
|
||||
return Certificate.convert_with_links(new_cert)
|
||||
|
||||
@base.Controller.api_version("1.5")
|
||||
@expose.expose(None, types.uuid_or_name, status_code=202)
|
||||
def patch(self, cluster_ident):
|
||||
context = pecan.request.context
|
||||
cluster = api_utils.get_resource('Cluster', cluster_ident)
|
||||
policy.enforce(context, 'certificate:rotate_ca', cluster,
|
||||
action='certificate:rotate_ca')
|
||||
if cluster.cluster_template.tls_disabled:
|
||||
raise exception.NotSupported("Rotating the CA certificate on a "
|
||||
"non-TLS cluster is not supported")
|
||||
pecan.request.rpcapi.rotate_ca_certificate(cluster)
|
||||
|
@ -37,10 +37,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
* 1.2 - Async bay operations support
|
||||
* 1.3 - Add bay rollback support
|
||||
* 1.4 - Add stats API
|
||||
* 1.5 - Add cluster CA certificate rotation support
|
||||
"""
|
||||
|
||||
BASE_VER = '1.1'
|
||||
CURRENT_MAX_VER = '1.4'
|
||||
CURRENT_MAX_VER = '1.5'
|
||||
|
||||
|
||||
class Version(object):
|
||||
|
@ -57,3 +57,12 @@ user documentation.
|
||||
- http://XXX/v1/stats or
|
||||
- http://XXX/v1/stats?project_id=<project-id> or
|
||||
- http://XXX/v1/stats?project_id=<project-id>&type=<stats-type>
|
||||
|
||||
|
||||
1.5
|
||||
---
|
||||
|
||||
Support for cluster CA certificate rotation
|
||||
|
||||
This gives admins a way to revoke access to an existing cluster once
|
||||
a user has been granted access.
|
||||
|
@ -58,6 +58,9 @@ class API(rpc_service.API):
|
||||
def get_ca_certificate(self, cluster):
|
||||
return self._call('get_ca_certificate', cluster=cluster)
|
||||
|
||||
def rotate_ca_certificate(self, cluster):
|
||||
return self._call('rotate_ca_certificate', cluster=cluster)
|
||||
|
||||
# Versioned Objects indirection API
|
||||
|
||||
def object_class_action(self, context, objname, objmethod, objver,
|
||||
|
@ -16,6 +16,7 @@
|
||||
from oslo_log import log as logging
|
||||
|
||||
from magnum.conductor.handlers.common import cert_manager
|
||||
from magnum.drivers.common import driver
|
||||
from magnum import objects
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -45,3 +46,8 @@ class Handler(object):
|
||||
certificate = objects.Certificate.from_object_cluster(cluster)
|
||||
certificate.pem = ca_cert.get_certificate()
|
||||
return certificate
|
||||
|
||||
def rotate_ca_certificate(self, context, cluster):
|
||||
cluster_driver = driver.Driver.get_driver_for_cluster(context,
|
||||
cluster)
|
||||
cluster_driver.rotate_ca_certificate(context, cluster)
|
||||
|
@ -184,3 +184,7 @@ class Driver(object):
|
||||
"""return the scale manager for this driver."""
|
||||
|
||||
return None
|
||||
|
||||
def rotate_ca_certificate(self, context, cluster):
|
||||
raise exception.NotSupported(
|
||||
"'rotate_ca_certificate' is not supported by this driver.")
|
||||
|
@ -38,7 +38,7 @@ class CertClient(client.MagnumClient):
|
||||
:returns: response object and ClusterCollection object
|
||||
"""
|
||||
|
||||
resp, body = self.get(self.cert_uri(cluster_id))
|
||||
resp, body = self.get(self.cert_uri(cluster_id), **kwargs)
|
||||
return self.deserialize(resp, body, cert_model.CertEntity)
|
||||
|
||||
def post_cert(self, model, **kwargs):
|
||||
|
@ -23,6 +23,11 @@ from magnum.tests.functional.common import config
|
||||
from magnum.tests.functional.common import datagen
|
||||
|
||||
|
||||
HEADERS = {'OpenStack-API-Version': 'container-infra latest',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
class ClusterTest(base.BaseTempestTest):
|
||||
|
||||
"""Tests for cluster CRUD."""
|
||||
@ -118,7 +123,7 @@ class ClusterTest(base.BaseTempestTest):
|
||||
self.assertEqual(204, resp.status)
|
||||
self.cluster_client.wait_for_cluster_to_delete(cluster_id)
|
||||
self.assertRaises(exceptions.NotFound, self.cert_client.get_cert,
|
||||
cluster_id)
|
||||
cluster_id, headers=HEADERS)
|
||||
return resp, model
|
||||
|
||||
def _get_cluster_by_id(self, cluster_id):
|
||||
@ -153,7 +158,7 @@ class ClusterTest(base.BaseTempestTest):
|
||||
|
||||
# test ca show
|
||||
resp, cert_model = self.cert_client.get_cert(
|
||||
cluster_model.uuid)
|
||||
cluster_model.uuid, headers=HEADERS)
|
||||
self.LOG.debug("cert resp: %s" % resp)
|
||||
self.assertEqual(200, resp.status)
|
||||
self.assertEqual(cert_model.cluster_uuid, cluster_model.uuid)
|
||||
@ -179,7 +184,8 @@ Q0uA0aVog3f5iJxCa3Hp5gxbJQ6zV6kJ0TEsuaaOhEko9sdpCoPOnRBm2i/XRD2D
|
||||
|
||||
cert_data_model = datagen.cert_data(cluster_model.uuid,
|
||||
csr_data=csr_sample)
|
||||
resp, cert_model = self.cert_client.post_cert(cert_data_model)
|
||||
resp, cert_model = self.cert_client.post_cert(cert_data_model,
|
||||
headers=HEADERS)
|
||||
self.LOG.debug("cert resp: %s" % resp)
|
||||
self.assertEqual(201, resp.status)
|
||||
self.assertEqual(cert_model.cluster_uuid, cluster_model.uuid)
|
||||
@ -193,7 +199,7 @@ Q0uA0aVog3f5iJxCa3Hp5gxbJQ6zV6kJ0TEsuaaOhEko9sdpCoPOnRBm2i/XRD2D
|
||||
self.assertRaises(
|
||||
exceptions.BadRequest,
|
||||
self.cert_client.post_cert,
|
||||
cert_data_model)
|
||||
cert_data_model, headers=HEADERS)
|
||||
|
||||
# test cluster delete
|
||||
self._delete_cluster(cluster_model.uuid)
|
||||
|
@ -112,7 +112,8 @@ class BaseMagnumClient(base.BaseMagnumTest):
|
||||
project_domain_id=project_domain_id,
|
||||
service_type='container-infra',
|
||||
region_name=region_name,
|
||||
magnum_url=magnum_url)
|
||||
magnum_url=magnum_url,
|
||||
api_version='latest')
|
||||
cls.keystone = ksclient.Client(username=user,
|
||||
password=passwd,
|
||||
project_name=project_name,
|
||||
|
@ -40,7 +40,7 @@ class TestRootController(api_base.FunctionalTest):
|
||||
[{u'href': u'http://localhost/v1/',
|
||||
u'rel': u'self'}],
|
||||
u'status': u'CURRENT',
|
||||
u'max_version': u'1.4',
|
||||
u'max_version': u'1.5',
|
||||
u'min_version': u'1.1'}]}
|
||||
|
||||
self.v1_expected = {
|
||||
|
@ -20,6 +20,9 @@ from magnum.tests.unit.api import utils as api_utils
|
||||
from magnum.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
HEADERS = {'OpenStack-API-Version': 'container-infra latest'}
|
||||
|
||||
|
||||
class TestCertObject(base.TestCase):
|
||||
|
||||
@mock.patch('magnum.api.utils.get_resource')
|
||||
@ -36,10 +39,10 @@ class TestCertObject(base.TestCase):
|
||||
self.assertEqual(cert_dict['pem'], cert.pem)
|
||||
|
||||
|
||||
class TestGetCertificate(api_base.FunctionalTest):
|
||||
class TestGetCaCertificate(api_base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestGetCertificate, self).setUp()
|
||||
super(TestGetCaCertificate, self).setUp()
|
||||
self.cluster = obj_utils.create_test_cluster(self.context)
|
||||
|
||||
conductor_api_patcher = mock.patch('magnum.conductor.api.API')
|
||||
@ -54,7 +57,8 @@ class TestGetCertificate(api_base.FunctionalTest):
|
||||
mock_cert.as_dict.return_value = fake_cert
|
||||
self.conductor_api.get_ca_certificate.return_value = mock_cert
|
||||
|
||||
response = self.get_json('/certificates/%s' % self.cluster.uuid)
|
||||
response = self.get_json('/certificates/%s' % self.cluster.uuid,
|
||||
headers=HEADERS)
|
||||
|
||||
self.assertEqual(self.cluster.uuid, response['cluster_uuid'])
|
||||
# check that bay is still valid as well
|
||||
@ -68,7 +72,8 @@ class TestGetCertificate(api_base.FunctionalTest):
|
||||
mock_cert.as_dict.return_value = fake_cert
|
||||
self.conductor_api.get_ca_certificate.return_value = mock_cert
|
||||
|
||||
response = self.get_json('/certificates/%s' % self.cluster.name)
|
||||
response = self.get_json('/certificates/%s' % self.cluster.name,
|
||||
headers=HEADERS)
|
||||
|
||||
self.assertEqual(self.cluster.uuid, response['cluster_uuid'])
|
||||
# check that bay is still valid as well
|
||||
@ -78,7 +83,7 @@ class TestGetCertificate(api_base.FunctionalTest):
|
||||
|
||||
def test_get_one_by_name_not_found(self):
|
||||
response = self.get_json('/certificates/not_found',
|
||||
expect_errors=True)
|
||||
expect_errors=True, headers=HEADERS)
|
||||
|
||||
self.assertEqual(404, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -91,7 +96,7 @@ class TestGetCertificate(api_base.FunctionalTest):
|
||||
uuid=uuidutils.generate_uuid())
|
||||
|
||||
response = self.get_json('/certificates/test_cluster',
|
||||
expect_errors=True)
|
||||
expect_errors=True, headers=HEADERS)
|
||||
|
||||
self.assertEqual(409, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
@ -103,7 +108,8 @@ class TestGetCertificate(api_base.FunctionalTest):
|
||||
mock_cert.as_dict.return_value = fake_cert
|
||||
self.conductor_api.get_ca_certificate.return_value = mock_cert
|
||||
|
||||
response = self.get_json('/certificates/%s' % self.cluster.uuid)
|
||||
response = self.get_json('/certificates/%s' % self.cluster.uuid,
|
||||
headers=HEADERS)
|
||||
|
||||
self.assertIn('links', response.keys())
|
||||
self.assertEqual(2, len(response['links']))
|
||||
@ -136,7 +142,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
new_cert = api_utils.cert_post_data(cluster_uuid=self.cluster.uuid)
|
||||
del new_cert['pem']
|
||||
|
||||
response = self.post_json('/certificates', new_cert)
|
||||
response = self.post_json('/certificates', new_cert, headers=HEADERS)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
self.assertEqual(new_cert['cluster_uuid'],
|
||||
@ -152,7 +158,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
new_cert['bay_uuid'] = new_cert['cluster_uuid']
|
||||
del new_cert['cluster_uuid']
|
||||
|
||||
response = self.post_json('/certificates', new_cert)
|
||||
response = self.post_json('/certificates', new_cert, headers=HEADERS)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
self.assertEqual(self.cluster.uuid, response.json['cluster_uuid'])
|
||||
@ -164,7 +170,7 @@ class TestPost(api_base.FunctionalTest):
|
||||
new_cert = api_utils.cert_post_data(cluster_uuid=self.cluster.name)
|
||||
del new_cert['pem']
|
||||
|
||||
response = self.post_json('/certificates', new_cert)
|
||||
response = self.post_json('/certificates', new_cert, headers=HEADERS)
|
||||
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertEqual(201, response.status_int)
|
||||
@ -176,13 +182,65 @@ class TestPost(api_base.FunctionalTest):
|
||||
del new_cert['pem']
|
||||
|
||||
response = self.post_json('/certificates', new_cert,
|
||||
expect_errors=True)
|
||||
expect_errors=True, headers=HEADERS)
|
||||
|
||||
self.assertEqual(400, response.status_int)
|
||||
self.assertEqual('application/json', response.content_type)
|
||||
self.assertTrue(response.json['errors'])
|
||||
|
||||
|
||||
class TestRotateCaCertificate(api_base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRotateCaCertificate, self).setUp()
|
||||
self.cluster = obj_utils.create_test_cluster(self.context)
|
||||
|
||||
conductor_api_patcher = mock.patch('magnum.conductor.api.API')
|
||||
self.conductor_api_class = conductor_api_patcher.start()
|
||||
self.conductor_api = mock.MagicMock()
|
||||
self.conductor_api_class.return_value = self.conductor_api
|
||||
self.addCleanup(conductor_api_patcher.stop)
|
||||
|
||||
def test_rotate_ca_cert(self):
|
||||
fake_cert = api_utils.cert_post_data()
|
||||
mock_cert = mock.MagicMock()
|
||||
mock_cert.as_dict.return_value = fake_cert
|
||||
self.conductor_api.rotate_ca_certificate.return_value = mock_cert
|
||||
|
||||
response = self.patch_json('/certificates/%s' % self.cluster.uuid,
|
||||
params={}, headers=HEADERS)
|
||||
|
||||
self.assertEqual(202, response.status_code)
|
||||
|
||||
|
||||
class TestRotateCaCertificateNonTls(api_base.FunctionalTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRotateCaCertificateNonTls, self).setUp()
|
||||
self.cluster_template = obj_utils.create_test_cluster_template(
|
||||
self.context, tls_disabled=True)
|
||||
self.cluster = obj_utils.create_test_cluster(self.context)
|
||||
|
||||
conductor_api_patcher = mock.patch('magnum.conductor.api.API')
|
||||
self.conductor_api_class = conductor_api_patcher.start()
|
||||
self.conductor_api = mock.MagicMock()
|
||||
self.conductor_api_class.return_value = self.conductor_api
|
||||
self.addCleanup(conductor_api_patcher.stop)
|
||||
|
||||
def test_rotate_ca_cert_non_tls(self):
|
||||
fake_cert = api_utils.cert_post_data()
|
||||
mock_cert = mock.MagicMock()
|
||||
mock_cert.as_dict.return_value = fake_cert
|
||||
self.conductor_api.rotate_ca_certificate.return_value = mock_cert
|
||||
|
||||
response = self.patch_json('/certificates/%s' % self.cluster.uuid,
|
||||
params={}, headers=HEADERS,
|
||||
expect_errors=True)
|
||||
self.assertEqual(400, response.status_code)
|
||||
self.assertIn("Rotating the CA certificate on a non-TLS cluster",
|
||||
response.json['errors'][0]['detail'])
|
||||
|
||||
|
||||
class TestCertPolicyEnforcement(api_base.FunctionalTest):
|
||||
|
||||
def _common_policy_check(self, rule, func, *arg, **kwarg):
|
||||
@ -199,11 +257,18 @@ class TestCertPolicyEnforcement(api_base.FunctionalTest):
|
||||
self._common_policy_check(
|
||||
"certificate:get", self.get_json,
|
||||
'/certificates/%s' % cluster.uuid,
|
||||
expect_errors=True)
|
||||
expect_errors=True, headers=HEADERS)
|
||||
|
||||
def test_policy_disallow_create(self):
|
||||
cluster = obj_utils.create_test_cluster(self.context)
|
||||
cert = api_utils.cert_post_data(cluster_uuid=cluster.uuid)
|
||||
self._common_policy_check(
|
||||
"certificate:create", self.post_json, '/certificates', cert,
|
||||
expect_errors=True)
|
||||
expect_errors=True, headers=HEADERS)
|
||||
|
||||
def test_policy_disallow_rotate(self):
|
||||
cluster = obj_utils.create_test_cluster(self.context)
|
||||
self._common_policy_check(
|
||||
"certificate:rotate_ca", self.patch_json,
|
||||
'/certificates/%s' % cluster.uuid, params={}, expect_errors=True,
|
||||
headers=HEADERS)
|
||||
|
@ -84,7 +84,8 @@ def create_test_cluster(context, **kw):
|
||||
"""
|
||||
cluster = get_test_cluster(context, **kw)
|
||||
create_test_cluster_template(context, uuid=cluster['cluster_template_id'],
|
||||
coe=kw.get('coe', 'swarm'))
|
||||
coe=kw.get('coe', 'swarm'),
|
||||
tls_disabled=kw.get('tls_disabled'))
|
||||
cluster.create()
|
||||
return cluster
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- Add microversion 1.5 to support rotation of a cluster's CA
|
||||
certificate. This gives admins a way to restrict/deny access to
|
||||
an existing cluster once a user has been granted access.
|
Loading…
x
Reference in New Issue
Block a user