From 78abb2e99b493544be85bf51455837303c9647e8 Mon Sep 17 00:00:00 2001 From: Dave McCowan Date: Sat, 19 Sep 2015 01:25:12 -0400 Subject: [PATCH] Adding Functional Tests and Supporting Fixes for Global Preferred CAs This commit containers the following changes to fix the global preferred certificate authority operations. - Add functional tests for global preferred CA operations - Change cas/CA_ID/unset-global-preferred to cas/unset-global-preferred - Allow all content types for set-global-preferred and unset-global-preferred - Add create_or_update_from_project_id() for PreferredCertificateAuthority table and refactor - Make reserved global project id an external project id - Use hard delete for PreferredCertificateAuthority entities Change-Id: I56fbdb0246f3159444778cd37d0080f771eb00f6 Partially-implements: blueprint add-cas APIImpact: --- barbican/api/controllers/cas.py | 65 +++++++--------- barbican/common/resources.py | 6 ++ barbican/model/models.py | 2 +- barbican/model/repositories.py | 43 ++++++----- barbican/tasks/certificate_resources.py | 13 +++- barbican/tests/api/controllers/test_cas.py | 15 ++-- ...st_repositories_certificate_authorities.py | 26 ++++++- etc/barbican/policy.json | 2 +- .../api/v1/behaviors/ca_behaviors.py | 21 ++++++ functionaltests/api/v1/functional/test_cas.py | 74 +++++++++++++++++++ functionaltests/run_tests.sh | 2 +- 11 files changed, 194 insertions(+), 75 deletions(-) diff --git a/barbican/api/controllers/cas.py b/barbican/api/controllers/cas.py index d7acf5706..70552d8eb 100644 --- a/barbican/api/controllers/cas.py +++ b/barbican/api/controllers/cas.py @@ -76,9 +76,8 @@ class CertificateAuthorityController(controllers.ACLMixin): route_table = { 'add-to-project': self.add_to_project, 'remove-from-project': self.remove_from_project, - 'set-preferred': self.set_preferred, 'set-global-preferred': self.set_global_preferred, - 'unset-global-preferred': self.unset_global_preferred, + 'set-preferred': self.set_preferred, } if name in route_table: return route_table[name] @@ -228,17 +227,11 @@ class CertificateAuthorityController(controllers.ACLMixin): if not project_ca: _requested_preferred_ca_not_a_project_ca() - preferred_ca = self.preferred_ca_repo.get_project_entities( - project_model.id) - if preferred_ca is not None: - self.preferred_ca_repo.update_preferred_ca(project_model.id, - self.ca) - else: - preferred_ca = models.PreferredCertificateAuthority( - project_model.id, self.ca.id) - self.preferred_ca_repo.create_from(preferred_ca) + self.preferred_ca_repo.create_or_update_by_project_id( + project_model.id, self.ca.id) @pecan.expose() + @utils.allow_all_content_types @controllers.handle_exceptions(u._('Set global preferred CA')) @controllers.enforce_rbac('certificate_authority:set_global_preferred') def set_global_preferred(self, external_project_id): @@ -246,31 +239,9 @@ class CertificateAuthorityController(controllers.ACLMixin): pecan.abort(405) LOG.debug("== Set global preferred CA %s", self.ca.id) - pref_ca = self.preferred_ca_repo.get_global_preferred_ca() - if pref_ca is None: - global_preferred_ca = models.PreferredCertificateAuthority( - self.preferred_ca_repo.PREFERRED_PROJECT_ID, - self.ca.id) - self.preferred_ca_repo.create_from(global_preferred_ca) - else: - self.preferred_ca_repo.update_global_preferred_ca(self.ca) - - @pecan.expose() - @controllers.handle_exceptions(u._('Unset global preferred CA')) - @controllers.enforce_rbac('certificate_authority:unset_global_preferred') - def unset_global_preferred(self, external_project_id): - if pecan.request.method != 'POST': - pecan.abort(405) - LOG.debug("== Unsetting global preferred CA") - self._remove_global_preferred_ca(external_project_id) - - def _remove_global_preferred_ca(self, external_project_id): - global_preferred_ca = self.preferred_ca_repo.get_project_entities( - self.preferred_ca_repo.PREFERRED_PROJECT_ID) - if global_preferred_ca: - self.preferred_ca_repo.delete_entity_by_id( - global_preferred_ca[0].id, - external_project_id) + project = res.get_or_create_global_preferred_project() + self.preferred_ca_repo.create_or_update_by_project_id( + project.id, self.ca.id) @index.when(method='DELETE') @utils.allow_all_content_types @@ -299,7 +270,8 @@ class CertificateAuthoritiesController(controllers.ACLMixin): route_table = { 'all': self.get_all, 'global-preferred': self.get_global_preferred, - 'preferred': self.preferred + 'preferred': self.preferred, + 'unset-global-preferred': self.unset_global_preferred, } if name in route_table: return route_table[name] @@ -437,13 +409,30 @@ class CertificateAuthoritiesController(controllers.ACLMixin): def get_global_preferred(self, external_project_id, **kw): LOG.debug('Start certificate_authorities get_global_preferred CA') - pref_ca = self.preferred_ca_repo.get_global_preferred_ca() + pref_ca = cert_resources.get_global_preferred_ca() if not pref_ca: pecan.abort(404, u._("No global preferred CA defined")) ca = self.ca_repo.get(entity_id=pref_ca.ca_id) return ca.to_dict_fields() + @pecan.expose() + @utils.allow_all_content_types + @controllers.handle_exceptions(u._('Unset global preferred CA')) + @controllers.enforce_rbac('certificate_authorities:unset_global_preferred') + def unset_global_preferred(self, external_project_id): + if pecan.request.method != 'POST': + pecan.abort(405) + LOG.debug("== Unsetting global preferred CA") + self._remove_global_preferred_ca(external_project_id) + + def _remove_global_preferred_ca(self, external_project_id): + global_preferred_ca = cert_resources.get_global_preferred_ca() + if global_preferred_ca: + self.preferred_ca_repo.delete_entity_by_id( + global_preferred_ca.id, + external_project_id) + @pecan.expose(generic=True, template='json') @utils.allow_all_content_types @controllers.handle_exceptions(u._('Retrieve project preferred CA')) diff --git a/barbican/common/resources.py b/barbican/common/resources.py index a385cf80b..4f9067dd7 100644 --- a/barbican/common/resources.py +++ b/barbican/common/resources.py @@ -23,6 +23,12 @@ from barbican.model import repositories LOG = utils.getLogger(__name__) +GLOBAL_PREFERRED_PROJECT_ID = "GLOBAL_PREFERRED" + + +def get_or_create_global_preferred_project(): + return get_or_create_project(GLOBAL_PREFERRED_PROJECT_ID) + def get_or_create_project(project_id): """Returns project with matching project_id. diff --git a/barbican/model/models.py b/barbican/model/models.py index 2813c8add..d65f267e4 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -982,7 +982,7 @@ class ProjectCertificateAuthority(BASE, SoftDeleteMixIn, ModelBase): 'ca_id': self.ca_id} -class PreferredCertificateAuthority(BASE, SoftDeleteMixIn, ModelBase): +class PreferredCertificateAuthority(BASE, ModelBase): """Stores preferred CAs for any project. Admins can define a set of CAs available for issuance requests for diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index ae492dfdf..d4aec93e6 100755 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -1629,9 +1629,11 @@ class ProjectCertificateAuthorityRepo(BaseRepo): class PreferredCertificateAuthorityRepo(BaseRepo): - """Repository for the PreferredCertificateAuthority entity.""" + """Repository for the PreferredCertificateAuthority entity. - PREFERRED_PROJECT_ID = "0" + PreferredCertificateAuthority entries are not soft delete. So there is no + need to have deleted=False filter in queries. + """ def get_by_create_date(self, offset_arg=None, limit_arg=None, project_id=None, ca_id=None, @@ -1648,7 +1650,6 @@ class PreferredCertificateAuthorityRepo(BaseRepo): query = session.query(models.PreferredCertificateAuthority) query = query.order_by(models.PreferredCertificateAuthority.created_at) - query = query.filter_by(deleted=False) if project_id: query = query.filter( @@ -1672,22 +1673,26 @@ class PreferredCertificateAuthorityRepo(BaseRepo): return entities, offset, limit, total - def get_global_preferred_ca(self): - pref_cas = self.get_project_entities(self.PREFERRED_PROJECT_ID) - if len(pref_cas) > 0: - return pref_cas[0] - return None + def create_or_update_by_project_id(self, project_id, ca_id, session=None): + """Create or update preferred CA for a project by project_id. - def update_global_preferred_ca(self, new_ca): - self.update_preferred_ca(self.PREFERRED_PROJECT_ID, new_ca) - - def update_preferred_ca(self, project_id, new_ca): - session = self.get_session() - query = session.query(models.PreferredCertificateAuthority).filter_by( - project_id=project_id) - entity = query.one() - entity.ca_id = new_ca.id - entity.save() + :param project_id: ID of project whose preferred CA will be saved + :param ca_id: ID of preferred CA + :param session: SQLAlchemy session object. + :return: None + """ + session = self.get_session(session) + query = session.query(models.PreferredCertificateAuthority) + query = query.filter_by(project_id=project_id) + try: + entity = query.one() + except sa_orm.exc.NoResultFound: + self.create_from( + models.PreferredCertificateAuthority(project_id, ca_id), + session=session) + else: + entity.ca_id = ca_id + entity.save(session) def _do_entity_name(self): """Sub-class hook: return entity name, such as for debugging.""" @@ -1709,7 +1714,7 @@ class PreferredCertificateAuthorityRepo(BaseRepo): :param session: existing db session reference. """ return session.query(models.PreferredCertificateAuthority).filter_by( - project_id=project_id).filter_by(deleted=False) + project_id=project_id) class SecretACLRepo(BaseRepo): diff --git a/barbican/tasks/certificate_resources.py b/barbican/tasks/certificate_resources.py index 5472afc08..1e59eea15 100644 --- a/barbican/tasks/certificate_resources.py +++ b/barbican/tasks/certificate_resources.py @@ -18,6 +18,7 @@ from OpenSSL import crypto from barbican.common import exception as excep from barbican.common import hrefs +from barbican.common import resources as res import barbican.common.utils as utils from barbican.model import models from barbican.model import repositories as repos @@ -315,6 +316,16 @@ def modify_certificate_request(order_model, updated_meta): raise NotImplementedError # pragma: no cover +def get_global_preferred_ca(): + project = res.get_or_create_global_preferred_project() + preferred_ca_repository = repos.get_preferred_ca_repository() + cas = preferred_ca_repository.get_project_entities(project.id) + if not cas: + return None + else: + return cas[0] + + def _get_ca_id(order_meta, project_id): ca_id = order_meta.get(cert.CA_ID) if ca_id: @@ -326,7 +337,7 @@ def _get_ca_id(order_meta, project_id): if total > 0: return cas[0].ca_id - global_ca = preferred_ca_repository.get_global_preferred_ca() + global_ca = get_global_preferred_ca() if global_ca: return global_ca.ca_id diff --git a/barbican/tests/api/controllers/test_cas.py b/barbican/tests/api/controllers/test_cas.py index 91d0f4010..73d5b5532 100644 --- a/barbican/tests/api/controllers/test_cas.py +++ b/barbican/tests/api/controllers/test_cas.py @@ -352,23 +352,17 @@ class WhenTestingCAsResource(utils.BarbicanAPIBaseTestCase): def test_should_unset_global_preferred(self): self.create_cas() - resp = self.app.post('/cas/{0}/unset-global-preferred'.format( - self.global_preferred_ca.id)) + resp = self.app.post( + '/cas/unset-global-preferred') self.assertEqual(204, resp.status_int) def test_should_unset_global_preferred_not_post(self): self.create_cas() resp = self.app.get( - '/cas/{0}/unset-global-preferred'.format(self.selected_ca_id), + '/cas/unset-global-preferred', expect_errors=True) self.assertEqual(405, resp.status_int) - def test_should_raise_unset_global_preferred_ca_not_found(self): - resp = self.app.post( - '/cas/bogus_ca/unset-global-preferred', - expect_errors=True) - self.assertEqual(404, resp.status_int) - def test_should_get_projects(self): self.create_cas() resp = self.app.get( @@ -456,6 +450,7 @@ class WhenTestingCAsResource(utils.BarbicanAPIBaseTestCase): def create_cas(self, set_project_cas=True): self.project = res.get_or_create_project(self.project_id) + self.global_project = res.get_or_create_global_preferred_project() project_repo.save(self.project) self.project_ca_ids = [] @@ -505,7 +500,7 @@ class WhenTestingCAsResource(utils.BarbicanAPIBaseTestCase): if ca_id == 1: # set global preferred ca pref_ca = models.PreferredCertificateAuthority( - preferred_ca_repo.PREFERRED_PROJECT_ID, + self.global_project.id, ca.id) preferred_ca_repo.create_from(pref_ca) preferred_ca_repo.save(pref_ca) diff --git a/barbican/tests/model/repositories/test_repositories_certificate_authorities.py b/barbican/tests/model/repositories/test_repositories_certificate_authorities.py index 565cf54d8..597b2f25b 100644 --- a/barbican/tests/model/repositories/test_repositories_certificate_authorities.py +++ b/barbican/tests/model/repositories/test_repositories_certificate_authorities.py @@ -13,6 +13,7 @@ import datetime from barbican.common import exception +from barbican.common import resources as res from barbican.model import models from barbican.model import repositories from barbican.tests import database_utils @@ -334,6 +335,8 @@ class WhenTestingPreferredCARepo(database_utils.RepositoryTestCase): 'ca_signing_certificate': 'XXXXX-updated-XXXXX', 'intermediates': 'YYYYY'} + self.global_project = res.get_or_create_global_preferred_project() + def _add_ca(self, parsed_ca, session): ca = self.ca_repo.create_from(models.CertificateAuthority(parsed_ca), session=session) @@ -354,7 +357,7 @@ class WhenTestingPreferredCARepo(database_utils.RepositoryTestCase): def _add_global_preferred_ca(self, ca_id, session): preferred_ca = self.preferred_ca_repo.create_from( models.PreferredCertificateAuthority( - self.preferred_ca_repo.PREFERRED_PROJECT_ID, + self.global_project.id, ca_id), session) return preferred_ca @@ -460,13 +463,28 @@ class WhenTestingPreferredCARepo(database_utils.RepositoryTestCase): session.commit() pca = self.preferred_ca_repo.get_project_entities( - self.preferred_ca_repo.PREFERRED_PROJECT_ID, + self.global_project.id, session) self.assertEqual([ca.id], [s.ca_id for s in pca]) - def test_should_update(self): + def test_should_create(self): session = self.ca_repo.get_session() ca = self._add_ca(self.parsed_ca, session) + project = self._add_project("project_1", session) + + self.preferred_ca_repo.create_or_update_by_project_id( + project.id, ca.id) session.commit() - self.ca_repo.update_entity(ca, self.parsed_modified_ca, session) + def test_should_update(self): + session = self.ca_repo.get_session() + ca1 = self._add_ca(self.parsed_ca, session) + ca2 = self._add_ca(self.parsed_ca2, session) + project = self._add_project("project_1", session) + + self.preferred_ca_repo.create_or_update_by_project_id( + project.id, ca1.id) + session.commit() + self.preferred_ca_repo.create_or_update_by_project_id( + project.id, ca2.id) + session.commit() diff --git a/etc/barbican/policy.json b/etc/barbican/policy.json index 89df153fe..ca89acf7e 100644 --- a/etc/barbican/policy.json +++ b/etc/barbican/policy.json @@ -55,6 +55,7 @@ "certificate_authorities:post": "rule:admin", "certificate_authorities:get_preferred_ca": "rule:all_users", "certificate_authorities:get_global_preferred_ca": "rule:service_admin", + "certificate_authorities:unset_global_preferred": "rule:service_admin", "certificate_authority:delete": "rule:admin", "certificate_authority:get": "rule:all_users", "certificate_authority:get_cacert": "rule:all_users", @@ -64,7 +65,6 @@ "certificate_authority:remove_from_project": "rule:admin", "certificate_authority:set_preferred": "rule:admin", "certificate_authority:set_global_preferred": "rule:service_admin", - "certificate_authority:unset_global_preferred": "rule:service_admin", "secret_acls:put_patch": "rule:secret_project_admin or rule:secret_project_creator", "secret_acls:delete": "rule:secret_project_admin or rule:secret_project_creator", "secret_acls:get": "rule:all_but_audit and rule:secret_project_match", diff --git a/functionaltests/api/v1/behaviors/ca_behaviors.py b/functionaltests/api/v1/behaviors/ca_behaviors.py index c222fd92f..964f556e8 100644 --- a/functionaltests/api/v1/behaviors/ca_behaviors.py +++ b/functionaltests/api/v1/behaviors/ca_behaviors.py @@ -154,3 +154,24 @@ class CABehaviors(base_behaviors.BaseBehaviors): response_model_type=ca_models.CAModel, extra_headers=extra_headers, use_auth=use_auth, user_name=user_name) + + def set_global_preferred(self, ca_ref, headers=None, + use_auth=True, user_name=None): + resp = self.client.post(ca_ref + '/set-global-preferred', + extra_headers=headers, use_auth=use_auth, + user_name=user_name) + return resp + + def unset_global_preferred(self, headers=None, + use_auth=True, user_name=None): + resp = self.client.post('cas/unset-global-preferred', + extra_headers=headers, + use_auth=use_auth, user_name=user_name) + return resp + + def get_global_preferred(self, extra_headers=None, + use_auth=True, user_name=None): + return self.client.get('cas/global-preferred', + response_model_type=ca_models.CAModel, + extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) diff --git a/functionaltests/api/v1/functional/test_cas.py b/functionaltests/api/v1/functional/test_cas.py index da0b16e4f..b33354c12 100644 --- a/functionaltests/api/v1/functional/test_cas.py +++ b/functionaltests/api/v1/functional/test_cas.py @@ -34,6 +34,7 @@ CONF = config.get_config() admin_a = CONF.rbac_users.admin_a creator_a = CONF.rbac_users.creator_a +service_admin = CONF.identity.service_admin order_simple_cmc_request_data = { 'type': 'certificate', @@ -321,3 +322,76 @@ class ProjectCATestCase(CATestCommon): # before) (resp, cas, final_total, _, __) = self.ca_behaviors.get_cas() self.assertEqual(initial_total, final_total) + + +class GlobalPreferredCATestCase(CATestCommon): + + def setUp(self): + super(GlobalPreferredCATestCase, self).setUp() + (_, self.cas, self.num_cas, _, _) = self.ca_behaviors.get_cas() + self.ca_ids = [hrefs.get_ca_id_from_ref(ref) for ref in self.cas] + + def tearDown(self): + self.ca_behaviors.unset_global_preferred(user_name=service_admin) + super(CATestCommon, self).tearDown() + + def test_global_preferred_no_project_admin_access(self): + resp = self.ca_behaviors.get_global_preferred() + self.assertEqual(403, resp.status_code) + resp = self.ca_behaviors.set_global_preferred(ca_ref=self.cas[1]) + self.assertEqual(403, resp.status_code) + resp = self.ca_behaviors.unset_global_preferred() + self.assertEqual(403, resp.status_code) + + def test_global_preferred_update(self): + if self.num_cas < 2: + self.skipTest("At least two CAs are required for this test") + resp = self.ca_behaviors.set_global_preferred( + ca_ref=self.cas[0], user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.ca_behaviors.get_global_preferred(user_name=service_admin) + self.assertEqual(200, resp.status_code) + self.assertEqual(self.ca_ids[0], resp.model.ca_id) + + resp = self.ca_behaviors.set_global_preferred( + ca_ref=self.cas[1], user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.ca_behaviors.get_global_preferred(user_name=service_admin) + self.assertEqual(200, resp.status_code) + self.assertEqual(self.ca_ids[1], resp.model.ca_id) + + def test_global_preferred_set_and_unset(self): + resp = self.ca_behaviors.unset_global_preferred( + user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.ca_behaviors.get_global_preferred(user_name=service_admin) + self.assertEqual(404, resp.status_code) + + resp = self.ca_behaviors.set_global_preferred( + ca_ref=self.cas[0], user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.ca_behaviors.get_global_preferred(user_name=service_admin) + self.assertEqual(200, resp.status_code) + self.assertEqual(self.ca_ids[0], resp.model.ca_id) + + resp = self.ca_behaviors.unset_global_preferred( + user_name=service_admin) + self.assertEqual(204, resp.status_code) + resp = self.ca_behaviors.get_global_preferred(user_name=service_admin) + self.assertEqual(404, resp.status_code) + + @testtools.skip("Skip test until ca behaviors tracks project cas") + def test_global_preferred_affects_project_preferred(self): + if self.num_cas < 2: + self.skipTest("At least two CAs are required for this test") + resp = self.ca_behaviors.get_preferred(user_name=admin_a) + self.assertEqual(200, resp.status_code) + self.assertEqual(self.ca_ids[0], resp.model.ca_id) + + resp = self.ca_behaviors.set_global_preferred( + ca_ref=self.cas[1], user_name=service_admin) + self.assertEqual(204, resp.status_code) + + resp = self.ca_behaviors.get_preferred(user_name=admin_a) + self.assertEqual(200, resp.status_code) + self.assertEqual(self.ca_ids[1], resp.model.ca_id) diff --git a/functionaltests/run_tests.sh b/functionaltests/run_tests.sh index e11a4d697..e93cd1c78 100755 --- a/functionaltests/run_tests.sh +++ b/functionaltests/run_tests.sh @@ -29,7 +29,7 @@ retval=$? testr slowest # run the tests in parallel -SKIP=^\(\?\!\.\*\(ProjectQuotasPagingTestCase\|QuotaEnforcementTestCase\|ListingCAsTestCase\|ProjectCATestCase\)\) +SKIP=^\(\?\!\.\*\(ProjectQuotasPagingTestCase\|QuotaEnforcementTestCase\|ListingCAsTestCase\|ProjectCATestCase\|GlobalPreferredCATestCase\)\) testr init testr run $SKIP --parallel --subunit | subunit-trace --no-failure-debug -f retval=$?