From b6f22571181b3ba5ae4c4af1464d5b44633fcbba Mon Sep 17 00:00:00 2001 From: Nguyen Hung Phuong Date: Thu, 18 Aug 2016 11:50:58 +0700 Subject: [PATCH 01/20] Clean imports in code In some part in the code we import objects. In the Openstack style guidelines they recommend to import only modules. http://docs.openstack.org/developer/hacking/#imports Change-Id: Ibd3464b52fd70bbfe77ce35cdffbbef95de24b12 --- functionaltests/api/v1/models/acl_models.py | 4 ++-- functionaltests/api/v1/models/ca_models.py | 4 ++-- functionaltests/api/v1/models/consumer_model.py | 4 ++-- functionaltests/api/v1/models/container_models.py | 6 +++--- functionaltests/api/v1/models/quota_models.py | 14 +++++++------- functionaltests/api/v1/models/secret_models.py | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/functionaltests/api/v1/models/acl_models.py b/functionaltests/api/v1/models/acl_models.py index 561724cb..73dd97b2 100644 --- a/functionaltests/api/v1/models/acl_models.py +++ b/functionaltests/api/v1/models/acl_models.py @@ -13,10 +13,10 @@ 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. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class AclModel(BaseModel): +class AclModel(base_models.BaseModel): def __init__(self, acl_ref=None, read=None): super(AclModel, self).__init__() diff --git a/functionaltests/api/v1/models/ca_models.py b/functionaltests/api/v1/models/ca_models.py index 7b44516a..cedef256 100644 --- a/functionaltests/api/v1/models/ca_models.py +++ b/functionaltests/api/v1/models/ca_models.py @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class CAModel(BaseModel): +class CAModel(base_models.BaseModel): def __init__(self, expiration=None, ca_id=None, ca_ref=None, status=None, updated=None, created=None, plugin_name=None, diff --git a/functionaltests/api/v1/models/consumer_model.py b/functionaltests/api/v1/models/consumer_model.py index ae7c5f3e..96dd1a88 100644 --- a/functionaltests/api/v1/models/consumer_model.py +++ b/functionaltests/api/v1/models/consumer_model.py @@ -13,10 +13,10 @@ 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. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class ConsumerModel(BaseModel): +class ConsumerModel(base_models.BaseModel): def __init__(self, name=None, URL=None, created=None, updated=None, status=None): diff --git a/functionaltests/api/v1/models/container_models.py b/functionaltests/api/v1/models/container_models.py index 2085d460..ab1dac69 100644 --- a/functionaltests/api/v1/models/container_models.py +++ b/functionaltests/api/v1/models/container_models.py @@ -13,17 +13,17 @@ 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. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class SecretRefModel(BaseModel): +class SecretRefModel(base_models.BaseModel): def __init__(self, name=None, secret_ref=None): self.name = name self.secret_ref = secret_ref -class ContainerModel(BaseModel): +class ContainerModel(base_models.BaseModel): def __init__(self, name=None, type=None, secret_refs=[], container_ref=None, consumers=None, status=None, diff --git a/functionaltests/api/v1/models/quota_models.py b/functionaltests/api/v1/models/quota_models.py index 64cd76ca..91cf31c4 100644 --- a/functionaltests/api/v1/models/quota_models.py +++ b/functionaltests/api/v1/models/quota_models.py @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class QuotasModel(BaseModel): +class QuotasModel(base_models.BaseModel): def __init__(self, secrets=None, orders=None, containers=None, consumers=None, cas=None): @@ -29,7 +29,7 @@ class QuotasModel(BaseModel): self.cas = cas -class QuotasResponseModel(BaseModel): +class QuotasResponseModel(base_models.BaseModel): def __init__(self, quotas=None): super(QuotasResponseModel, self).__init__() @@ -41,7 +41,7 @@ class QuotasResponseModel(BaseModel): return cls(quotas=quotas) -class ProjectQuotaRequestModel(BaseModel): +class ProjectQuotaRequestModel(base_models.BaseModel): def __init__(self, project_quotas=None): super(ProjectQuotaRequestModel, self).__init__() @@ -53,14 +53,14 @@ class ProjectQuotaRequestModel(BaseModel): return cls(project_quotas=project_quotas) -class ProjectQuotaOneModel(BaseModel): +class ProjectQuotaOneModel(base_models.BaseModel): def __init__(self, project_quotas=None): super(ProjectQuotaOneModel, self).__init__() self.project_quotas = QuotasModel(**project_quotas) -class ProjectQuotaListItemModel(BaseModel): +class ProjectQuotaListItemModel(base_models.BaseModel): def __init__(self, project_id=None, project_quotas=None): super(ProjectQuotaListItemModel, self).__init__() @@ -68,7 +68,7 @@ class ProjectQuotaListItemModel(BaseModel): self.project_quotas = QuotasModel(**project_quotas) -class ProjectQuotaListModel(BaseModel): +class ProjectQuotaListModel(base_models.BaseModel): def __init__(self, project_quotas=None): super(ProjectQuotaListModel, self).__init__() diff --git a/functionaltests/api/v1/models/secret_models.py b/functionaltests/api/v1/models/secret_models.py index 9337e8a5..d1f83700 100644 --- a/functionaltests/api/v1/models/secret_models.py +++ b/functionaltests/api/v1/models/secret_models.py @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. """ -from functionaltests.api.v1.models.base_models import BaseModel +from functionaltests.api.v1.models import base_models -class SecretModel(BaseModel): +class SecretModel(base_models.BaseModel): def __init__(self, name=None, expiration=None, algorithm=None, secret_ref=None, bit_length=None, mode=None, secret_type=None, From 14df7412d2022539482e438d4e8ded982e3c9147 Mon Sep 17 00:00:00 2001 From: liujiong Date: Thu, 18 Aug 2016 23:08:36 +0800 Subject: [PATCH 02/20] Fix test suite cleanup Some test suites did not cleanup all created objects Change-Id: I4237d92817267e07fc9f56c4f3cff1ace6a1978b --- functionaltests/api/v1/functional/test_acls.py | 1 + functionaltests/api/v1/functional/test_cas.py | 2 ++ functionaltests/api/v1/functional/test_certificate_orders.py | 3 +++ functionaltests/api/v1/functional/test_consumers.py | 1 + functionaltests/api/v1/functional/test_orders.py | 4 ++++ functionaltests/api/v1/smoke/test_consumers.py | 1 + 6 files changed, 12 insertions(+) diff --git a/functionaltests/api/v1/functional/test_acls.py b/functionaltests/api/v1/functional/test_acls.py index 41b99365..fd8e3a04 100644 --- a/functionaltests/api/v1/functional/test_acls.py +++ b/functionaltests/api/v1/functional/test_acls.py @@ -169,6 +169,7 @@ class AclTestCase(base.TestCase): self.acl_behaviors.delete_all_created_acls() self.secret_behaviors.delete_all_created_secrets() self.container_behaviors.delete_all_created_containers() + self.consumer_behaviors.delete_all_created_consumers() super(AclTestCase, self).tearDown() @utils.parameterized_dataset(test_data_read_secret_rbac_only) diff --git a/functionaltests/api/v1/functional/test_cas.py b/functionaltests/api/v1/functional/test_cas.py index c19e2024..9ef64560 100644 --- a/functionaltests/api/v1/functional/test_cas.py +++ b/functionaltests/api/v1/functional/test_cas.py @@ -112,6 +112,8 @@ class CATestCommon(base.TestCase): def tearDown(self): self.order_behaviors.delete_all_created_orders() self.ca_behaviors.delete_all_created_cas() + self.container_behaviors.delete_all_created_containers() + self.secret_behaviors.delete_all_created_secrets() super(CATestCommon, self).tearDown() def send_test_order(self, ca_ref=None, user_name=None, diff --git a/functionaltests/api/v1/functional/test_certificate_orders.py b/functionaltests/api/v1/functional/test_certificate_orders.py index 1cdbdc02..c4763ffe 100644 --- a/functionaltests/api/v1/functional/test_certificate_orders.py +++ b/functionaltests/api/v1/functional/test_certificate_orders.py @@ -155,6 +155,9 @@ class CertificatesTestCase(base.TestCase): def tearDown(self): self.behaviors.delete_all_created_orders() + self.ca_behaviors.delete_all_created_cas() + self.container_behaviors.delete_all_created_containers() + self.secret_behaviors.delete_all_created_secrets() super(CertificatesTestCase, self).tearDown() def wait_for_order( diff --git a/functionaltests/api/v1/functional/test_consumers.py b/functionaltests/api/v1/functional/test_consumers.py index c40f84c2..0d8ee816 100644 --- a/functionaltests/api/v1/functional/test_consumers.py +++ b/functionaltests/api/v1/functional/test_consumers.py @@ -74,6 +74,7 @@ class ConsumersBaseTestCase(base.TestCase): def tearDown(self): self.secret_behaviors.delete_all_created_secrets() self.container_behaviors.delete_all_created_containers() + self.consumer_behaviors.delete_all_created_consumers() super(ConsumersBaseTestCase, self).tearDown() def _create_a_secret(self): diff --git a/functionaltests/api/v1/functional/test_orders.py b/functionaltests/api/v1/functional/test_orders.py index 25dcc292..d16044e6 100644 --- a/functionaltests/api/v1/functional/test_orders.py +++ b/functionaltests/api/v1/functional/test_orders.py @@ -89,6 +89,8 @@ class OrdersTestCase(base.TestCase): def tearDown(self): self.behaviors.delete_all_created_orders() + self.container_behaviors.delete_all_created_containers() + self.secret_behaviors.delete_all_created_secrets() super(OrdersTestCase, self).tearDown() def wait_for_order(self, order_resp, order_ref): @@ -654,6 +656,8 @@ class OrdersUnauthedTestCase(base.TestCase): def tearDown(self): self.behaviors.delete_all_created_orders() + self.container_behaviors.delete_all_created_containers() + self.secret_behaviors.delete_all_created_secrets() super(OrdersUnauthedTestCase, self).tearDown() @testcase.attr('negative', 'security') diff --git a/functionaltests/api/v1/smoke/test_consumers.py b/functionaltests/api/v1/smoke/test_consumers.py index 8c73af65..6c0d7b82 100644 --- a/functionaltests/api/v1/smoke/test_consumers.py +++ b/functionaltests/api/v1/smoke/test_consumers.py @@ -98,6 +98,7 @@ class ConsumersTestCase(base.TestCase): def tearDown(self): self.secret_behaviors.delete_all_created_secrets() self.container_behaviors.delete_all_created_containers() + self.consumer_behaviors.delete_all_created_consumers() super(ConsumersTestCase, self).tearDown() @testcase.attr('positive') From 7a80895381db41e3381013570ddc9e3594c2fd71 Mon Sep 17 00:00:00 2001 From: Tony Breeds Date: Mon, 22 Aug 2016 15:33:49 +1000 Subject: [PATCH 03/20] Support upper-constratints.txt in tox environments Change-Id: I1af65c599f139304878c0de2d01808c7f95a1a41 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index be578bf3..2c59bc89 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] +minversion = 2.0 envlist = pep8,py35,py34,py27,docs [testenv] -install_command = pip install -U {opts} {packages} +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -U {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt From 043e8e28eb39ac1a402f4ecff2f06850e07a3251 Mon Sep 17 00:00:00 2001 From: gecong1973 Date: Tue, 23 Aug 2016 16:05:34 +0800 Subject: [PATCH 04/20] Remove white space between print and () TrivialFix Change-Id: Id2976410c96dfee35baaa6b1e7ea0f382c7f5cba --- barbican/cmd/barbican_manage.py | 6 +++--- barbican/cmd/pkcs11_key_generation.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/barbican/cmd/barbican_manage.py b/barbican/cmd/barbican_manage.py index 2972e37e..154ace20 100644 --- a/barbican/cmd/barbican_manage.py +++ b/barbican/cmd/barbican_manage.py @@ -170,7 +170,7 @@ class HSMCommands(object): self.pkcs11.generate_key(int(length), self.session, str(label), encrypt=True, wrap=True, master_key=True) self.pkcs11.return_session(self.session) - print ("MKEK successfully generated!") + print("MKEK successfully generated!") gen_hmac_description = "Generates a new HMAC key" @@ -193,7 +193,7 @@ class HSMCommands(object): self.pkcs11.generate_key(int(length), self.session, str(label), sign=True, master_key=True) self.pkcs11.return_session(self.session) - print ("HMAC successfully generated!") + print("HMAC successfully generated!") rewrap_pkek_description = "Re-wrap project MKEKs" @@ -214,7 +214,7 @@ class HSMCommands(object): def _verify_label_does_not_exist(self, label, session): key_handle = self.pkcs11.get_key_handle(label, session) if key_handle: - print ( + print( "The label {label} already exists! " "Please try again.".format(label=label) ) diff --git a/barbican/cmd/pkcs11_key_generation.py b/barbican/cmd/pkcs11_key_generation.py index 7877d1e5..ae96a5a7 100644 --- a/barbican/cmd/pkcs11_key_generation.py +++ b/barbican/cmd/pkcs11_key_generation.py @@ -86,7 +86,7 @@ class KeyGenerator(object): def verify_label_does_not_exist(self, label, session): key_handle = self.pkcs11.get_key_handle(label, session) if key_handle: - print ( + print( "The label {label} already exists! " "Please try again.".format(label=label) ) @@ -97,7 +97,7 @@ class KeyGenerator(object): self.verify_label_does_not_exist(args.label, self.session) self.pkcs11.generate_key(int(args.length), self.session, args.label, encrypt=True, wrap=True, master_key=True) - print ("MKEK successfully generated!") + print("MKEK successfully generated!") def generate_hmac(self, args): """Process the generate HMAC with given arguments""" @@ -105,7 +105,7 @@ class KeyGenerator(object): self.pkcs11.generate_key(int(args.length), self.session, args.label, sign=True, master_key=True) - print ("HMAC successfully generated!") + print("HMAC successfully generated!") def execute(self): """Parse the command line arguments.""" From 78b0a41de3723be92e31a702717b65f9cda59429 Mon Sep 17 00:00:00 2001 From: zhangyanxian Date: Thu, 25 Aug 2016 07:23:49 +0000 Subject: [PATCH 05/20] Some minor code optimization in post_test_hook.sh set -ex is helpful for debugging test progress and make test quit promptly once exception happens Change-Id: I0b507fa5fd85d8bcff2b3b682854020d0075d6f9 --- functionaltests/post_test_hook.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/functionaltests/post_test_hook.sh b/functionaltests/post_test_hook.sh index 472a3c3a..48c9be18 100755 --- a/functionaltests/post_test_hook.sh +++ b/functionaltests/post_test_hook.sh @@ -15,6 +15,9 @@ # This script is executed inside post_test_hook function in devstack gate. # Install packages from test-requirements.txt + +set -ex + sudo pip install -r /opt/stack/new/barbican/test-requirements.txt cd /opt/stack/new/barbican/functionaltests From f44879a69e4dd3e5bf353819f0b6c02e6d38eba3 Mon Sep 17 00:00:00 2001 From: liujiong Date: Sat, 27 Aug 2016 22:48:15 +0800 Subject: [PATCH 06/20] Fix typo in barbican/tests/keys.py Change-Id: Ic0d383cb885565683f0f39211559e86fd25adeef --- barbican/tests/keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/barbican/tests/keys.py b/barbican/tests/keys.py index 454d98d1..43d1337f 100644 --- a/barbican/tests/keys.py +++ b/barbican/tests/keys.py @@ -15,7 +15,7 @@ def get_private_key_pem(): - """Returns a private key in PCKS#8 format + """Returns a private key in PKCS#8 format This key was created by issuing the following openssl commands: From 20ffc77b60439984fc54bb75e05ce1d7ed164568 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Date: Sun, 28 Aug 2016 06:17:05 +0000 Subject: [PATCH 07/20] Add Barbican Verification to Install Guide Adds the verification of operation for the Barbican Key Manager Service to the install-guide. Change-Id: Ie4723acdee590fc61a52a352ac57a50cf71534ce --- install-guide/source/index.rst | 4 ++- install-guide/source/verify.rst | 61 +++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/install-guide/source/index.rst b/install-guide/source/index.rst index 722e7d1c..b4b74561 100644 --- a/install-guide/source/index.rst +++ b/install-guide/source/index.rst @@ -10,7 +10,9 @@ Key Manager service verify.rst next-steps.rst -The Key Manager service (barbican) provides... +The Key Manager service (barbican) provides secure storage, provisioning and +management of secret data. This includes keying material such as symmetric +keys, asymmetric keys, certificates and raw binary data. This chapter assumes a working setup of OpenStack following the `OpenStack Installation Tutorial `_. diff --git a/install-guide/source/verify.rst b/install-guide/source/verify.rst index 7a8b2df1..a7ed6191 100644 --- a/install-guide/source/verify.rst +++ b/install-guide/source/verify.rst @@ -3,22 +3,71 @@ Verify operation ~~~~~~~~~~~~~~~~ -Verify operation of the Key Manager service. +Verify operation of the Key Manager (barbican) service. .. note:: Perform these commands on the controller node. -#. Source the ``admin`` project credentials to gain access to - admin-only CLI commands: +#. Source the ``admin`` credentials to be able to perform Barbican + API calls: .. code-block:: console $ . admin-openrc -#. List service components to verify successful launch and registration - of each process: +#. Use the OpenStack CLI to store a secret: .. code-block:: console - $ openstack key manager service list + $ openstack secret store --name mysecret --payload j4=]d21 + +---------------+-----------------------------------------------------------------------+ + | Field | Value | + +---------------+-----------------------------------------------------------------------+ + | Secret href | http://10.0.2.15:9311/v1/secrets/655d7d30-c11a-49d9-a0f1-34cdf53a36fa | + | Name | mysecret | + | Created | None | + | Status | None | + | Content types | None | + | Algorithm | aes | + | Bit length | 256 | + | Secret type | opaque | + | Mode | cbc | + | Expiration | None | + +---------------+-----------------------------------------------------------------------+ + +#. Confirm that the secret was stored by retrieving it: + + .. code-block:: console + + $ openstack secret get http://10.0.2.15:9311/v1/secrets/655d7d30-c11a-49d9-a0f1-34cdf53a36fa + +---------------+-----------------------------------------------------------------------+ + | Field | Value | + +---------------+-----------------------------------------------------------------------+ + | Secret href | http://10.0.2.15:9311/v1/secrets/655d7d30-c11a-49d9-a0f1-34cdf53a36fa | + | Name | mysecret | + | Created | 2016-08-16 16:04:10+00:00 | + | Status | ACTIVE | + | Content types | {u'default': u'application/octet-stream'} | + | Algorithm | aes | + | Bit length | 256 | + | Secret type | opaque | + | Mode | cbc | + | Expiration | None | + +---------------+-----------------------------------------------------------------------+ + + .. note:: + + Some items are populated after the secret has been created and will only + display when retrieving it. + +#. Confirm that the secret payload was stored by retrieving it: + + .. code-block:: console + + $ openstack secret get http://10.0.2.15:9311/v1/secrets/655d7d30-c11a-49d9-a0f1-34cdf53a36fa --payload + +---------+---------+ + | Field | Value | + +---------+---------+ + | Payload | j4=]d21 | + +---------+---------+ From a8d8981d7b0d8eb43baaf76c49282e4ce2aa7a80 Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Fri, 2 Sep 2016 14:24:50 -0700 Subject: [PATCH 08/20] Assigning unwrapped kek handle to new variable to avoid overwrite Change-Id: Ib654c042cd8f62e0589881682ddaf410d406d1db Closes-Bug: #1619794 --- barbican/cmd/pkcs11_kek_rewrap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/barbican/cmd/pkcs11_kek_rewrap.py b/barbican/cmd/pkcs11_kek_rewrap.py index 47ec707c..81744d95 100644 --- a/barbican/cmd/pkcs11_kek_rewrap.py +++ b/barbican/cmd/pkcs11_kek_rewrap.py @@ -79,16 +79,18 @@ class KekRewrap(object): kek_data = iv + wrapped_key self.pkcs11.verify_hmac(kek_mkhk, hmac, kek_data, session) # Unwrap KEK - kek = self.pkcs11.unwrap_key(kek_mkek, iv, wrapped_key, session) + current_kek = self.pkcs11.unwrap_key(kek_mkek, iv, wrapped_key, + session) # Wrap KEK with new master keys - new_kek = self.pkcs11.wrap_key(self.new_mkek, kek, session) + new_kek = self.pkcs11.wrap_key(self.new_mkek, current_kek, + session) # Compute HMAC for rewrapped KEK new_kek_data = new_kek['iv'] + new_kek['wrapped_key'] new_hmac = self.pkcs11.compute_hmac(self.new_mkhk, new_kek_data, session) # Destroy unwrapped KEK - self.pkcs11.destroy_object(kek, session) + self.pkcs11.destroy_object(current_kek, session) # Build updated meta dict updated_meta = meta_dict.copy() From 38ecf5b51fef1293e9c1d95d8110c50ae5997f28 Mon Sep 17 00:00:00 2001 From: Pan Date: Thu, 25 Aug 2016 12:56:07 -0400 Subject: [PATCH 09/20] Remove consumer check for project_id to match containers I believe this is the correct behavior, as it would match how containers handles these operations. This change facilitates the LBaaS Barbican TLS workflow (which should be the same as what other services will use in the future too). The RBAC settings for consumer POST should be set to use the same ACL rules as container GET (plus admin). The RBAC settings for consumer DELETE should be: * Any user with Delete permissions on the Container * Any user that both: has ACL Read access to the Container; is a member of the project that created the Consumer being deleted Change-Id: Ie84784573893934c2887814a200e7386314b4f18 Closes-Bug: #1519170 --- barbican/api/controllers/consumers.py | 50 +++++----- barbican/tests/api/test_resources.py | 95 +++++++++++++++---- etc/barbican/policy.json | 8 +- .../api/v1/functional/test_acls.py | 8 +- 4 files changed, 111 insertions(+), 50 deletions(-) diff --git a/barbican/api/controllers/consumers.py b/barbican/api/controllers/consumers.py index 887529a1..c8032fec 100644 --- a/barbican/api/controllers/consumers.py +++ b/barbican/api/controllers/consumers.py @@ -33,6 +33,12 @@ def _consumer_not_found(): 'another castle.')) +def _consumer_ownership_mismatch(): + """Throw exception indicating the user does not own this consumer.""" + pecan.abort(403, u._('Not Allowed. Sorry, only the creator of a consumer ' + 'can delete it.')) + + class ContainerConsumerController(controllers.ACLMixin): """Handles Consumer entity retrieval and deletion requests.""" @@ -51,7 +57,6 @@ class ContainerConsumerController(controllers.ACLMixin): def on_get(self, external_project_id): consumer = self.consumer_repo.get( entity_id=self.consumer_id, - external_project_id=external_project_id, suppress_exception=True) if not consumer: _consumer_not_found() @@ -92,11 +97,6 @@ class ContainerConsumersController(controllers.ACLMixin): LOG.debug(u._('Start consumers on_get ' 'for container-ID %s:'), self.container_id) - try: - self.container_repo.get(self.container_id, external_project_id) - except exception.NotFound: - controllers.containers.container_not_found() - result = self.consumer_repo.get_by_container_id( self.container_id, offset_arg=kw.get('offset', 0), @@ -136,18 +136,13 @@ class ContainerConsumersController(controllers.ACLMixin): data = api.load_body(pecan.request, validator=self.validator) LOG.debug('Start on_post...%s', data) - try: - container = self.container_repo.get(self.container_id, - external_project_id) - except exception.NotFound: - controllers.containers.container_not_found() + container = self._get_container(self.container_id) self.quota_enforcer.enforce(project) new_consumer = models.ContainerConsumerMetadatum(self.container_id, project.id, data) - new_consumer.project_id = project.id self.consumer_repo.create_or_update_from(new_consumer, container) url = hrefs.convert_consumer_to_href(new_consumer.container_id) @@ -156,8 +151,7 @@ class ContainerConsumersController(controllers.ACLMixin): LOG.info(u._LI('Created a consumer for project: %s'), external_project_id) - return self._return_container_data(self.container_id, - external_project_id) + return self._return_container_data(self.container_id) @index.when(method='DELETE', template='json') @controllers.handle_exceptions(u._('ContainerConsumer deletion')) @@ -176,6 +170,13 @@ class ContainerConsumersController(controllers.ACLMixin): _consumer_not_found() LOG.debug("Found consumer: %s", consumer) + container = self._get_container(self.container_id) + owner_of_consumer = consumer.project_id == external_project_id + owner_of_container = container.project.external_id \ + == external_project_id + if not owner_of_consumer and not owner_of_container: + _consumer_ownership_mismatch() + try: self.consumer_repo.delete_entity_by_id(consumer.id, external_project_id) @@ -183,21 +184,22 @@ class ContainerConsumersController(controllers.ACLMixin): LOG.exception(u._LE('Problem deleting consumer')) _consumer_not_found() - ret_data = self._return_container_data( - self.container_id, - external_project_id - ) + ret_data = self._return_container_data(self.container_id) LOG.info(u._LI('Deleted a consumer for project: %s'), external_project_id) return ret_data - def _return_container_data(self, container_id, external_project_id): - try: - container = self.container_repo.get(container_id, - external_project_id) - dict_fields = container.to_dict_fields() - except Exception: + def _get_container(self, container_id): + container = self.container_repo.get_container_by_id( + container_id, suppress_exception=True) + if not container: controllers.containers.container_not_found() + return container + + def _return_container_data(self, container_id): + container = self._get_container(container_id) + + dict_fields = container.to_dict_fields() for secret_ref in dict_fields['secret_refs']: hrefs.convert_to_hrefs(secret_ref) diff --git a/barbican/tests/api/test_resources.py b/barbican/tests/api/test_resources.py index 7c6a1202..10e65a60 100644 --- a/barbican/tests/api/test_resources.py +++ b/barbican/tests/api/test_resources.py @@ -786,7 +786,6 @@ class WhenCreatingConsumersUsingConsumersResource(FunctionalTest): # Set up mocked container repo self.container_repo = mock.MagicMock() - self.container_repo.get.return_value = self.container self.container_repo.get_container_by_id.return_value = self.container self.setup_container_repository_mock(self.container_repo) @@ -824,22 +823,12 @@ class WhenCreatingConsumersUsingConsumersResource(FunctionalTest): ) self.assertEqual(415, resp.status_int) - def test_should_404_consumer_bad_container_id(self): - self.container_repo.get.side_effect = excep.NotFound() + def test_should_404_when_container_ref_doesnt_exist(self): + self.container_repo.get_container_by_id.return_value = None resp = self.app.post_json( '/containers/{0}/consumers/'.format('bad_id'), self.consumer_ref, expect_errors=True ) - self.container_repo.get.side_effect = None - self.assertEqual(404, resp.status_int) - - def test_should_raise_exception_when_container_ref_doesnt_exist(self): - self.container_repo.get.return_value = None - resp = self.app.post_json( - '/containers/{0}/consumers/'.format(self.container.id), - self.consumer_ref, - expect_errors=True - ) self.assertEqual(404, resp.status_int) @@ -896,7 +885,6 @@ class WhenGettingOrDeletingConsumersUsingConsumerResource(FunctionalTest): # Set up mocked container repo self.container_repo = mock.MagicMock() - self.container_repo.get.return_value = self.container self.container_repo.get_container_by_id.return_value = self.container self.setup_container_repository_mock(self.container_repo) @@ -928,12 +916,11 @@ class WhenGettingOrDeletingConsumersUsingConsumerResource(FunctionalTest): self.assertEqual(self.consumer.name, resp.json['consumers'][0]['name']) self.assertEqual(self.consumer.URL, resp.json['consumers'][0]['URL']) - def test_should_404_with_bad_container_id(self): - self.container_repo.get.side_effect = excep.NotFound() + def test_should_404_when_container_ref_doesnt_exist(self): + self.container_repo.get_container_by_id.return_value = None resp = self.app.get('/containers/{0}/consumers/'.format( 'bad_id' ), expect_errors=True) - self.container_repo.get.side_effect = None self.assertEqual(404, resp.status_int) def test_should_get_consumer_by_id(self): @@ -1105,7 +1092,7 @@ class WhenPerformingUnallowedOperationsOnConsumers(FunctionalTest): # Set up container repo self.container_repo = mock.MagicMock() - self.container_repo.get.return_value = self.container + self.container_repo.get_container_by_id.return_value = self.container self.setup_container_repository_mock(self.container_repo) # Set up container consumer repo @@ -1156,3 +1143,75 @@ class WhenPerformingUnallowedOperationsOnConsumers(FunctionalTest): expect_errors=True ) self.assertEqual(405, resp.status_int) + + +class WhenOwnershipMismatch(FunctionalTest): + + def setUp(self): + super( + WhenOwnershipMismatch, self + ).setUp() + self.app = webtest.TestApp(app.build_wsgi_app(self.root)) + self.app.extra_environ = get_barbican_env(self.external_project_id) + + @property + def root(self): + self._init() + + class RootController(object): + containers = controllers.containers.ContainersController() + return RootController() + + def _init(self): + self.external_project_id = 'keystoneid1234' + self.project_internal_id = 'projectid1234' + + # Set up mocked project + self.project = models.Project() + self.project.id = self.project_internal_id + self.project.external_id = self.external_project_id + + # Set up mocked project repo + self.project_repo = mock.MagicMock() + self.project_repo.get.return_value = self.project + self.setup_project_repository_mock(self.project_repo) + + # Set up mocked container + self.container = create_container( + id_ref='id1', + project_id=self.project_internal_id, + external_project_id='differentProjectId') + + # Set up mocked consumers + self.consumer = create_consumer(self.container.id, + self.project_internal_id, + id_ref='id2') + self.consumer2 = create_consumer(self.container.id, + self.project_internal_id, + id_ref='id3') + + self.consumer_ref = { + 'name': self.consumer.name, + 'URL': self.consumer.URL + } + + # Set up mocked container repo + self.container_repo = mock.MagicMock() + self.container_repo.get.return_value = self.container + self.container_repo.get_container_by_id.return_value = self.container + self.setup_container_repository_mock(self.container_repo) + + # Set up mocked container consumer repo + self.consumer_repo = mock.MagicMock() + self.consumer_repo.get_by_values.return_value = self.consumer + self.consumer_repo.delete_entity_by_id.return_value = None + self.setup_container_consumer_repository_mock(self.consumer_repo) + + # Set up mocked secret repo + self.setup_secret_repository_mock() + + def test_consumer_check_ownership_mismatch(self): + resp = self.app.delete_json( + '/containers/{0}/consumers/'.format(self.container.id), + self.consumer_ref, expect_errors=True) + self.assertEqual(403, resp.status_int) diff --git a/etc/barbican/policy.json b/etc/barbican/policy.json index 06e0b06c..87e87cb5 100644 --- a/etc/barbican/policy.json +++ b/etc/barbican/policy.json @@ -38,10 +38,10 @@ "order:get": "rule:all_users", "order:put": "rule:admin_or_creator", "order:delete": "rule:admin", - "consumer:get": "rule:all_users", - "consumers:get": "rule:all_users", - "consumers:post": "rule:admin", - "consumers:delete": "rule:admin", + "consumer:get": "rule:admin or rule:observer or rule:creator or rule:audit or rule:container_non_private_read or rule:container_project_creator or rule:container_project_admin or rule:container_acl_read", + "consumers:get": "rule:admin or rule:observer or rule:creator or rule:audit or rule:container_non_private_read or rule:container_project_creator or rule:container_project_admin or rule:container_acl_read", + "consumers:post": "rule:admin or rule:container_non_private_read or rule:container_project_creator or rule:container_project_admin or rule:container_acl_read", + "consumers:delete": "rule:admin or rule:container_non_private_read or rule:container_project_creator or rule:container_project_admin or rule:container_acl_read", "containers:post": "rule:admin_or_creator", "containers:get": "rule:all_but_audit", "container:get": "rule:container_non_private_read or rule:container_project_creator or rule:container_project_admin or rule:container_acl_read", diff --git a/functionaltests/api/v1/functional/test_acls.py b/functionaltests/api/v1/functional/test_acls.py index 41b99365..166c12c0 100644 --- a/functionaltests/api/v1/functional/test_acls.py +++ b/functionaltests/api/v1/functional/test_acls.py @@ -130,8 +130,8 @@ test_data_read_container_consumer_acl_only = { 'with_creator_a': {'user': creator_a, 'expected_return': 200}, 'with_observer_a': {'user': observer_a, 'expected_return': 200}, 'with_auditor_a': {'user': auditor_a, 'expected_return': 200}, - 'with_admin_b': {'user': admin_b, 'expected_return': 404}, - 'with_observer_b': {'user': observer_b, 'expected_return': 404}, + 'with_admin_b': {'user': admin_b, 'expected_return': 200}, + 'with_observer_b': {'user': observer_b, 'expected_return': 200}, } test_data_delete_container_consumer_acl_only = { @@ -139,7 +139,7 @@ test_data_delete_container_consumer_acl_only = { 'with_creator_a': {'user': creator_a, 'expected_return': 403}, 'with_observer_a': {'user': observer_a, 'expected_return': 403}, 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, - 'with_admin_b': {'user': admin_b, 'expected_return': 404}, + 'with_admin_b': {'user': admin_b, 'expected_return': 403}, 'with_observer_b': {'user': observer_b, 'expected_return': 403}, } @@ -148,7 +148,7 @@ test_data_create_container_consumer_acl_only = { 'with_creator_a': {'user': creator_a, 'expected_return': 403}, 'with_observer_a': {'user': observer_a, 'expected_return': 403}, 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, - 'with_admin_b': {'user': admin_b, 'expected_return': 404}, + 'with_admin_b': {'user': admin_b, 'expected_return': 200}, 'with_observer_b': {'user': observer_b, 'expected_return': 403}, } From c98980af45492a0b127d1d5a8dce6d33d1beb985 Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Wed, 7 Sep 2016 08:12:40 +0000 Subject: [PATCH 10/20] Imported Translations from Zanata For more information about this automatic import see: https://wiki.openstack.org/wiki/Translations/Infrastructure Change-Id: Ic0e9323d79c7752aa08e2b91cd0e726410bad025 --- barbican/locale/zh_CN/LC_MESSAGES/barbican.po | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/barbican/locale/zh_CN/LC_MESSAGES/barbican.po b/barbican/locale/zh_CN/LC_MESSAGES/barbican.po index e5e112f0..077b9862 100644 --- a/barbican/locale/zh_CN/LC_MESSAGES/barbican.po +++ b/barbican/locale/zh_CN/LC_MESSAGES/barbican.po @@ -9,13 +9,13 @@ # Jiong Liu , 2016. #zanata msgid "" msgstr "" -"Project-Id-Version: barbican 3.0.0.0b3.dev18\n" +"Project-Id-Version: barbican 3.0.0.0b4.dev3\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-08-05 03:25+0000\n" +"POT-Creation-Date: 2016-09-07 03:23+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2016-07-26 09:43+0000\n" +"PO-Revision-Date: 2016-09-01 01:38+0000\n" "Last-Translator: Jiong Liu \n" "Language: zh-CN\n" "Plural-Forms: nplurals=1; plural=0;\n" @@ -778,12 +778,6 @@ msgstr "NSS证书数据库的密码" msgid "Password to login to PKCS11 session" msgstr "登录到PKCS11会话的密码" -msgid "Path to CA certicate chain file" -msgstr "CA证书链文件的路径" - -msgid "Path to CA certicate file" -msgstr "CA证书文件的路径" - msgid "Path to CA certificate key file" msgstr "CA证书密钥文件的路径" @@ -1368,4 +1362,4 @@ msgid "{request} not found for {operation} for order_id {order_id}" msgstr "未找到order_id为{order_id}的{operation}操作的{request}请求" msgid "{schema_name}' within '{parent_schema_name}" -msgstr "带有'{parent_schema_name}'的'{schema_name}'" +msgstr "带有{parent_schema_name}的{schema_name}'" From 669a995196468e9fae3805419f25b4d9e6c13aa9 Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Tue, 12 Jul 2016 14:04:36 -0700 Subject: [PATCH 11/20] Adding API docs for multiple backend support changes. Updated multiple plugin configuration sample as per recent discussion. Change-Id: I7d5c0f088c392ae73dea603198c1845c346b2d39 Partially-Implements: blueprint multiple-secret-backend --- doc/source/api/index.rst | 1 + doc/source/api/reference/store_backends.rst | 416 ++++++++++++++++++++ doc/source/setup/index.rst | 1 + doc/source/setup/plugin_backends.rst | 100 +++++ 4 files changed, 518 insertions(+) create mode 100644 doc/source/api/reference/store_backends.rst create mode 100644 doc/source/setup/plugin_backends.rst diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index 40d15adc..2cddcd85 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -17,6 +17,7 @@ API Reference ./reference/secrets ./reference/secret_types ./reference/secret_metadata + ./reference/store_backends.rst ./reference/containers ./reference/acls ./reference/certificates diff --git a/doc/source/api/reference/store_backends.rst b/doc/source/api/reference/store_backends.rst new file mode 100644 index 00000000..ddf44d9d --- /dev/null +++ b/doc/source/api/reference/store_backends.rst @@ -0,0 +1,416 @@ +***************************** +Secret Stores API - Reference +***************************** + +Barbican provides API to manage secret stores available in a deployment. APIs +are provided for listing available secret stores and to manage project level +secret store mapping. There are two types of secret stores. One is global +default secret store which is used for all projects. And then project +`preferred` secret store which is used to store all *new* secrets created in +that project. For an introduction to multiple store backends support, see +:doc:`Using Multiple Secret Store Plugins ` . This +document will focus on the details of the Barbican `/v1/secret-stores` REST API. + +When multiple secret store backends support is not enabled in service +configuration, then all of these API will return resource not found (http +status code 404) error. Error message text will highlight that the support is +not enabled in configuration. + +GET /v1/secret-stores +##################### +Project administrator can request list of available secret store backends. +Response contains list of secret stores which are currently configured in +barbican deployment. If multiple store backends support is not enabled, then +list will return resource not found (404) error. + +.. _get_secret_stores_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + + GET /secret-stores + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + Accept: application/json + + Response: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "secret-stores":[ + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.114283", + "name": "PKCS11 HSM", + "created": "2016-08-22T23:46:45.114283", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/4d27b7a7-b82f-491d-88c0-746bd67dadc8", + "global_default": True, + "crypto_plugin": "p11_crypto", + "secret_store_plugin": "store_crypto" + }, + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.124554", + "name": "KMIP HSM", + "created": "2016-08-22T23:46:45.124554", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/93869b0f-60eb-4830-adb9-e2f7154a080b", + "global_default": False, + "crypto_plugin": None, + "secret_store_plugin": "kmip_plugin" + }, + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.127866", + "name": "Software Only Crypto", + "created": "2016-08-22T23:46:45.127866", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/0da45858-9420-42fe-a269-011f5f35deaa", + "global_default": False, + "crypto_plugin": "simple_crypto", + "secret_store_plugin": "store_crypto" + } + } + + +.. _get_secret_stores_response_attributes: + +Response Attributes +******************* + ++---------------+--------+---------------------------------------------+ +| Name | Type | Description | ++===============+========+=============================================+ +| secret-stores | list | A list of secret store references | ++---------------+--------+---------------------------------------------+ +| name | string | store and crypto plugin name delimited by + | +| | | (plus) sign. | ++---------------+--------+---------------------------------------------+ +| secret_store | string | URL for referencing a specific secret store | +| _ref | | | ++---------------+--------+---------------------------------------------+ + +.. _get_secret_stores_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 200 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | Not Found. When multiple secret store backends support is not enabled. | ++------+--------------------------------------------------------------------------+ + + +GET /v1/secret-stores/{secret_store_id} +####################################### + +A project administrator (user with admin role) can request details of secret +store by its ID. Returned response will highlight whether this secret store is +currently configured as global default or not. + +.. _get_secret_stores_id_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + GET /secret-stores/93869b0f-60eb-4830-adb9-e2f7154a080b + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + Accept: application/json + + Response: + HTTP/1.1 200 OK + Content-Type: application/json + + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.124554", + "name": "KMIP HSM", + "created": "2016-08-22T23:46:45.124554", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/93869b0f-60eb-4830-adb9-e2f7154a080b", + "global_default": False, + "crypto_plugin": None, + "secret_store_plugin": "kmip_plugin" + } + + +.. _get_secret_stores_id_response_attributes: + +Response Attributes +******************* + ++------------------+---------+---------------------------------------------------------------+ +| Name | Type | Description | ++==================+=========+===============================================================+ +| name | string | store and crypto plugin name delimited by '+' (plus) sign | ++------------------+---------+---------------------------------------------------------------+ +| global_default | boolean | flag indicating if this secret store is global default or not | ++------------------+---------+---------------------------------------------------------------+ +| status | list | Status of the secret store | ++------------------+---------+---------------------------------------------------------------+ +| updated | time | Date and time secret store was last updated | ++------------------+---------+---------------------------------------------------------------+ +| created | time | Date and time secret store was created | ++------------------+---------+---------------------------------------------------------------+ +| secret_store_ref | string | URL for referencing a specific secret store | ++------------------+---------+---------------------------------------------------------------+ + + +.. _get_secret_stores_id_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 200 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | Not Found. When multiple secret store backends support is not enabled or | +| | that secret store id does not exist. | ++------+--------------------------------------------------------------------------+ + +GET /v1/secret-stores/preferred +############################### + +A project administrator (user with admin role) can request a reference to the +preferred secret store if assigned previously. When a preferred secret store is +set for a project, then new project secrets are stored using that store +backend. If multiple secret store support is not enabled, then this resource +will return 404 (Not Found) error. + +.. _get_secret_stores_preferred_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + + GET /v1/secret-stores/preferred + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + Accept: application/json + + + Response: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.114283", + "name": "PKCS11 HSM", + "created": "2016-08-22T23:46:45.114283", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/4d27b7a7-b82f-491d-88c0-746bd67dadc8", + "global_default": True, + "crypto_plugin": "p11_crypto", + "secret_store_plugin": "store_crypto" + } + + +.. _get_secret_stores_preferred_response_attributes: + +Response Attributes +******************* + ++------------------+--------+-----------------------------------------------+ +| Name | Type | Description | ++==================+========+===============================================+ +| secret_store_ref | string | A URL that references a specific secret store | ++------------------+--------+-----------------------------------------------+ + +.. _get_secret_stores_preferred_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 200 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | Not found. No preferred secret store has been defined or multiple secret | +| | store backends support is not enabled. | ++------+--------------------------------------------------------------------------+ + +POST /v1/secret-stores/{secret_store_id}/preferred +################################################## + +A project administrator can set a secret store backend to be preferred store +backend for his/her project. From there on, any new secret stored in that +project will use specified plugin backend for storage and reading thereafter. +Existing secret storage will not be impacted as each secret captures its plugin +backend information when initially stored. If multiple secret store support is +not enabled, then this resource will return 404 (Not Found) error. + +.. _post_secret_stores_id_preferred_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + + POST /v1/secret-stores/7776adb8-e865-413c-8ccc-4f09c3fe0213/preferred + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + + Response: + + HTTP/1.1 204 No Content + + +.. _post_secret_stores_id_preferred_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 204 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | The requested entity was not found or multiple secret store backends | +| | support is not enabled. | ++------+--------------------------------------------------------------------------+ + + +DELETE /v1/secret-stores/{secret_store_id}/preferred +#################################################### + +A project administrator can remove preferred secret store backend setting. If +multiple secret store support is not enabled, then this resource will return +404 (Not Found) error. + +.. _delete_secret_stores_id_preferred_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + + DELETE /v1/secret-stores/7776adb8-e865-413c-8ccc-4f09c3fe0213/preferred + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + + Response: + + HTTP/1.1 204 No Content + +.. _delete_secret_stores_id_preferred_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 204 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | The requested entity was not found or multiple secret store backends | +| | support is not enabled. | ++------+--------------------------------------------------------------------------+ + + +GET /v1/secret-stores/global-default +#################################### + +A project or service administrator can can request a reference to the secret +store that is used as default secret store backend for the deployment. + +.. _get_secret_stores_global_default_request_response: + +Request/Response: +***************** + +.. code-block:: javascript + + Request: + + GET /v1/secret-stores/global-default + Headers: + X-Auth-Token: "f9cf2d480ba3485f85bdb9d07a4959f1" + Accept: application/json + + + Response: + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "status": "ACTIVE", + "updated": "2016-08-22T23:46:45.114283", + "name": "PKCS11 HSM", + "created": "2016-08-22T23:46:45.114283", + "secret_store_ref": "http://localhost:9311/v1/secret-stores/4d27b7a7-b82f-491d-88c0-746bd67dadc8", + "global_default": True, + "crypto_plugin": "p11_crypto", + "secret_store_plugin": "store_crypto" + } + + +.. _get_secret_stores_global_default_response_attributes: + +Response Attributes +******************* + ++------------------+--------+-----------------------------------------------+ +| Name | Type | Description | ++==================+========+===============================================+ +| secret_store_ref | string | A URL that references a specific secret store | ++------------------+--------+-----------------------------------------------+ + +.. _get_secret_stores_global_default_status_codes: + +HTTP Status Codes +***************** + ++------+--------------------------------------------------------------------------+ +| Code | Description | ++======+==========================================================================+ +| 200 | Successful Request | ++------+--------------------------------------------------------------------------+ +| 401 | Authentication error. Missing or invalid X-Auth-Token. | ++------+--------------------------------------------------------------------------+ +| 403 | The user was authenticated, but is not authorized to perform this action | ++------+--------------------------------------------------------------------------+ +| 404 | Not Found. When multiple secret store backends support is not enabled. | ++------+--------------------------------------------------------------------------+ + diff --git a/doc/source/setup/index.rst b/doc/source/setup/index.rst index 1d3af9f5..ac81eced 100644 --- a/doc/source/setup/index.rst +++ b/doc/source/setup/index.rst @@ -11,3 +11,4 @@ Setting up Barbican troubleshooting noauth audit + plugin_backends diff --git a/doc/source/setup/plugin_backends.rst b/doc/source/setup/plugin_backends.rst new file mode 100644 index 00000000..09cf7a83 --- /dev/null +++ b/doc/source/setup/plugin_backends.rst @@ -0,0 +1,100 @@ +Using Secret Store Plugins in Barbican +====================================== + + +Summary +------- + +By default, Barbican is configured to use one active secret store plugin in a +deployment. This means that all of the new secrets are going to be stored via +same plugin mechanism (i.e. same storage backend). + +In **Newton** OpenStack release, support for configuring multiple secret store +plugin backends is added (`Spec Link`_). As part of this change, client can +choose to select preferred plugin backend for storing their secret at a project +level. + + +.. _Spec Link: https://review.openstack.org/#/c/263972 + + +Enabling Multiple Barbican Backends +----------------------------------- + +Multiple backends support may be needed in specific deployment/ use-case +scenarios and can be enabled via configuration. + +For this, a Barbican deployment may have more than one secret storage backend +added in service configuration. Project administrators will have choice of +pre-selecting one backend as the preferred choice for secrets created under +that project. Any **new** secret created under that project will use the +preferred backend to store its key material. When there is no project level +storage backend selected, then new secret will use the global secret storage +backend. + +Multiple plugin configuration can be defined as follows. + +.. code-block:: ini + + [secretstore] + # Set to True when multiple plugin backends support is needed + enable_multiple_secret_stores = True + stores_lookup_suffix = software, kmip, pkcs11, dogtag + + [secretstore:software] + secret_store_plugin = store_crypto + crypto_plugin = simple_crypto + + [secretstore:kmip] + secret_store_plugin = kmip_plugin + global_default = True + + [secretstore:dogtag] + secret_store_plugin = dogtag_plugin + + [secretstore:pkcs11] + secret_store_plugin = store_crypto + crypto_plugin = p11_crypto + +When `enable_multiple_secret_stores` is enabled (True), then list property +`stores_lookup_suffix` is used for looking up supported plugin names in +configuration section. This section name is constructed using pattern +'secretstore:{one_of_suffix}'. One of the plugin **must** be explicitly +identified as global default i.e. `global_default = True`. Ordering of suffix +and label used does not matter as long as there is a matching section defined +in service configuration. + +.. note:: + + For existing Barbican deployment case, its recommended to keep existing + secretstore and crypto plugin (if applicable) name combination to be used as + global default secret store. This is needed to be consistent with existing + behavior. + +.. warning:: + + When multiple plugins support is enabled, then `enabled_secretstore_plugins` + and `enabled_crypto_plugins` values are **not** used to instantiate relevant + plugins. Only above mentioned mechanism is used to identify and instantiate + store and crypto plugins. + +Multiple backend can be useful in following type of usage scenarios. + +* In a deployment, a deployer may be okay in storing their dev/test resources + using a low-security secret store, such as one backend using software-only + crypto, but may want to use an HSM-backed secret store for production + resources. +* In a deployment, for certain use cases where a client requires high + concurrent access of stored keys, HSM might not be a good storage backend. + Also scaling them horizontally to provide higher scalability is a costly + approach with respect to database. +* HSM devices generally have limited storage capacity so a deployment will + have to watch its stored keys size proactively to remain under the limit + constraint. This is more applicable in KMIP backend than with PKCS11 backend + because of plugin's different storage apporach. This aspect can also result + from above use case scenario where deployment is storing non-sensitive (from + dev/test environment) encryption keys in HSM. +* Barbican running as IaaS service or platform component where some class of + client services have strict compliance requirements (e.g. FIPS) so will use + HSM backed plugins whereas others may be okay storing keys in software-only + crypto plugin. From 29edae6da2e8fea94a913530f89f737e1ad2695c Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Fri, 22 Jul 2016 09:28:08 -0700 Subject: [PATCH 12/20] Adding multiple backend db model and repository support (Part 1) Added alembic migration support scripts for new tables. Partially-Implements: blueprint multiple-secret-backend Change-Id: If982a8f73e63379ed66803565700d620eca0dcb9 --- ...5cba_model_for_multiple_backend_support.py | 62 +++ barbican/model/models.py | 99 ++++ barbican/model/repositories.py | 162 +++++++ .../test_repositores_secret_stores.py | 426 ++++++++++++++++++ barbican/tests/model/test_models.py | 113 +++++ 5 files changed, 862 insertions(+) create mode 100644 barbican/model/migration/alembic_migrations/versions/39cf2e645cba_model_for_multiple_backend_support.py create mode 100644 barbican/tests/model/repositories/test_repositores_secret_stores.py diff --git a/barbican/model/migration/alembic_migrations/versions/39cf2e645cba_model_for_multiple_backend_support.py b/barbican/model/migration/alembic_migrations/versions/39cf2e645cba_model_for_multiple_backend_support.py new file mode 100644 index 00000000..37ea5785 --- /dev/null +++ b/barbican/model/migration/alembic_migrations/versions/39cf2e645cba_model_for_multiple_backend_support.py @@ -0,0 +1,62 @@ +"""Model for multiple backend support + +Revision ID: 39cf2e645cba +Revises: d2780d5aa510 +Create Date: 2016-07-29 16:45:22.953811 + +""" + +# revision identifiers, used by Alembic. +revision = '39cf2e645cba' +down_revision = 'd2780d5aa510' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ctx = op.get_context() + con = op.get_bind() + table_exists = ctx.dialect.has_table(con.engine, 'secret_stores') + if not table_exists: + op.create_table( + 'secret_stores', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('store_plugin', sa.String(length=255), nullable=False), + sa.Column('crypto_plugin', sa.String(length=255), nullable=True), + sa.Column('global_default', sa.Boolean(), nullable=False, + default=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('store_plugin', 'crypto_plugin', + name='_secret_stores_plugin_names_uc'), + sa.UniqueConstraint('name', + name='_secret_stores_name_uc') + ) + + table_exists = ctx.dialect.has_table(con.engine, 'project_secret_store') + if not table_exists: + op.create_table( + 'project_secret_store', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('project_id', sa.String(length=36), nullable=False), + sa.Column('secret_store_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['projects.id'],), + sa.ForeignKeyConstraint( + ['secret_store_id'], ['secret_stores.id'],), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('project_id', + name='_project_secret_store_project_uc') + ) + op.create_index(op.f('ix_project_secret_store_project_id'), + 'project_secret_store', ['project_id'], unique=True) diff --git a/barbican/model/models.py b/barbican/model/models.py index 0afe0c0a..05a80711 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -1379,3 +1379,102 @@ class ProjectQuotas(BASE, ModelBase): if self.cas: ret['cas'] = self.cas return ret + + +class SecretStores(BASE, ModelBase): + """List of secret stores defined via service configuration. + + This class provides a list of secret stores entities with their respective + secret store plugin and crypto plugin names. + + SecretStores deletes are NOT soft-deletes. + """ + + __tablename__ = 'secret_stores' + + store_plugin = sa.Column(sa.String(255), nullable=False) + crypto_plugin = sa.Column(sa.String(255), nullable=True) + global_default = sa.Column(sa.Boolean, nullable=False, default=False) + name = sa.Column(sa.String(255), nullable=False) + + __table_args__ = (sa.UniqueConstraint( + 'store_plugin', 'crypto_plugin', + name='_secret_stores_plugin_names_uc'), + sa.UniqueConstraint('name', name='_secret_stores_name_uc'),) + + def __init__(self, name, store_plugin, crypto_plugin=None, + global_default=None): + """Creates secret store entity.""" + super(SecretStores, self).__init__() + + msg = u._("Must supply non-Blank {0} argument for SecretStores entry.") + + if not name: + raise exception.MissingArgumentError(msg.format("name")) + if not store_plugin: + raise exception.MissingArgumentError(msg.format("store_plugin")) + + self.store_plugin = store_plugin + self.name = name + self.crypto_plugin = crypto_plugin + if global_default is not None: + self.global_default = global_default + + self.status = States.ACTIVE + + def _do_extra_dict_fields(self): + """Sub-class hook method: return dict of fields.""" + return {'secret_store_id': self.id, + 'store_plugin': self.store_plugin, + 'crypto_plugin': self.crypto_plugin, + 'global_default': self.global_default, + 'name': self.name} + + +class ProjectSecretStore(BASE, ModelBase): + """Stores secret store to be used for new project secrets. + + This class maintains secret store and project mapping so that new project + secret entries uses it as plugin backend. + + ProjectSecretStores deletes are NOT soft-deletes. + """ + + __tablename__ = 'project_secret_store' + + secret_store_id = sa.Column(sa.String(36), + sa.ForeignKey('secret_stores.id'), + index=True, + nullable=False) + project_id = sa.Column(sa.String(36), + sa.ForeignKey('projects.id'), + index=True, + nullable=False) + + secret_store = orm.relationship("SecretStores", backref="project_store") + project = orm.relationship('Project', + backref=orm.backref('preferred_secret_store')) + + __table_args__ = (sa.UniqueConstraint( + 'project_id', name='_project_secret_store_project_uc'),) + + def __init__(self, project_id, secret_store_id): + """Creates project secret store mapping entity.""" + super(ProjectSecretStore, self).__init__() + + msg = u._("Must supply non-None {0} argument for ProjectSecretStore " + " entry.") + + if not project_id: + raise exception.MissingArgumentError(msg.format("project_id")) + self.project_id = project_id + if not secret_store_id: + raise exception.MissingArgumentError(msg.format("secret_store_id")) + self.secret_store_id = secret_store_id + + self.status = States.ACTIVE + + def _do_extra_dict_fields(self): + """Sub-class hook method: return dict of fields.""" + return {'secret_store_id': self.secret_store_id, + 'project_id': self.project_id} diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index 3eb4321e..cb5e1715 100644 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -68,6 +68,8 @@ _SECRET_META_REPOSITORY = None _SECRET_USER_META_REPOSITORY = None _SECRET_REPOSITORY = None _TRANSPORT_KEY_REPOSITORY = None +_SECRET_STORES_REPOSITORY = None +_PROJECT_SECRET_STORE_REPOSITORY = None CONF = config.CONF @@ -2189,6 +2191,153 @@ class ProjectQuotasRepo(BaseRepo): entity.delete(session=session) +class SecretStoresRepo(BaseRepo): + """Repository for the SecretStores entity. + + SecretStores entries are not soft delete. So there is no + need to have deleted=False filter in queries. + """ + + def get_all(self, session=None): + """Get list of available secret stores. + + Status value is not used while getting complete list as + we will just maintain ACTIVE ones. No other state is used and + needed here. + :param session: SQLAlchemy session object. + :return: None + """ + session = self.get_session(session) + query = session.query(models.SecretStores) + query.order_by(models.SecretStores.created_at.asc()) + return query.all() + + def _do_entity_name(self): + """Sub-class hook: return entity name, such as for debugging.""" + return "SecretStores" + + def _do_build_get_query(self, entity_id, external_project_id, session): + """Sub-class hook: build a retrieve query.""" + return session.query(models.SecretStores).filter_by( + id=entity_id) + + def _do_validate(self, values): + """Sub-class hook: validate values.""" + pass + + +class ProjectSecretStoreRepo(BaseRepo): + """Repository for the ProjectSecretStore entity. + + ProjectSecretStore entries are not soft delete. So there is no + need to have deleted=False filter in queries. + """ + + def get_secret_store_for_project(self, project_id, external_project_id, + suppress_exception=False, session=None): + """Returns preferred secret store for a project if set. + + :param project_id: ID of project whose preferred secret store is set + :param external_project_id: external ID of project whose preferred + secret store is set + :param suppress_exception: when True, NotFound is not raised + :param session: SQLAlchemy session object. + + Will return preferred secret store by external project id if provided + otherwise uses barbican project identifier to lookup. + + Throws exception in case no preferred secret store is defined and + supporess_exception=False. If suppress_exception is True, then returns + None for no preferred secret store for a project found. + """ + session = self.get_session(session) + if external_project_id is None: + query = session.query(models.ProjectSecretStore).filter_by( + project_id=project_id) + else: + query = session.query(models.ProjectSecretStore) + query = query.join(models.Project, + models.ProjectSecretStore.project) + query = query.filter(models.Project.external_id == + external_project_id) + try: + entity = query.one() + except sa_orm.exc.NoResultFound: + LOG.info(u._LE("No preferred secret store found for project = %s"), + project_id) + entity = None + if not suppress_exception: + _raise_entity_not_found(self._do_entity_name(), project_id) + return entity + + def create_or_update_for_project(self, project_id, secret_store_id, + session=None): + """Create or update preferred secret store for a project. + + :param project_id: ID of project whose preferred secret store is set + :param secret_store_id: ID of secret store + :param session: SQLAlchemy session object. + :return: None + + If preferred secret store is not set for given project, then create + new preferred secret store setting for that project. If secret store + setting for project is already there, then it updates with given secret + store id. + """ + session = self.get_session(session) + try: + entity = self.get_secret_store_for_project(project_id, None, + session=session) + except exception.NotFound: + entity = self.create_from( + models.ProjectSecretStore(project_id, secret_store_id), + session=session) + else: + entity.secret_store_id = secret_store_id + entity.save(session) + return entity + + def get_count_by_secret_store(self, secret_store_id, session=None): + """Gets count of projects mapped to a given secret store. + + :param secret_store_id: id of secret stores entity + :param session: existing db session reference. If None, gets session. + :return: an number 0 or greater + + This method is supposed to provide count of projects which are + currently set to use input secret store as their preferred store. This + is used when existing secret store configuration is removed and + validation is done to make sure that there are no projects using it as + preferred secret store. + """ + session = self.get_session(session) + query = session.query(models.ProjectSecretStore).filter_by( + secret_store_id=secret_store_id) + return query.count() + + def _do_entity_name(self): + """Sub-class hook: return entity name, such as for debugging.""" + return "ProjectSecretStore" + + def _do_build_get_query(self, entity_id, external_project_id, session): + """Sub-class hook: build a retrieve query.""" + return session.query(models.ProjectSecretStore).filter_by( + id=entity_id) + + def _do_validate(self, values): + """Sub-class hook: validate values.""" + pass + + def _build_get_project_entities_query(self, project_id, session): + """Builds query for getting preferred secret stores list for a project. + + :param project_id: id of barbican project entity + :param session: existing db session reference. + """ + return session.query(models.ProjectSecretStore).filter_by( + project_id=project_id) + + def get_ca_repository(): """Returns a singleton Secret repository instance.""" global _CA_REPOSITORY @@ -2316,6 +2465,19 @@ def get_transport_key_repository(): return _get_repository(_TRANSPORT_KEY_REPOSITORY, TransportKeyRepo) +def get_secret_stores_repository(): + """Returns a singleton Secret Stores repository instance.""" + global _SECRET_STORES_REPOSITORY + return _get_repository(_SECRET_STORES_REPOSITORY, SecretStoresRepo) + + +def get_project_secret_store_repository(): + """Returns a singleton Project Secret Store repository instance.""" + global _PROJECT_SECRET_STORE_REPOSITORY + return _get_repository(_PROJECT_SECRET_STORE_REPOSITORY, + ProjectSecretStoreRepo) + + def _get_repository(global_ref, repo_class): if not global_ref: global_ref = repo_class() diff --git a/barbican/tests/model/repositories/test_repositores_secret_stores.py b/barbican/tests/model/repositories/test_repositores_secret_stores.py new file mode 100644 index 00000000..40811e9b --- /dev/null +++ b/barbican/tests/model/repositories/test_repositores_secret_stores.py @@ -0,0 +1,426 @@ +# 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 uuid + +from barbican.common import exception +from barbican.model import models +from barbican.model import repositories +from barbican.tests import database_utils + + +class WhenTestingSecretStoresRepo(database_utils.RepositoryTestCase): + + def setUp(self): + super(WhenTestingSecretStoresRepo, self).setUp() + self.s_stores_repo = repositories.get_secret_stores_repository() + self.def_name = "PKCS11 HSM" + self.def_store_plugin = "store_crypto" + self.def_crypto_plugin = "p11_crypto" + self.default_secret_store = self._create_secret_store( + self.def_name, self.def_store_plugin, self.def_crypto_plugin, True) + + def _create_secret_store(self, name, store_plugin, crypto_plugin=None, + global_default=None): + session = self.s_stores_repo.get_session() + + s_stores_model = models.SecretStores(name=name, + store_plugin=store_plugin, + crypto_plugin=crypto_plugin, + global_default=global_default) + s_stores = self.s_stores_repo.create_from(s_stores_model, + session=session) + + s_stores.save(session=session) + + session.commit() + return s_stores + + def test_get_by_entity_id(self): + + session = self.s_stores_repo.get_session() + s_stores = self.s_stores_repo.get(self.default_secret_store.id, + session=session) + + self.assertIsNotNone(s_stores) + self.assertEqual(self.def_store_plugin, s_stores.store_plugin) + self.assertEqual(self.def_crypto_plugin, s_stores.crypto_plugin) + self.assertEqual(True, s_stores.global_default) + self.assertEqual(models.States.ACTIVE, s_stores.status) + + def test_should_raise_notfound_exception_get_by_entity_id(self): + self.assertRaises(exception.NotFound, self.s_stores_repo.get, + "invalid_id", suppress_exception=False) + + def test_delete_entity_by_id(self): + + session = self.s_stores_repo.get_session() + s_stores = self.s_stores_repo.get(self.default_secret_store.id, + session=session) + self.assertIsNotNone(s_stores) + + self.s_stores_repo.delete_entity_by_id(self.default_secret_store.id, + None, session=session) + s_stores = self.s_stores_repo.get(self.default_secret_store.id, + suppress_exception=True, + session=session) + + self.assertIsNone(s_stores) + + def test_get_all(self): + + session = self.s_stores_repo.get_session() + all_stores = self.s_stores_repo.get_all(session=session) + + self.assertIsNotNone(all_stores) + self.assertEqual(1, len(all_stores)) + + self._create_secret_store("db backend", "store_crypto", + "simple_crypto", False) + all_stores = self.s_stores_repo.get_all(session=session) + self.assertEqual(2, len(all_stores)) + + self.assertEqual("simple_crypto", all_stores[1].crypto_plugin) + self.assertEqual("store_crypto", all_stores[1].store_plugin) + self.assertEqual("db backend", all_stores[1].name) + self.assertEqual(False, all_stores[1].global_default) + + def test_no_data_case_for_get_all(self): + + self.s_stores_repo.delete_entity_by_id(self.default_secret_store.id, + None) + session = self.s_stores_repo.get_session() + all_stores = self.s_stores_repo.get_all(session=session) + self.assertEqual([], all_stores) + + def test_get_all_check_sorting_order(self): + """Check that all stores are sorted in ascending creation time + + """ + session = self.s_stores_repo.get_session() + + self._create_secret_store("second_name", "second_store", + "second_crypto", False) + m_stores = self._create_secret_store("middle_name", "middle_store", + "middle_crypto", False) + self._create_secret_store("last_name", "last_store", "last_crypto", + False) + + all_stores = self.s_stores_repo.get_all(session=session) + self.assertIsNotNone(all_stores) + self.assertEqual(4, len(all_stores)) + # returned list is sorted by created_at field so check for last entry + self.assertEqual("last_crypto", all_stores[3].crypto_plugin) + self.assertEqual("last_store", all_stores[3].store_plugin) + self.assertEqual("last_name", all_stores[3].name) + self.assertEqual(False, all_stores[3].global_default) + + # Now delete in between entry and create as new entry + self.s_stores_repo.delete_entity_by_id(m_stores.id, None, + session=session) + all_stores = self.s_stores_repo.get_all(session=session) + + self._create_secret_store("middle_name", "middle_store", + "middle_crypto", False) + all_stores = self.s_stores_repo.get_all(session=session) + # now newly created entry should be last one. + self.assertEqual("middle_crypto", all_stores[3].crypto_plugin) + self.assertEqual("middle_store", all_stores[3].store_plugin) + self.assertEqual("middle_name", all_stores[3].name) + self.assertEqual(False, all_stores[3].global_default) + + def test_should_raise_duplicate_for_same_plugin_names(self): + """Check for store and crypto plugin name combination uniqueness""" + + name = 'second_name' + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + self._create_secret_store(name, store_plugin, crypto_plugin, False) + self.assertRaises(exception.Duplicate, self._create_secret_store, + "thrid_name", store_plugin, crypto_plugin, False) + + def test_should_raise_duplicate_for_same_names(self): + """Check for secret store 'name' uniqueness""" + + name = 'Db backend' + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + self._create_secret_store(name, store_plugin, crypto_plugin, False) + self.assertRaises(exception.Duplicate, self._create_secret_store, + name, "another_store", "another_crypto", False) + + def test_do_entity_name(self): + """Code coverage for entity_name which is used in case of exception. + + Raising duplicate error for store and crypto plugin combination + """ + name = "DB backend" + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + self._create_secret_store(name, store_plugin, crypto_plugin, False) + try: + self._create_secret_store(name, store_plugin, crypto_plugin, False) + self.assertFail() + except exception.Duplicate as ex: + self.assertIn("SecretStores", ex.message) + + +class WhenTestingProjectSecretStoreRepo(database_utils.RepositoryTestCase): + + def setUp(self): + super(WhenTestingProjectSecretStoreRepo, self).setUp() + self.proj_store_repo = repositories.\ + get_project_secret_store_repository() + self.def_name = "PKCS11 HSM" + self.def_store_plugin = "store_crypto" + self.def_crypto_plugin = "p11_crypto" + self.default_secret_store = self._create_secret_store( + self.def_name, self.def_store_plugin, self.def_crypto_plugin, True) + + def _create_secret_store(self, name, store_plugin, crypto_plugin=None, + global_default=None): + s_stores_repo = repositories.get_secret_stores_repository() + session = s_stores_repo.get_session() + + s_stores_model = models.SecretStores(name=name, + store_plugin=store_plugin, + crypto_plugin=crypto_plugin, + global_default=global_default) + s_stores = s_stores_repo.create_from(s_stores_model, + session=session) + s_stores.save(session=session) + + session.commit() + return s_stores + + def _create_project(self): + session = self.proj_store_repo.get_session() + + project = models.Project() + project.external_id = "keystone_project_id" + uuid.uuid4().hex + project.save(session=session) + return project + + def _create_project_store(self, project_id, secret_store_id): + session = self.proj_store_repo.get_session() + + proj_model = models.ProjectSecretStore(project_id, secret_store_id) + + proj_s_store = self.proj_store_repo.create_from(proj_model, session) + proj_s_store.save(session=session) + return proj_s_store + + def test_get_by_entity_id(self): + """Tests for 'get' call by project secret store id""" + + project = self._create_project() + + proj_s_store = self._create_project_store(project.id, + self.default_secret_store.id) + + session = self.proj_store_repo.get_session() + s_stores = self.proj_store_repo.get(proj_s_store.id, session=session) + + self.assertIsNotNone(proj_s_store) + self.assertEqual(project.id, proj_s_store.project_id) + self.assertEqual(self.default_secret_store.id, + proj_s_store.secret_store_id) + self.assertEqual(models.States.ACTIVE, s_stores.status) + # assert values via relationship + self.assertEqual(self.default_secret_store.store_plugin, + proj_s_store.secret_store.store_plugin) + self.assertEqual(project.external_id, proj_s_store.project.external_id) + + def test_should_raise_notfound_exception_get_by_entity_id(self): + self.assertRaises(exception.NotFound, self.proj_store_repo.get, + "invalid_id", suppress_exception=False) + + def test_delete_entity_by_id(self): + + project = self._create_project() + + proj_s_store = self._create_project_store(project.id, + self.default_secret_store.id) + + session = self.proj_store_repo.get_session() + proj_s_store = self.proj_store_repo.get(proj_s_store.id, + session=session) + + self.assertIsNotNone(proj_s_store) + + self.proj_store_repo.delete_entity_by_id(proj_s_store.id, None, + session=session) + proj_s_store = self.proj_store_repo.get(proj_s_store.id, + suppress_exception=True, + session=session) + + self.assertIsNone(proj_s_store) + + def test_should_raise_duplicate_for_same_project_id(self): + """Check preferred secret store is set only once for project""" + + project1 = self._create_project() + name = "first_name" + store_plugin = 'first_store' + crypto_plugin = 'first_crypto' + s_store1 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + # set preferred secret store for project1 + self._create_project_store(project1.id, + s_store1.id) + + name = "second_name" + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + s_store2 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + self.assertRaises(exception.Duplicate, self._create_project_store, + project1.id, s_store2.id) + + def test_do_entity_name(self): + """Code coverage for entity_name which is used in case of exception. + + Raising duplicate error when try to set another entry for existing + project + """ + project1 = self._create_project() + name = "first name" + store_plugin = 'first_store' + crypto_plugin = 'first_crypto' + s_store1 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + # set preferred secret store for project1 + self._create_project_store(project1.id, + s_store1.id) + try: + name = "second_name" + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + s_store2 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + self._create_project_store(project1.id, s_store2.id) + self.assertFail() + except exception.Duplicate as ex: + self.assertIn("ProjectSecretStore", ex.message) + + def test_get_secret_store_for_project(self): + project1 = self._create_project() + name = "first_name" + store_plugin = 'first_store' + crypto_plugin = 'first_crypto' + s_store1 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + # set preferred secret store for project1 + proj_s_store = self._create_project_store(project1.id, s_store1.id) + + # get preferred secret store by barbican project id + read_project_s_store = self.proj_store_repo.\ + get_secret_store_for_project(project1.id, None) + + self.assertEqual(proj_s_store.project_id, + read_project_s_store.project_id) + self.assertEqual(proj_s_store.secret_store_id, + read_project_s_store.secret_store_id) + + # get preferred secret store by keystone project id + read_project_s_store = self.proj_store_repo.\ + get_secret_store_for_project(None, project1.external_id) + + self.assertEqual(proj_s_store.project_id, + read_project_s_store.project_id) + self.assertEqual(project1.external_id, + read_project_s_store.project.external_id) + + self.assertEqual(proj_s_store.secret_store_id, + read_project_s_store.secret_store_id) + + def test_raise_notfound_exception_get_secret_store_for_project(self): + self.assertRaises(exception.NotFound, + self.proj_store_repo.get_secret_store_for_project, + "invalid_id", None, suppress_exception=False) + + def test_with_exception_suppressed_get_secret_store_for_project(self): + returned_value = self.proj_store_repo.\ + get_secret_store_for_project("invalid_id", None, + suppress_exception=True) + self.assertIsNone(returned_value) + + def test_get_project_entities(self): + entities = self.proj_store_repo.get_project_entities(uuid.uuid4().hex) + self.assertEqual([], entities) + + def test_create_or_update_for_project(self): + project1 = self._create_project() + name = "first_name" + store_plugin = 'first_store' + crypto_plugin = 'first_crypto' + s_store1 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + # assert that no preferred secret store is set project. + entity = self.proj_store_repo.get_secret_store_for_project( + project1.id, None, suppress_exception=True) + self.assertIsNone(entity) + + # create/set preferred secret store now + created_entity = self.proj_store_repo.create_or_update_for_project( + project1.id, s_store1.id) + + entity = self.proj_store_repo.get_secret_store_for_project( + project1.id, None, suppress_exception=False) + self.assertIsNotNone(entity) # new preferred secret store + + self.assertEqual(project1.id, entity.project_id) + self.assertEqual(s_store1.id, entity.secret_store_id) + self.assertEqual(store_plugin, entity.secret_store.store_plugin) + self.assertEqual(crypto_plugin, entity.secret_store.crypto_plugin) + self.assertEqual(name, entity.secret_store.name) + + name = 'second_name' + store_plugin = 'second_store' + crypto_plugin = 'second_crypto' + s_store2 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + updated_entity = self.proj_store_repo.create_or_update_for_project( + project1.id, s_store2.id) + + self.assertEqual(created_entity.id, updated_entity.id) + self.assertEqual(s_store2.id, updated_entity.secret_store_id) + + def test_get_count_by_secret_store(self): + project1 = self._create_project() + name = "first_name" + store_plugin = 'first_store' + crypto_plugin = 'first_crypto' + s_store1 = self._create_secret_store(name, store_plugin, + crypto_plugin, False) + + count = self.proj_store_repo.get_count_by_secret_store(s_store1.id) + self.assertEqual(0, count) + + # create/set preferred secret store now + self.proj_store_repo.create_or_update_for_project(project1.id, + s_store1.id) + + count = self.proj_store_repo.get_count_by_secret_store(s_store1.id) + self.assertEqual(1, count) + + project2 = self._create_project() + self.proj_store_repo.create_or_update_for_project(project2.id, + s_store1.id) + + count = self.proj_store_repo.get_count_by_secret_store(s_store1.id) + self.assertEqual(2, count) diff --git a/barbican/tests/model/test_models.py b/barbican/tests/model/test_models.py index 1814ebf2..0e1d55e8 100644 --- a/barbican/tests/model/test_models.py +++ b/barbican/tests/model/test_models.py @@ -639,5 +639,118 @@ class WhenCreatingNewProjectQuotas(utils.BaseTestCase): self.assertEqual(106, project_quotas.to_dict_fields()['cas']) + +class WhenCreatingNewSecretStores(utils.BaseTestCase): + def setUp(self): + super(WhenCreatingNewSecretStores, self).setUp() + + def test_new_secret_stores_for_all_input(self): + name = "db backend" + store_plugin = 'store_crypto' + crypto_plugin = 'simple_crypto' + ss = models.SecretStores(name, store_plugin, crypto_plugin, + global_default=True) + + self.assertEqual(store_plugin, ss.store_plugin) + self.assertEqual(crypto_plugin, ss.crypto_plugin) + self.assertEqual(name, ss.name) + self.assertEqual(True, ss.global_default) + self.assertEqual(models.States.ACTIVE, ss.status) + + def test_new_secret_stores_required_input_only(self): + store_plugin = 'store_crypto' + name = "db backend" + ss = models.SecretStores(name, store_plugin) + + self.assertEqual(store_plugin, ss.store_plugin) + self.assertEqual(name, ss.name) + self.assertIsNone(ss.crypto_plugin) + self.assertIsNone(ss.global_default) # False default is not used + self.assertEqual(models.States.ACTIVE, ss.status) + + def test_should_throw_exception_missing_store_plugin(self): + name = "db backend" + self.assertRaises(exception.MissingArgumentError, + models.SecretStores, name, None) + self.assertRaises(exception.MissingArgumentError, + models.SecretStores, name, "") + + def test_should_throw_exception_missing_name(self): + store_plugin = 'store_crypto' + self.assertRaises(exception.MissingArgumentError, + models.SecretStores, None, store_plugin) + self.assertRaises(exception.MissingArgumentError, + models.SecretStores, "", store_plugin) + + def test_secret_stores_check_to_dict_fields(self): + name = "pkcs11 backend" + store_plugin = 'store_crypto' + crypto_plugin = 'p11_crypto' + ss = models.SecretStores(name, store_plugin, crypto_plugin, + global_default=True) + self.assertEqual(store_plugin, + ss.to_dict_fields()['store_plugin']) + self.assertEqual(crypto_plugin, + ss.to_dict_fields()['crypto_plugin']) + self.assertEqual(True, + ss.to_dict_fields()['global_default']) + self.assertEqual(models.States.ACTIVE, + ss.to_dict_fields()['status']) + self.assertEqual(name, ss.to_dict_fields()['name']) + + # check with required input only + ss = models.SecretStores(name, store_plugin) + self.assertEqual(store_plugin, + ss.to_dict_fields()['store_plugin']) + self.assertIsNone(ss.to_dict_fields()['crypto_plugin']) + self.assertIsNone(ss.to_dict_fields()['global_default']) + self.assertEqual(models.States.ACTIVE, + ss.to_dict_fields()['status']) + self.assertEqual(name, ss.to_dict_fields()['name']) + + +class WhenCreatingNewProjectSecretStore(utils.BaseTestCase): + def setUp(self): + super(WhenCreatingNewProjectSecretStore, self).setUp() + + def test_new_project_secret_store(self): + + project_id = 'proj_123456' + name = "db backend" + store_plugin = 'store_crypto' + crypto_plugin = 'simple_crypto' + ss = models.SecretStores(name, store_plugin, crypto_plugin, + global_default=True) + ss.id = "ss_123456" + + project_ss = models.ProjectSecretStore(project_id, ss.id) + self.assertEqual(project_id, project_ss.project_id) + self.assertEqual(ss.id, project_ss.secret_store_id) + self.assertEqual(models.States.ACTIVE, project_ss.status) + + def test_should_throw_exception_missing_project_id(self): + self.assertRaises(exception.MissingArgumentError, + models.ProjectSecretStore, None, "ss_123456") + self.assertRaises(exception.MissingArgumentError, + models.ProjectSecretStore, "", "ss_123456") + + def test_should_throw_exception_missing_secret_store_id(self): + self.assertRaises(exception.MissingArgumentError, + models.ProjectSecretStore, "proj_123456", None) + self.assertRaises(exception.MissingArgumentError, + models.ProjectSecretStore, "proj_123456", "") + + def test_project_secret_store_check_to_dict_fields(self): + project_id = 'proj_123456' + secret_store_id = 'ss_7689012' + project_ss = models.ProjectSecretStore(project_id, secret_store_id) + self.assertEqual(project_id, + project_ss.to_dict_fields()['project_id']) + self.assertEqual(secret_store_id, + project_ss.to_dict_fields()['secret_store_id']) + self.assertEqual(models.States.ACTIVE, + project_ss.to_dict_fields()['status']) + + if __name__ == '__main__': unittest.main() From db01c213aec35057eb7c4aaeab06c5b114d688d6 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Thu, 8 Sep 2016 14:51:06 -0400 Subject: [PATCH 13/20] standardize release note page ordering In order to support automatically updating the release notes when we create stable branches, we want the pages to be in a standard order. This patch updates the order to be reverse chronological, so the most recent notes appear at the top. Change-Id: Ib364dcc8eb31275a31c83b68d7914263b183e393 Signed-off-by: Doug Hellmann --- releasenotes/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 9756fa4d..e6e38eba 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -7,6 +7,6 @@ Contents: .. toctree:: :maxdepth: 1 - liberty unreleased mitaka + liberty From 25a702cda54cb70fc730730c3a510264dd333f2e Mon Sep 17 00:00:00 2001 From: Hu Jie Date: Mon, 29 Aug 2016 08:17:06 +0000 Subject: [PATCH 14/20] typo fix TrivialFix Change-Id: I176e195345e4ec3425a1c1f31b233e8c169d9b17 --- barbican/common/exception.py | 2 +- barbican/model/clean.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/barbican/common/exception.py b/barbican/common/exception.py index 003c3acd..f530f880 100644 --- a/barbican/common/exception.py +++ b/barbican/common/exception.py @@ -279,7 +279,7 @@ class LimitExceeded(BarbicanHTTPException): class ServiceUnavailable(BarbicanException): - message = u._("The request returned 503 Service Unavilable. This " + message = u._("The request returned 503 Service Unavailable. This " "generally occurs on service overload or other transient " "outage.") diff --git a/barbican/model/clean.py b/barbican/model/clean.py index d2e759b8..5f61f239 100644 --- a/barbican/model/clean.py +++ b/barbican/model/clean.py @@ -297,7 +297,7 @@ def soft_delete_expired_secrets(threshold_date): update_count += children_count LOG.info(u._LI("Soft deleted %(update_count)s entries due to secret " "expiration and %(acl_total)s secret acl entries " - "wereremoved from the database") % + "were removed from the database") % {'update_count': update_count, 'acl_total': acl_total}) return update_count + acl_total From 6c814fefbc062b2643b52bb5281743b482f6ea9c Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 13 Sep 2016 10:44:38 +1000 Subject: [PATCH 15/20] Don't inspect oslo.context When creating an oslo.context we inspected the __init__ string to figure out if roles were part of the context. This was added in oslo.context 2.2 and there needed to be compatibility between old and new. Now that requirements specify oslo.context >= 2.9 we don't need this clause any more, and it is causing release issues with older versions of positional. Remove the inspection and rely on newer versions of oslo.context. Closes-Bug: #1620963 Change-Id: I2fcb4f1718ae5e5b50e26d9dee0e0df2f1e6cf72 --- barbican/context.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/barbican/context.py b/barbican/context.py index 264d7f95..a50c4832 100644 --- a/barbican/context.py +++ b/barbican/context.py @@ -13,7 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -import inspect import oslo_context from oslo_policy import policy @@ -29,23 +28,12 @@ class RequestContext(oslo_context.context.RequestContext): accesses the system, as well as additional request information. """ - def __init__(self, roles=None, policy_enforcer=None, project=None, - **kwargs): + def __init__(self, policy_enforcer=None, project=None, **kwargs): # prefer usage of 'project' instead of 'tenant' if project: kwargs['tenant'] = project self.project = project self.policy_enforcer = policy_enforcer or policy.Enforcer(CONF) - - # NOTE(edtubill): oslo_context 2.2.0 now has a roles attribute in - # the RequestContext. This will make sure of backwards compatibility - # with past oslo_context versions. - argspec = inspect.getargspec(super(RequestContext, self).__init__) - if 'roles' in argspec.args: - kwargs['roles'] = roles - else: - self.roles = roles or [] - super(RequestContext, self).__init__(**kwargs) def to_dict(self): From f4141861ef8e69c875bd552b254d4147c6a6cdbb Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Mon, 1 Aug 2016 12:29:59 -0700 Subject: [PATCH 16/20] Changes for multiple backend conf and friendly plugin names (Part 2) Added feature flag to enable/disable multiple backend support. Added friendly plugin name in secretstore and crypto plugins conf. Modified way of defining multiple plugin conf. With multiple backend support, now plugins supported and global default plugin is defined explictly in service configuration. With feature flag on, plugins to be loaded are identified via reading new configuration structure. Change-Id: I3c761c5a441f42c9f54a67d72514e8c1357a2e6a Partially-Implements: blueprint multiple-secret-backend --- barbican/common/config.py | 17 ++ barbican/common/exception.py | 33 ++++ barbican/common/utils.py | 12 ++ barbican/model/models.py | 5 +- barbican/plugin/crypto/crypto.py | 12 ++ barbican/plugin/crypto/manager.py | 29 +++- barbican/plugin/crypto/p11_crypto.py | 6 + barbican/plugin/crypto/simple_crypto.py | 9 +- barbican/plugin/dogtag.py | 9 +- barbican/plugin/interface/secret_store.py | 56 ++++++- barbican/plugin/kmip_secret_store.py | 10 +- barbican/plugin/util/multiple_backends.py | 103 ++++++++++++ barbican/tests/plugin/crypto/test_crypto.py | 3 + .../tests/plugin/crypto/test_p11_crypto.py | 6 + .../plugin/interface/test_secret_store.py | 45 ++++++ barbican/tests/plugin/test_dogtag.py | 6 +- barbican/tests/plugin/test_kmip.py | 6 + .../plugin/util/test_multiple_backends.py | 138 +++++++++++++++++ barbican/tests/utils.py | 146 ++++++++++++++++++ etc/barbican/barbican.conf | 12 ++ 20 files changed, 648 insertions(+), 15 deletions(-) create mode 100644 barbican/plugin/util/multiple_backends.py create mode 100644 barbican/tests/plugin/util/test_multiple_backends.py diff --git a/barbican/common/config.py b/barbican/common/config.py index 3ecf32a0..507be03e 100644 --- a/barbican/common/config.py +++ b/barbican/common/config.py @@ -264,3 +264,20 @@ def set_middleware_defaults(): CONF = new_config() LOG = logging.getLogger(__name__) parse_args(CONF) + +# Adding global scope dict for all different configs created in various +# modules. In barbican, each plugin module creates its own *new* config +# instance so its error prone to share/access config values across modules +# as these module imports introduce a cyclic dependency. To avoid this, each +# plugin can set this dict after its own config instance is created and parsed. +_CONFIGS = {} + + +def set_module_config(name, module_conf): + """Each plugin can set its own conf instance with its group name.""" + _CONFIGS[name] = module_conf + + +def get_module_config(name): + """Get handle to plugin specific config instance by its group name.""" + return _CONFIGS[name] diff --git a/barbican/common/exception.py b/barbican/common/exception.py index 003c3acd..ef559fba 100644 --- a/barbican/common/exception.py +++ b/barbican/common/exception.py @@ -530,3 +530,36 @@ class P11CryptoKeyHandleException(PKCS11Exception): class P11CryptoTokenException(PKCS11Exception): message = u._("No token was found in slot %(slot_id)s") + + +class MultipleSecretStoreLookupFailed(BarbicanException): + """Raised when a plugin lookup suffix is missing during config read.""" + def __init__(self): + msg = u._("Plugin lookup property 'stores_lookup_suffix' is not " + "defined in service configuration") + super(MultipleSecretStoreLookupFailed, self).__init__(msg) + + +class MultipleStoreIncorrectGlobalDefault(BarbicanException): + """Raised when a plugin lookup is missing or failed during config read.""" + def __init__(self, occurence): + msg = None + if occurence > 1: + msg = u._("There are {count} plugins with global default as " + "True in service configuration. Only one plugin can have" + " this as True").format(count=occurence) + else: + msg = u._("There is no plugin defined with global default as True." + " One of plugin must be identified as global default") + + super(MultipleStoreIncorrectGlobalDefault, self).__init__(msg) + + +class MultipleStorePluginValueMissing(BarbicanException): + """Raised when a store plugin value is missing in service configuration.""" + def __init__(self, section_name): + super(MultipleStorePluginValueMissing, self).__init__( + u._("In section '{0}', secret_store_plugin value is missing" + ).format(section_name) + ) + self.section_name = section_name diff --git a/barbican/common/utils.py b/barbican/common/utils.py index a6b35ad6..5de22d4c 100644 --- a/barbican/common/utils.py +++ b/barbican/common/utils.py @@ -36,6 +36,13 @@ CONF = config.CONF # Current API version API_VERSION = 'v1' +# Added here to remove cyclic dependency. +# In barbican.model.models module SecretType.OPAQUE was imported from +# barbican.plugin.interface.secret_store which introduces a cyclic dependency +# if `secret_store` plugin needs to use db model classes. So moving shared +# value to another common python module which is already imported in both. +SECRET_TYPE_OPAQUE = "opaque" + def _do_allow_certain_content_types(func, content_types_list=[]): # Allows you to bypass pecan's content-type restrictions @@ -176,3 +183,8 @@ def get_class_for(module_name, class_name): def generate_uuid(): return str(uuid.uuid4()) + + +def is_multiple_backends_enabled(): + secretstore_conf = config.get_module_config('secretstore') + return secretstore_conf.secretstore.enable_multiple_secret_stores diff --git a/barbican/model/models.py b/barbican/model/models.py index 05a80711..f64b2643 100644 --- a/barbican/model/models.py +++ b/barbican/model/models.py @@ -31,7 +31,6 @@ from sqlalchemy import types as sql_types from barbican.common import exception from barbican.common import utils from barbican import i18n as u -from barbican.plugin.interface import secret_store LOG = utils.getLogger(__name__) BASE = declarative.declarative_base() @@ -277,7 +276,7 @@ class Secret(BASE, SoftDeleteMixIn, ModelBase): name = sa.Column(sa.String(255)) secret_type = sa.Column(sa.String(255), - server_default=secret_store.SecretType.OPAQUE) + server_default=utils.SECRET_TYPE_OPAQUE) expiration = sa.Column(sa.DateTime, default=None) algorithm = sa.Column(sa.String(255)) bit_length = sa.Column(sa.Integer) @@ -317,7 +316,7 @@ class Secret(BASE, SoftDeleteMixIn, ModelBase): self.name = parsed_request.get('name') self.secret_type = parsed_request.get( 'secret_type', - secret_store.SecretType.OPAQUE) + utils.SECRET_TYPE_OPAQUE) expiration = self._iso_to_datetime(parsed_request.get ('expiration')) self.expiration = expiration diff --git a/barbican/plugin/crypto/crypto.py b/barbican/plugin/crypto/crypto.py index e9cfabef..7e77421b 100644 --- a/barbican/plugin/crypto/crypto.py +++ b/barbican/plugin/crypto/crypto.py @@ -240,6 +240,18 @@ class CryptoPluginBase(object): persist the data that is assigned to these DTOs by the plugin. """ + @abc.abstractmethod + def get_plugin_name(self): + """Gets user friendly plugin name. + + This plugin name is expected to be read from config file. + There will be a default defined for plugin name which can be customized + in specific deployment if needed. + + This name needs to be unique across a deployment. + """ + raise NotImplementedError # pragma: no cover + @abc.abstractmethod def encrypt(self, encrypt_dto, kek_meta_dto, project_id): """Encryption handler function diff --git a/barbican/plugin/crypto/manager.py b/barbican/plugin/crypto/manager.py index 692b2d91..cfd7295b 100644 --- a/barbican/plugin/crypto/manager.py +++ b/barbican/plugin/crypto/manager.py @@ -20,6 +20,7 @@ from barbican.common import utils from barbican import i18n as u from barbican.plugin.crypto import crypto from barbican.plugin.interface import secret_store +from barbican.plugin.util import multiple_backends from barbican.plugin.util import utils as plugin_utils @@ -47,6 +48,8 @@ CONF.register_group(crypto_opt_group) CONF.register_opts(crypto_opts, group=crypto_opt_group) config.parse_args(CONF) +config.set_module_config("crypto", CONF) + class _CryptoPluginManager(named.NamedExtensionManager): def __init__(self, conf=CONF, invoke_args=(), invoke_kwargs={}): @@ -57,12 +60,16 @@ class _CryptoPluginManager(named.NamedExtensionManager): initializing a new instance of this class use the PLUGIN_MANAGER at the module level. """ + crypto_conf = config.get_module_config('crypto') + plugin_names = self._get_internal_plugin_names(crypto_conf) + super(_CryptoPluginManager, self).__init__( - conf.crypto.namespace, - conf.crypto.enabled_crypto_plugins, + crypto_conf.crypto.namespace, + plugin_names, invoke_on_load=False, # Defer creating plugins to utility below. invoke_args=invoke_args, - invoke_kwds=invoke_kwargs + invoke_kwds=invoke_kwargs, + name_order=True # extensions sorted as per order of plugin names ) plugin_utils.instantiate_plugins( @@ -111,6 +118,22 @@ class _CryptoPluginManager(named.NamedExtensionManager): return decrypting_plugin + def _get_internal_plugin_names(self, crypto_conf): + """Gets plugin names used for loading via stevedore. + + When multiple secret store support is enabled, then crypto plugin names + are read via updated configuration structure. If not enabled, then it + reads MultiStr property in 'crypto' config section. + """ + + if utils.is_multiple_backends_enabled(): + parsed_stores = multiple_backends.read_multiple_backends_config() + plugin_names = [store.crypto_plugin for store in parsed_stores + if store.crypto_plugin] + else: + plugin_names = crypto_conf.crypto.enabled_crypto_plugins + return plugin_names + def get_manager(): """Return a singleton crypto plugin manager.""" diff --git a/barbican/plugin/crypto/p11_crypto.py b/barbican/plugin/crypto/p11_crypto.py index 6ae88f89..b3dec953 100644 --- a/barbican/plugin/crypto/p11_crypto.py +++ b/barbican/plugin/crypto/p11_crypto.py @@ -69,6 +69,9 @@ p11_crypto_plugin_opts = [ cfg.IntOpt('seed_length', help=u._('Amount of data to read from file for seed'), default=32), + cfg.StrOpt('plugin_name', + help=u._('User friendly plugin name'), + default='PKCS11 HSM'), ] CONF.register_group(p11_crypto_plugin_group) CONF.register_opts(p11_crypto_plugin_opts, group=p11_crypto_plugin_group) @@ -104,6 +107,9 @@ class P11CryptoPlugin(plugin.CryptoPluginBase): self._configure_object_cache() + def get_plugin_name(self): + return self.conf.p11_crypto_plugin.plugin_name + def encrypt(self, encrypt_dto, kek_meta_dto, project_id): return self._call_pkcs11(self._encrypt, encrypt_dto, kek_meta_dto, project_id) diff --git a/barbican/plugin/crypto/simple_crypto.py b/barbican/plugin/crypto/simple_crypto.py index c49d31cf..4377b1fb 100644 --- a/barbican/plugin/crypto/simple_crypto.py +++ b/barbican/plugin/crypto/simple_crypto.py @@ -34,7 +34,10 @@ simple_crypto_plugin_opts = [ cfg.StrOpt('kek', default='dGhpcnR5X3R3b19ieXRlX2tleWJsYWhibGFoYmxhaGg=', help=u._('Key encryption key to be used by Simple Crypto ' - 'Plugin'), secret=True) + 'Plugin'), secret=True), + cfg.StrOpt('plugin_name', + help=u._('User friendly plugin name'), + default='Software Only Crypto'), ] CONF.register_group(simple_crypto_plugin_group) CONF.register_opts(simple_crypto_plugin_opts, group=simple_crypto_plugin_group) @@ -46,11 +49,15 @@ class SimpleCryptoPlugin(c.CryptoPluginBase): def __init__(self, conf=CONF): self.master_kek = conf.simple_crypto_plugin.kek + self.plugin_name = conf.simple_crypto_plugin.plugin_name LOG.warning(u._LW("This plugin is NOT meant for a production " "environment. This is meant just for development " "and testing purposes. Please use another plugin " "for production.")) + def get_plugin_name(self): + return self.plugin_name + def _get_kek(self, kek_meta_dto): if not kek_meta_dto.plugin_meta: raise ValueError(u._('KEK not yet created.')) diff --git a/barbican/plugin/dogtag.py b/barbican/plugin/dogtag.py index 646c0c27..7a8e4554 100644 --- a/barbican/plugin/dogtag.py +++ b/barbican/plugin/dogtag.py @@ -74,7 +74,10 @@ dogtag_plugin_opts = [ default=cm.CA_INFO_DEFAULT_EXPIRATION_DAYS, help=u._('Time in days for CA entries to expire')), cfg.StrOpt('plugin_working_dir', - help=u._('Working directory for Dogtag plugin')) + help=u._('Working directory for Dogtag plugin')), + cfg.StrOpt('plugin_name', + help=u._('User friendly plugin name'), + default='Dogtag KRA'), ] CONF.register_group(dogtag_plugin_group) @@ -203,9 +206,13 @@ class DogtagKRAPlugin(sstore.SecretStoreBase): self.keyclient = kraclient.keys self.keyclient.set_transport_cert(KRA_TRANSPORT_NICK) + self.plugin_name = conf.dogtag_plugin.plugin_name LOG.debug(u._("completed DogtagKRAPlugin init")) + def get_plugin_name(self): + return self.plugin_name + def store_secret(self, secret_dto): """Store a secret in the KRA diff --git a/barbican/plugin/interface/secret_store.py b/barbican/plugin/interface/secret_store.py index 9df6a9b2..727dd16f 100644 --- a/barbican/plugin/interface/secret_store.py +++ b/barbican/plugin/interface/secret_store.py @@ -23,6 +23,7 @@ from barbican.common import config from barbican.common import exception from barbican.common import utils from barbican import i18n as u +from barbican.plugin.util import multiple_backends from barbican.plugin.util import utils as plugin_utils @@ -42,12 +43,24 @@ store_opts = [ cfg.MultiStrOpt('enabled_secretstore_plugins', default=DEFAULT_PLUGINS, help=u._('List of secret store plugins to load.') - ) + ), + cfg.BoolOpt('enable_multiple_secret_stores', + default=False, + help=u._('Flag to enable multiple secret store plugin' + ' backend support. Default is False') + ), + cfg.ListOpt('stores_lookup_suffix', + default=None, + help=u._('List of suffix to use for looking up plugins which ' + 'are supported with multiple backend support.') + ) ] CONF.register_group(store_opt_group) CONF.register_opts(store_opts, group=store_opt_group) config.parse_args(CONF) +config.set_module_config("secretstore", CONF) + class SecretStorePluginNotFound(exception.BarbicanHTTPException): """Raised when no plugins are installed.""" @@ -238,7 +251,7 @@ class SecretType(object): opaque data. Opaque data can be any kind of data. This data type signals to Barbican to just store the information and do not worry about the format or encoding. This is the default type if no type is specified by the user.""" - OPAQUE = "opaque" + OPAQUE = utils.SECRET_TYPE_OPAQUE class KeyAlgorithm(object): @@ -345,6 +358,18 @@ class AsymmetricKeyMetadataDTO(object): @six.add_metaclass(abc.ABCMeta) class SecretStoreBase(object): + @abc.abstractmethod + def get_plugin_name(self): + """Gets user friendly plugin name. + + This plugin name is expected to be read from config file. + There will be a default defined for plugin name which can be customized + in specific deployment if needed. + + This name needs to be unique across a deployment. + """ + raise NotImplementedError # pragma: no cover + @abc.abstractmethod def generate_symmetric_key(self, key_spec): """Generate a new symmetric key and store it. @@ -496,12 +521,16 @@ def _enforce_extensions_configured(plugin_related_function): class SecretStorePluginManager(named.NamedExtensionManager): def __init__(self, conf=CONF, invoke_args=(), invoke_kwargs={}): + ss_conf = config.get_module_config('secretstore') + plugin_names = self._get_internal_plugin_names(ss_conf) + super(SecretStorePluginManager, self).__init__( - conf.secretstore.namespace, - conf.secretstore.enabled_secretstore_plugins, + ss_conf.secretstore.namespace, + plugin_names, invoke_on_load=False, # Defer creating plugins to utility below. invoke_args=invoke_args, - invoke_kwds=invoke_kwargs + invoke_kwds=invoke_kwargs, + name_order=True # extensions sorted as per order of plugin names ) plugin_utils.instantiate_plugins( @@ -574,6 +603,23 @@ class SecretStorePluginManager(named.NamedExtensionManager): return plugin raise SecretStoreSupportedPluginNotFound() + def _get_internal_plugin_names(self, secretstore_conf): + """Gets plugin names used for loading via stevedore. + + When multiple secret store support is enabled, then secret store plugin + names are read via updated configuration structure. If not enabled, + then it reads MultiStr property in 'secretstore' config section. + """ + + if utils.is_multiple_backends_enabled(): + parsed_stores = multiple_backends.read_multiple_backends_config() + plugin_names = [store.store_plugin for store in parsed_stores + if store.store_plugin] + else: + plugin_names = secretstore_conf.secretstore.\ + enabled_secretstore_plugins + return plugin_names + def get_manager(): global _SECRET_STORE diff --git a/barbican/plugin/kmip_secret_store.py b/barbican/plugin/kmip_secret_store.py index e1849261..40a7328f 100644 --- a/barbican/plugin/kmip_secret_store.py +++ b/barbican/plugin/kmip_secret_store.py @@ -77,7 +77,10 @@ kmip_opts = [ cfg.BoolOpt('pkcs1_only', default=False, help=u._('Only support PKCS#1 encoding of asymmetric keys'), - ) + ), + cfg.StrOpt('plugin_name', + help=u._('User friendly plugin name'), + default='KMIP HSM'), ] CONF.register_group(kmip_opt_group) CONF.register_opts(kmip_opts, group=kmip_opt_group) @@ -217,6 +220,8 @@ class KMIPSecretStore(ss.SecretStoreBase): enums.CryptographicAlgorithm.RSA: ss.KeyAlgorithm.RSA } + self.plugin_name = conf.kmip_plugin.plugin_name + if conf.kmip_plugin.keyfile is not None: self._validate_keyfile_permissions(conf.kmip_plugin.keyfile) @@ -252,6 +257,9 @@ class KMIPSecretStore(ss.SecretStoreBase): username=config.username, password=config.password) + def get_plugin_name(self): + return self.plugin_name + def generate_symmetric_key(self, key_spec): """Generate a symmetric key. diff --git a/barbican/plugin/util/multiple_backends.py b/barbican/plugin/util/multiple_backends.py new file mode 100644 index 00000000..2186bd08 --- /dev/null +++ b/barbican/plugin/util/multiple_backends.py @@ -0,0 +1,103 @@ +# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 collections + +from oslo_config import cfg + +from barbican.common import config +from barbican.common import exception +from barbican.common import utils +from barbican import i18n as u + +LOG = utils.getLogger(__name__) + +LOOKUP_PLUGINS_PREFIX = "secretstore:" + + +def read_multiple_backends_config(): + """Reads and validates multiple backend related configuration. + + Multiple backends configuration is read only when multiple secret store + flag is enabled. + Configuration is validated to make sure that section specific to + provided suffix exists in service configuration. Also validated that only + one of section has global_default = True and its not missing. + """ + conf = config.get_module_config('secretstore') + + parsed_stores = None + if utils.is_multiple_backends_enabled(): + suffix_list = conf.secretstore.stores_lookup_suffix + if not suffix_list: + raise exception.MultipleSecretStoreLookupFailed() + + def register_options_dynamically(conf, group_name): + store_opt_group = cfg.OptGroup( + name=group_name, title='Plugins needed for this backend') + store_opts = [ + cfg.StrOpt('secret_store_plugin', + default=None, + help=u._('Internal name used to identify' + 'secretstore_plugin') + ), + cfg.StrOpt('crypto_plugin', + default=None, + help=u._('Internal name used to identify ' + 'crypto_plugin.') + ), + cfg.BoolOpt('global_default', + default=False, + help=u._('Flag to indicate if this plugin is ' + 'global default plugin for deployment. ' + 'Default is False.') + ), + ] + conf.register_group(store_opt_group) + conf.register_opts(store_opts, group=store_opt_group) + + group_names = [] + # construct group names using those suffix and dynamically register + # oslo config options under that group name + for suffix in suffix_list: + group_name = LOOKUP_PLUGINS_PREFIX + suffix + register_options_dynamically(conf, group_name) + group_names.append(group_name) + + store_conf = collections.namedtuple('store_conf', ['store_plugin', + 'crypto_plugin', + 'global_default']) + parsed_stores = [] + global_default_count = 0 + # Section related to group names based of suffix list are always found + # as we are dynamically registering group and its options. + for group_name in group_names: + conf_section = getattr(conf, group_name) + if conf_section.global_default: + global_default_count += 1 + + store_plugin = conf_section.secret_store_plugin + if not store_plugin: + raise exception.MultipleStorePluginValueMissing(conf_section) + + parsed_stores.append(store_conf(store_plugin, + conf_section.crypto_plugin, + conf_section.global_default)) + + if global_default_count != 1: + raise exception.MultipleStoreIncorrectGlobalDefault( + global_default_count) + + return parsed_stores diff --git a/barbican/tests/plugin/crypto/test_crypto.py b/barbican/tests/plugin/crypto/test_crypto.py index f3448d64..af0d1f6b 100644 --- a/barbican/tests/plugin/crypto/test_crypto.py +++ b/barbican/tests/plugin/crypto/test_crypto.py @@ -356,3 +356,6 @@ class WhenTestingSimpleCryptoPlugin(utils.BaseTestCase): response_dto.kek_meta_extended, mock.MagicMock()) self.assertEqual(16, len(key)) + + def test_get_plugin_name(self): + self.assertIsNotNone(self.plugin.get_plugin_name()) diff --git a/barbican/tests/plugin/crypto/test_p11_crypto.py b/barbican/tests/plugin/crypto/test_p11_crypto.py index a9386256..caf12efa 100644 --- a/barbican/tests/plugin/crypto/test_p11_crypto.py +++ b/barbican/tests/plugin/crypto/test_p11_crypto.py @@ -64,6 +64,9 @@ class WhenTestingP11CryptoPlugin(utils.BaseTestCase): self.cfg_mock.p11_crypto_plugin.seed_file = '' self.cfg_mock.p11_crypto_plugin.seed_length = 32 + self.plugin_name = 'Test PKCS11 plugin' + self.cfg_mock.p11_crypto_plugin.plugin_name = self.plugin_name + self.plugin = p11_crypto.P11CryptoPlugin( conf=self.cfg_mock, pkcs11=self.pkcs11 ) @@ -382,3 +385,6 @@ class WhenTestingP11CryptoPlugin(utils.BaseTestCase): self.assertEqual(self.pkcs11.finalize.call_count, 1) self.assertEqual(self.plugin._create_pkcs11.call_count, 1) self.assertEqual(self.plugin._configure_object_cache.call_count, 1) + + def test_get_plugin_name(self): + self.assertEqual(self.plugin_name, self.plugin.get_plugin_name()) diff --git a/barbican/tests/plugin/interface/test_secret_store.py b/barbican/tests/plugin/interface/test_secret_store.py index 9cecbffc..e3b9a256 100644 --- a/barbican/tests/plugin/interface/test_secret_store.py +++ b/barbican/tests/plugin/interface/test_secret_store.py @@ -16,7 +16,11 @@ import mock from barbican.common import utils as common_utils +from barbican.plugin.crypto import crypto +from barbican.plugin.crypto import manager as cm +from barbican.plugin.crypto import p11_crypto from barbican.plugin.interface import secret_store as str +from barbican.plugin import store_crypto from barbican.tests import utils @@ -27,6 +31,9 @@ class TestSecretStore(str.SecretStoreBase): super(TestSecretStore, self).__init__() self.alg_list = supported_alg_list + def get_plugin_name(self): + raise NotImplementedError # pragma: no cover + def generate_symmetric_key(self, key_spec): raise NotImplementedError # pragma: no cover @@ -59,6 +66,9 @@ class TestSecretStoreWithTransportKey(str.SecretStoreBase): super(TestSecretStoreWithTransportKey, self).__init__() self.alg_list = supported_alg_list + def get_plugin_name(self): + raise NotImplementedError # pragma: no cover + def generate_symmetric_key(self, key_spec): raise NotImplementedError # pragma: no cover @@ -243,3 +253,38 @@ class WhenTestingSecretStorePluginManager(utils.BaseTestCase): self.manager.get_plugin_store( key_spec=keySpec, transport_key_needed=True)) + + +class TestSecretStorePluginManagerMultipleBackend( + utils.MultipleBackendsTestCase): + + def test_plugin_created_as_per_mulitple_backend_conf(self): + """Check plugins are created as per multiple backend conf + + """ + + store_plugin_names = ['store_crypto', 'kmip_plugin', 'store_crypto'] + crypto_plugin_names = ['p11_crypto', '', 'simple_crypto'] + + self.init_via_conf_file(store_plugin_names, + crypto_plugin_names, enabled=True) + + with mock.patch('barbican.plugin.crypto.p11_crypto.P11CryptoPlugin.' + '_create_pkcs11') as m_pkcs11, \ + mock.patch('kmip.pie.client.ProxyKmipClient') as m_kmip: + + manager = str.SecretStorePluginManager() + + # check pkcs11 and kmip plugin instantiation call is invoked + m_pkcs11.called_once_with(mock.ANY, mock.ANY) + m_kmip.called_once_with(mock.ANY) + # check store crypto adapter is matched as its defined first. + keySpec = str.KeySpec(str.KeyAlgorithm.AES, 128) + plugin_found = manager.get_plugin_store(keySpec) + self.assertIsInstance(plugin_found, + store_crypto.StoreCryptoAdapterPlugin) + + # check pkcs11 crypto is matched as its defined first. + crypto_plugin = cm.get_manager().get_plugin_store_generate( + crypto.PluginSupportTypes.ENCRYPT_DECRYPT) + self.assertIsInstance(crypto_plugin, p11_crypto.P11CryptoPlugin) diff --git a/barbican/tests/plugin/test_dogtag.py b/barbican/tests/plugin/test_dogtag.py index ec4a0584..ed9462c9 100644 --- a/barbican/tests/plugin/test_dogtag.py +++ b/barbican/tests/plugin/test_dogtag.py @@ -52,9 +52,10 @@ class WhenTestingDogtagKRAPlugin(utils.BaseTestCase): # create nss db for test only self.nss_dir = tempfile.mkdtemp() + self.plugin_name = "Test Dogtag KRA plugin" self.cfg_mock = mock.MagicMock(name='config mock') self.cfg_mock.dogtag_plugin = mock.MagicMock( - nss_db_path=self.nss_dir) + nss_db_path=self.nss_dir, plugin_name=self.plugin_name) self.plugin = dogtag_import.DogtagKRAPlugin(self.cfg_mock) self.plugin.keyclient = self.keyclient_mock @@ -63,6 +64,9 @@ class WhenTestingDogtagKRAPlugin(utils.BaseTestCase): self.patcher.stop() os.rmdir(self.nss_dir) + def test_get_plugin_name(self): + self.assertEqual(self.plugin_name, self.plugin.get_plugin_name()) + def test_generate_symmetric_key(self): key_spec = sstore.KeySpec(sstore.KeyAlgorithm.AES, 128) self.plugin.generate_symmetric_key(key_spec) diff --git a/barbican/tests/plugin/test_kmip.py b/barbican/tests/plugin/test_kmip.py index 4564240a..fe5abbab 100644 --- a/barbican/tests/plugin/test_kmip.py +++ b/barbican/tests/plugin/test_kmip.py @@ -920,3 +920,9 @@ class WhenTestingKMIPSecretStore(utils.BaseTestCase): CONF.kmip_plugin.keyfile = '/some/path' kss.KMIPSecretStore(CONF) self.assertEqual(1, len(m.mock_calls)) + + def test_get_plugin_name(self): + CONF = kss.CONF + CONF.kmip_plugin.plugin_name = "Test KMIP Plugin" + secret_store = kss.KMIPSecretStore(CONF) + self.assertEqual("Test KMIP Plugin", secret_store.get_plugin_name()) diff --git a/barbican/tests/plugin/util/test_multiple_backends.py b/barbican/tests/plugin/util/test_multiple_backends.py new file mode 100644 index 00000000..c05696d7 --- /dev/null +++ b/barbican/tests/plugin/util/test_multiple_backends.py @@ -0,0 +1,138 @@ +# (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# +# 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 collections +import mock + +from barbican.common import config +from barbican.common import exception +from barbican.plugin.util import multiple_backends +from barbican.tests import utils as test_utils + + +class MockedManager(object): + + NAME_PREFIX = "friendly_" + + def __init__(self, names): + ExtTuple = collections.namedtuple('ExtTuple', ['name', 'obj']) + self.extensions = [] + for name in names: + m = mock.MagicMock() + m.get_plugin_name.return_value = self.NAME_PREFIX + name + new_extension = ExtTuple(name, m) + self.extensions.append(new_extension) + + +class WhenReadingMultipleBackendsConfig(test_utils.MultipleBackendsTestCase): + + def setUp(self): + super(WhenReadingMultipleBackendsConfig, self).setUp() + + def test_successful_conf_read(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True, + global_default_index=1) + + stores = multiple_backends.read_multiple_backends_config() + + self.assertEqual(len(ss_plugins), len(stores)) + self.assertEqual('ss_p1', stores[0].store_plugin) + self.assertEqual('cr_p1', stores[0].crypto_plugin) + self.assertEqual(False, stores[0].global_default) + self.assertEqual('ss_p2', stores[1].store_plugin) + self.assertEqual('cr_p2', stores[1].crypto_plugin) + self.assertEqual(True, stores[1].global_default) + self.assertEqual('ss_p3', stores[2].store_plugin) + self.assertEqual('cr_p3', stores[2].crypto_plugin) + self.assertEqual(False, stores[2].global_default) + + def test_fail_when_store_plugin_name_missing(self): + ss_plugins = ['ss_p1', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + + self.assertRaises(exception.MultipleStorePluginValueMissing, + multiple_backends.read_multiple_backends_config) + + def test_fail_when_store_plugin_name_is_blank(self): + ss_plugins = ['ss_p1', '', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + + self.assertRaises(exception.MultipleStorePluginValueMissing, + multiple_backends.read_multiple_backends_config) + + def test_successful_conf_read_when_crypto_plugin_name_is_missing(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + + stores = multiple_backends.read_multiple_backends_config() + self.assertEqual(len(ss_plugins), len(stores)) + + def test_conf_read_when_multiple_plugin_disabled(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=False) + + stores = multiple_backends.read_multiple_backends_config() + self.assertIsNone(stores) + + def test_successful_conf_read_when_crypto_plugin_name_is_blank(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', '', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + + stores = multiple_backends.read_multiple_backends_config() + self.assertEqual(len(ss_plugins), len(stores)) + + def test_fail_when_global_default_not_specified(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True, + global_default_index=-1) + + self.assertRaises(exception.MultipleStoreIncorrectGlobalDefault, + multiple_backends.read_multiple_backends_config) + + def test_fail_when_stores_lookup_suffix_missing_when_enabled(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True, + global_default_index=0) + + conf = config.get_module_config('secretstore') + conf.set_override("stores_lookup_suffix", [], group='secretstore', + enforce_type=True) + self.assertRaises(exception.MultipleSecretStoreLookupFailed, + multiple_backends.read_multiple_backends_config) + + def test_fail_when_secretstore_section_missing(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True, + global_default_index=-1) + ss_conf = config.get_module_config('secretstore') + + existing_value = ss_conf.secretstore.stores_lookup_suffix + existing_value.append('unknown_section') + + ss_conf.set_override('stores_lookup_suffix', existing_value, + 'secretstore') + + self.assertRaises(exception.MultipleStorePluginValueMissing, + multiple_backends.read_multiple_backends_config) diff --git a/barbican/tests/utils.py b/barbican/tests/utils.py index c0701c42..0e4dc048 100644 --- a/barbican/tests/utils.py +++ b/barbican/tests/utils.py @@ -22,7 +22,10 @@ import types import uuid import mock +from oslo_config import cfg import oslotest.base as oslotest +from oslotest import createfile + import six from six.moves.urllib import parse import webtest @@ -30,7 +33,14 @@ import webtest from OpenSSL import crypto from barbican.api import app +from barbican.common import config import barbican.context +from barbican.model import repositories +from barbican.plugin.crypto import manager as cm +from barbican.plugin.crypto import p11_crypto + +from barbican.plugin.interface import secret_store +from barbican.plugin import kmip_secret_store as kss from barbican.tests import database_utils @@ -400,6 +410,142 @@ def parameterized_dataset(build_data): return decorator +def setup_oslo_config_conf(testcase, content, conf_instance=None): + + conf_file_fixture = testcase.useFixture( + createfile.CreateFileWithContent('barbican', content)) + if conf_instance is None: + conf_instance = cfg.CONF + conf_instance([], project="barbican", + default_config_files=[conf_file_fixture.path]) + + testcase.addCleanup(conf_instance.reset) + + +def setup_multiple_secret_store_plugins_conf(testcase, store_plugin_names, + crypto_plugin_names, + global_default_index, + conf_instance=None, + multiple_support_enabled=None): + """Sets multiple secret store support conf as oslo conf file. + + Generating file based conf based on input store and crypto plugin names + provided as list. Index specified in argument is used to mark that specific + secret store as global_default = True. + + Input lists are + 'store_plugins': ['store_crypto', 'kmip_plugin', 'store_crypto'], + 'crypto_plugins': ['simple_crypto', '', 'p11_crypto'], + + Sample output conf file generated is + + [secretstore] + enable_multiple_secret_stores = True + stores_lookup_suffix = plugin_0, plugin_1, plugin_2 + + [secretstore:plugin_0] + secret_store_plugin = store_crypto + crypto_plugin = simple_crypto + global_default = True + + [secretstore:plugin_1] + secret_store_plugin = kmip_plugin + + [secretstore:plugin_2] + secret_store_plugin = store_crypto + crypto_plugin = p11_crypto + + """ + + def _get_conf_line(name, value, section=None): + out_line = "\n[{0}]\n".format(section) if section else "" + out_line += "{0} = {1}\n".format(name, value) if name else "" + return out_line + + if multiple_support_enabled is None: + multiple_support_enabled = True + + conf_content = "" + + if store_plugin_names is not None: + + if len(store_plugin_names) < len(crypto_plugin_names): + max_count = len(crypto_plugin_names) + else: + max_count = len(store_plugin_names) + + lookup_names = ['plugin_{0}'.format(indx) for indx in range(max_count)] + section_names = ['secretstore:{0}'.format(lname) for lname in + lookup_names] + lookup_str = ", ".join(lookup_names) + + conf_content = _get_conf_line('enable_multiple_secret_stores', + multiple_support_enabled, + section='secretstore') + conf_content += _get_conf_line('stores_lookup_suffix', + lookup_str, section=None) + + for indx, section_name in enumerate(section_names): + if indx < len(store_plugin_names): + store_plugin = store_plugin_names[indx] + conf_content += _get_conf_line('secret_store_plugin', + store_plugin, + section=section_name) + else: + conf_content += _get_conf_line(None, None, + section=section_name) + if indx < len(crypto_plugin_names): + crypto_plugin = crypto_plugin_names[indx] + conf_content += _get_conf_line('crypto_plugin', crypto_plugin, + section=None) + if indx == global_default_index: + conf_content += _get_conf_line('global_default', 'True', + section=None) + + setup_oslo_config_conf(testcase, conf_content, conf_instance) + + +class MultipleBackendsTestCase(database_utils.RepositoryTestCase): + + def setUp(self): + super(MultipleBackendsTestCase, self).setUp() + + def _mock_plugin_settings(self): + + kmip_conf = kss.CONF + kmip_conf.kmip_plugin.username = "sample_username" + kmip_conf.kmip_plugin.password = "sample_password" + kmip_conf.kmip_plugin.keyfile = None + kmip_conf.kmip_plugin.pkcs1_only = False + + pkcs11_conf = p11_crypto.CONF + pkcs11_conf.p11_crypto_plugin.library_path = "/tmp" # any dummy path + + def init_via_conf_file(self, store_plugin_names, crypto_plugin_names, + enabled=True, global_default_index=0): + secretstore_conf = config.get_module_config('secretstore') + + setup_multiple_secret_store_plugins_conf( + self, store_plugin_names=store_plugin_names, + crypto_plugin_names=crypto_plugin_names, + global_default_index=global_default_index, + conf_instance=secretstore_conf, + multiple_support_enabled=enabled) + + # clear globals if already set in previous tests + secret_store._SECRET_STORE = None # clear secret store manager + cm._PLUGIN_MANAGER = None # clear crypto manager + self._mock_plugin_settings() + + def _get_secret_store_entry(self, store_plugin, crypto_plugin): + all_ss = repositories.get_secret_stores_repository().get_all() + for ss in all_ss: + if (ss.store_plugin == store_plugin and + ss.crypto_plugin == crypto_plugin): + return ss + return None + + def create_timestamp_w_tz_and_offset(timezone=None, days=0, hours=0, minutes=0, seconds=0): """Creates a timestamp with a timezone and offset in days diff --git a/etc/barbican/barbican.conf b/etc/barbican/barbican.conf index db4ded48..94cc50b7 100644 --- a/etc/barbican/barbican.conf +++ b/etc/barbican/barbican.conf @@ -263,6 +263,9 @@ enabled_crypto_plugins = simple_crypto # the kek should be a 32-byte value which is base64 encoded kek = 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=' +# User friendly plugin name +# plugin_name = 'Software Only Crypto' + [dogtag_plugin] pem_path = '/etc/barbican/kra_admin_cert.pem' dogtag_host = localhost @@ -274,6 +277,9 @@ simple_cmc_profile = 'caOtherCert' ca_expiration_time = 1 plugin_working_dir = '/etc/barbican/dogtag' +# User friendly plugin name +# plugin_name = 'Dogtag KRA' + [p11_crypto_plugin] # Path to vendor PKCS11 library @@ -297,6 +303,9 @@ hmac_label = 'my_hmac_label' # Max number of items in pkek cache # pkek_cache_limit = 100 +# User friendly plugin name +# plugin_name = 'PKCS11 HSM' + # ================== KMIP plugin ===================== [kmip_plugin] @@ -308,6 +317,9 @@ keyfile = '/path/to/certs/cert.key' certfile = '/path/to/certs/cert.crt' ca_certs = '/path/to/certs/LocalCA.crt' +# User friendly plugin name +# plugin_name = 'KMIP HSM' + # ================= Certificate plugin =================== [certificate] From b05c4b6f86181421c310df1de9a49b9319f095f8 Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Thu, 18 Aug 2016 10:27:55 -0700 Subject: [PATCH 17/20] Central logic to sync secret store data with conf data (Part 3) Modified logic to retrieve applicable plugins when preferred project is set. This is part 2 of central logic changes needed for multiple backend support work. Change-Id: I15eb7ca88611a12677227647e3026822e67413b0 Partially-Implements: blueprint multiple-secret-backend --- barbican/common/exception.py | 26 +- barbican/plugin/crypto/manager.py | 8 +- barbican/plugin/interface/secret_store.py | 25 +- barbican/plugin/resources.py | 17 +- barbican/plugin/store_crypto.py | 9 +- barbican/plugin/util/multiple_backends.py | 191 ++++++++ .../plugin/interface/test_secret_store.py | 32 +- .../plugin/util/test_multiple_backends.py | 438 +++++++++++++++++- barbican/tests/utils.py | 3 + 9 files changed, 723 insertions(+), 26 deletions(-) diff --git a/barbican/common/exception.py b/barbican/common/exception.py index ef559fba..373c3695 100644 --- a/barbican/common/exception.py +++ b/barbican/common/exception.py @@ -532,6 +532,30 @@ class P11CryptoTokenException(PKCS11Exception): message = u._("No token was found in slot %(slot_id)s") +class MultipleStorePreferredPluginMissing(BarbicanException): + """Raised when a preferred plugin is missing in service configuration.""" + def __init__(self, store_name): + super(MultipleStorePreferredPluginMissing, self).__init__( + u._("Preferred Secret Store plugin '{store_name}' is not " + "currently set in service configuration. This is probably a " + "server misconfiguration.").format( + store_name=store_name) + ) + self.store_name = store_name + + +class MultipleStorePluginStillInUse(BarbicanException): + """Raised when a used plugin is missing in service configuration.""" + def __init__(self, store_name): + super(MultipleStorePluginStillInUse, self).__init__( + u._("Secret Store plugin '{store_name}' is still in use and can " + "not be removed. Its missing in service configuration. This is" + " probably a server misconfiguration.").format( + store_name=store_name) + ) + self.store_name = store_name + + class MultipleSecretStoreLookupFailed(BarbicanException): """Raised when a plugin lookup suffix is missing during config read.""" def __init__(self): @@ -541,7 +565,7 @@ class MultipleSecretStoreLookupFailed(BarbicanException): class MultipleStoreIncorrectGlobalDefault(BarbicanException): - """Raised when a plugin lookup is missing or failed during config read.""" + """Raised when a global default for only one plugin is not set to True.""" def __init__(self, occurence): msg = None if occurence > 1: diff --git a/barbican/plugin/crypto/manager.py b/barbican/plugin/crypto/manager.py index cfd7295b..f3e2a74c 100644 --- a/barbican/plugin/crypto/manager.py +++ b/barbican/plugin/crypto/manager.py @@ -76,14 +76,15 @@ class _CryptoPluginManager(named.NamedExtensionManager): self, invoke_args, invoke_kwargs) def get_plugin_store_generate(self, type_needed, algorithm=None, - bit_length=None, mode=None): + bit_length=None, mode=None, project_id=None): """Gets a secret store or generate plugin that supports provided type. :param type_needed: PluginSupportTypes that contains details on the type of plugin required :returns: CryptoPluginBase plugin implementation """ - active_plugins = plugin_utils.get_active_plugins(self) + active_plugins = multiple_backends.get_applicable_crypto_plugins( + self, project_id=project_id, existing_plugin_name=None) if not active_plugins: raise crypto.CryptoPluginNotFound() @@ -125,7 +126,8 @@ class _CryptoPluginManager(named.NamedExtensionManager): are read via updated configuration structure. If not enabled, then it reads MultiStr property in 'crypto' config section. """ - + # to cache default global secret store value on first use + self.global_default_store_dict = None if utils.is_multiple_backends_enabled(): parsed_stores = multiple_backends.read_multiple_backends_config() plugin_names = [store.crypto_plugin for store in parsed_stores diff --git a/barbican/plugin/interface/secret_store.py b/barbican/plugin/interface/secret_store.py index 727dd16f..34e56d35 100644 --- a/barbican/plugin/interface/secret_store.py +++ b/barbican/plugin/interface/secret_store.py @@ -533,12 +533,13 @@ class SecretStorePluginManager(named.NamedExtensionManager): name_order=True # extensions sorted as per order of plugin names ) - plugin_utils.instantiate_plugins( - self, invoke_args, invoke_kwargs) + plugin_utils.instantiate_plugins(self, invoke_args, invoke_kwargs) + + multiple_backends.sync_secret_stores(self) @_enforce_extensions_configured def get_plugin_store(self, key_spec, plugin_name=None, - transport_key_needed=False): + transport_key_needed=False, project_id=None): """Gets a secret store plugin. :param: plugin_name: set to plugin_name to get specific plugin @@ -547,7 +548,8 @@ class SecretStorePluginManager(named.NamedExtensionManager): key is required. :returns: SecretStoreBase plugin implementation """ - active_plugins = plugin_utils.get_active_plugins(self) + active_plugins = multiple_backends.get_applicable_store_plugins( + self, project_id=project_id, existing_plugin_name=plugin_name) if plugin_name is not None: for plugin in active_plugins: @@ -590,7 +592,7 @@ class SecretStorePluginManager(named.NamedExtensionManager): raise StorePluginNotAvailableOrMisconfigured(plugin_name) @_enforce_extensions_configured - def get_plugin_generate(self, key_spec): + def get_plugin_generate(self, key_spec, project_id=None): """Gets a secret generate plugin. :param key_spec: KeySpec that contains details on the type of key to @@ -598,7 +600,10 @@ class SecretStorePluginManager(named.NamedExtensionManager): :returns: SecretStoreBase plugin implementation """ - for plugin in plugin_utils.get_active_plugins(self): + active_plugins = multiple_backends.get_applicable_store_plugins( + self, project_id=project_id, existing_plugin_name=None) + + for plugin in active_plugins: if plugin.generate_supports(key_spec): return plugin raise SecretStoreSupportedPluginNotFound() @@ -610,10 +615,12 @@ class SecretStorePluginManager(named.NamedExtensionManager): names are read via updated configuration structure. If not enabled, then it reads MultiStr property in 'secretstore' config section. """ - + # to cache default global secret store value on first use + self.global_default_store_dict = None if utils.is_multiple_backends_enabled(): - parsed_stores = multiple_backends.read_multiple_backends_config() - plugin_names = [store.store_plugin for store in parsed_stores + self.parsed_stores = multiple_backends.\ + read_multiple_backends_config() + plugin_names = [store.store_plugin for store in self.parsed_stores if store.store_plugin] else: plugin_names = secretstore_conf.secretstore.\ diff --git a/barbican/plugin/resources.py b/barbican/plugin/resources.py index 4192bf41..785a9765 100644 --- a/barbican/plugin/resources.py +++ b/barbican/plugin/resources.py @@ -20,14 +20,15 @@ from barbican.plugin import store_crypto from barbican.plugin.util import translations as tr -def _get_transport_key_model(key_spec, transport_key_needed): +def _get_transport_key_model(key_spec, transport_key_needed, project_id): key_model = None if transport_key_needed: # get_plugin_store() will throw an exception if no suitable # plugin with transport key is found plugin_manager = secret_store.get_manager() store_plugin = plugin_manager.get_plugin_store( - key_spec=key_spec, transport_key_needed=True) + key_spec=key_spec, transport_key_needed=True, + project_id=project_id) plugin_name = utils.generate_fullname_for(store_plugin) key_repo = repos.get_transport_key_repository() @@ -80,7 +81,8 @@ def store_secret(unencrypted_raw, content_type_raw, content_encoding, # leave. A subsequent call to this method should provide both the Secret # entity created here *and* the secret data to store into it. if not unencrypted_raw: - key_model = _get_transport_key_model(key_spec, transport_key_needed) + key_model = _get_transport_key_model(key_spec, transport_key_needed, + project_id=project_model.id) _save_secret_in_repo(secret_model, project_model) return secret_model, key_model @@ -94,7 +96,8 @@ def store_secret(unencrypted_raw, content_type_raw, content_encoding, plugin_manager = secret_store.get_manager() store_plugin = plugin_manager.get_plugin_store(key_spec=key_spec, - plugin_name=plugin_name) + plugin_name=plugin_name, + project_id=project_model.id) secret_dto = secret_store.SecretDTO(type=secret_model.secret_type, secret=unencrypted, @@ -166,7 +169,8 @@ def generate_secret(spec, content_type, project_model): mode=spec.get('mode')) plugin_manager = secret_store.get_manager() - generate_plugin = plugin_manager.get_plugin_generate(key_spec) + generate_plugin = plugin_manager.get_plugin_generate( + key_spec, project_id=project_model.id) # Create secret model to eventually save metadata to. secret_model = models.Secret(spec) @@ -192,7 +196,8 @@ def generate_asymmetric_secret(spec, content_type, project_model): passphrase=spec.get('passphrase')) plugin_manager = secret_store.get_manager() - generate_plugin = plugin_manager.get_plugin_generate(key_spec) + generate_plugin = plugin_manager.get_plugin_generate( + key_spec, project_id=project_model.id) # Create secret models to eventually save metadata to. private_secret_model = models.Secret(spec) diff --git a/barbican/plugin/store_crypto.py b/barbican/plugin/store_crypto.py index 591e9427..5efcc2a3 100644 --- a/barbican/plugin/store_crypto.py +++ b/barbican/plugin/store_crypto.py @@ -74,7 +74,8 @@ class StoreCryptoAdapterPlugin(object): # Find HSM-style 'crypto' plugin. encrypting_plugin = manager.get_manager().get_plugin_store_generate( - crypto.PluginSupportTypes.ENCRYPT_DECRYPT + crypto.PluginSupportTypes.ENCRYPT_DECRYPT, + project_id=context.project_model.id ) # Find or create a key encryption key metadata. @@ -163,7 +164,8 @@ class StoreCryptoAdapterPlugin(object): plugin_type, key_spec.alg, key_spec.bit_length, - key_spec.mode) + key_spec.mode, + project_id=context.project_model.id) # Find or create a key encryption key metadata. kek_datum_model, kek_meta_dto = _find_or_create_kek_objects( @@ -197,7 +199,8 @@ class StoreCryptoAdapterPlugin(object): raise sstore.SecretAlgorithmNotSupportedException(key_spec.alg) generating_plugin = manager.get_manager().get_plugin_store_generate( - plugin_type, key_spec.alg, key_spec.bit_length, None) + plugin_type, key_spec.alg, key_spec.bit_length, + project_id=context.project_model.id) # Find or create a key encryption key metadata. kek_datum_model, kek_meta_dto = _find_or_create_kek_objects( diff --git a/barbican/plugin/util/multiple_backends.py b/barbican/plugin/util/multiple_backends.py index 2186bd08..d45cbafe 100644 --- a/barbican/plugin/util/multiple_backends.py +++ b/barbican/plugin/util/multiple_backends.py @@ -21,6 +21,8 @@ from barbican.common import config from barbican.common import exception from barbican.common import utils from barbican import i18n as u +from barbican.model import models as db_models +from barbican.model import repositories as db_repos LOG = utils.getLogger(__name__) @@ -101,3 +103,192 @@ def read_multiple_backends_config(): global_default_count) return parsed_stores + + +def sync_secret_stores(secretstore_manager, crypto_manager=None): + """Synchronize secret store plugin names between service conf and database + + This method reads secret and crypto store plugin name from service + configuration and then synchronizes corresponding data maintained in + database SecretStores table. + + Any new plugin name(s) added in service configuration is added as a new + entry in SecretStores table. If global_default value is changed for + existing plugins, then global_default flag is updated to reflect that + change in database. If plugin name is removed from service configuration, + then removal is possible as long as respective plugin names are NOT set as + preferred secret store for a project. If it is used and plugin name is + removed, then error is raised. This logic is intended to be invoked at + server startup so any error raised here will result in critical failure. + """ + if not utils.is_multiple_backends_enabled(): + return + + # doing local import to avoid circular dependency between manager and + # current utils module + from barbican.plugin.crypto import manager as cm + + secret_stores_repo = db_repos.get_secret_stores_repository() + proj_store_repo = db_repos.get_project_secret_store_repository() + if crypto_manager is None: + crypto_manager = cm.get_manager() + + def get_friendly_name_dict(ext_manager): + """Returns dict of plugin internal name and friendly name entries.""" + names_dict = {} + for ext in ext_manager.extensions: + if ext.obj and hasattr(ext.obj, 'get_plugin_name'): + names_dict[ext.name] = ext.obj.get_plugin_name() + return names_dict + + ss_friendly_names = get_friendly_name_dict(secretstore_manager) + crypto_friendly_names = get_friendly_name_dict(crypto_manager) + # get existing secret stores data from database + db_stores = secret_stores_repo.get_all() + + # read secret store data from service configuration + conf_stores = [] + for parsed_store in secretstore_manager.parsed_stores: + crypto_plugin = parsed_store.crypto_plugin + if not crypto_plugin: + crypto_plugin = None + + if crypto_plugin: + friendly_name = crypto_friendly_names.get(crypto_plugin) + else: + friendly_name = ss_friendly_names.get(parsed_store.store_plugin) + + conf_stores.append(db_models.SecretStores( + name=friendly_name, store_plugin=parsed_store.store_plugin, + crypto_plugin=crypto_plugin, + global_default=parsed_store.global_default)) + + if db_stores: + def fn_match(lh_store, rh_store): + return (lh_store.store_plugin == rh_store.store_plugin and + lh_store.crypto_plugin == rh_store.crypto_plugin) + + for conf_store in conf_stores: + # find existing db entry for plugin using conf based plugin names + db_store_match = next((db_store for db_store in db_stores if + fn_match(conf_store, db_store)), None) + if db_store_match: + # update existing db entry if global default is changed now + if db_store_match.global_default != conf_store.global_default: + db_store_match.global_default = conf_store.global_default + # persist flag change. + db_store_match.save() + # remove matches store from local list after processing + db_stores.remove(db_store_match) + else: # new conf entry as no match found in existing entries + secret_stores_repo.create_from(conf_store) + + # entries still present in db list are no longer configured in service + # configuration, so try to remove them provided there is no project + # is using it as preferred secret store. + for db_store in db_stores: + if proj_store_repo.get_count_by_secret_store(db_store.id) == 0: + secret_stores_repo.delete_entity_by_id(db_store.id, None) + else: + raise exception.MultipleStorePluginStillInUse(db_store.name) + else: # initial setup case when there is no secret stores data in db + for conf_store in conf_stores: + secret_stores_repo.create_from(conf_store) + + +def get_global_default_secret_store(): + secret_store_repo = db_repos.get_secret_stores_repository() + + default_ss = None + for secret_store in secret_store_repo.get_all(): + if secret_store.global_default: + default_ss = secret_store + break + return default_ss + + +def get_applicable_crypto_plugins(manager, project_id, existing_plugin_name): + """Get list of crypto plugins available for use. + + :param: manager instance of crypto manager + :param: project_id project to identify preferred store if set + :param: existing_plugin_name full plugin name. If a secret has an existing + plugin defined, then we do not care if any preferred plugins have + been defined. We will return all configured plugins as if multiple + plugin support was not enabled. Subsequent code in the caller will + select the plugin by name. + + When multiple backends support is enabled: + It return project preferred plugin as list when it is setup earlier. + If project preferred plugin is not set, then it uses plugin from default + secret store. + Plugin name is 'crypto_plugin' field value on identified secret store data. + It returns matched plugin as list to match existing functionality. + + When multiple backends support is NOT enabled: + In this case, it just returns list of all active plugins which is + existing functionality before support for multiple backends is added. + """ + return _get_applicable_plugins_for_type(manager, project_id, + existing_plugin_name, + 'crypto_plugin') + + +def get_applicable_store_plugins(manager, project_id, existing_plugin_name): + """Get list of secret store plugins available for use. + + :param: manager instance of secret store manager + :param: project_id project to identify preferred store if set + :param: existing_plugin_name full plugin name. If a secret has an existing + plugin defined, then we do not care if any preferred plugins have + been defined. We will return all configured plugins as if multiple + plugin support was not enabled. Subsequent code in the caller will + select the plugin by name. + + When multiple backends support is enabled: + It return project preferred plugin as list when it is setup earlier. + If project preferred plugin is not set, then it uses plugin from default + secret store. + Plugin name is 'store_plugin' field value on identified secret store data. + It returns matched plugin as list to match existing functionality. + + When multiple backends support is NOT enabled: + In this case, it just returns list of all active plugins which is + existing functionality before support for multiple backends is added. + """ + return _get_applicable_plugins_for_type(manager, project_id, + existing_plugin_name, + 'store_plugin') + + +def _get_applicable_plugins_for_type(manager, project_id, existing_plugin_name, + plugin_type_field): + + plugins = [] + plugin_dict = {ext.name: ext.obj for ext in manager.extensions if ext.obj} + if utils.is_multiple_backends_enabled() and existing_plugin_name is None: + proj_store_repo = db_repos.get_project_secret_store_repository() + plugin_store = proj_store_repo.get_secret_store_for_project( + project_id, None, suppress_exception=True) + + # If project specific store is not set, then use global default one. + if not plugin_store: + if manager.global_default_store_dict is None: + # Need to cache data as dict instead of db object to be usable + # across various request sqlalchemy sessions + store_dict = get_global_default_secret_store().to_dict_fields() + manager.global_default_store_dict = store_dict + secret_store_data = manager.global_default_store_dict + else: + secret_store_data = plugin_store.secret_store.to_dict_fields() + + applicable_plugin_name = secret_store_data[plugin_type_field] + if applicable_plugin_name in plugin_dict: + plugins = [plugin_dict.get(applicable_plugin_name)] + elif applicable_plugin_name: # applicable_plugin_name has value + raise exception.MultipleStorePreferredPluginMissing( + applicable_plugin_name) + else: + plugins = plugin_dict.values() + + return plugins diff --git a/barbican/tests/plugin/interface/test_secret_store.py b/barbican/tests/plugin/interface/test_secret_store.py index e3b9a256..5637f702 100644 --- a/barbican/tests/plugin/interface/test_secret_store.py +++ b/barbican/tests/plugin/interface/test_secret_store.py @@ -20,6 +20,7 @@ from barbican.plugin.crypto import crypto from barbican.plugin.crypto import manager as cm from barbican.plugin.crypto import p11_crypto from barbican.plugin.interface import secret_store as str +from barbican.plugin import kmip_secret_store as kss from barbican.plugin import store_crypto from barbican.tests import utils @@ -259,9 +260,7 @@ class TestSecretStorePluginManagerMultipleBackend( utils.MultipleBackendsTestCase): def test_plugin_created_as_per_mulitple_backend_conf(self): - """Check plugins are created as per multiple backend conf - - """ + """Check plugins are created as per multiple backend conf""" store_plugin_names = ['store_crypto', 'kmip_plugin', 'store_crypto'] crypto_plugin_names = ['p11_crypto', '', 'simple_crypto'] @@ -288,3 +287,30 @@ class TestSecretStorePluginManagerMultipleBackend( crypto_plugin = cm.get_manager().get_plugin_store_generate( crypto.PluginSupportTypes.ENCRYPT_DECRYPT) self.assertIsInstance(crypto_plugin, p11_crypto.P11CryptoPlugin) + + def test_plugin_created_kmip_default_mulitple_backend_conf(self): + """Check plugins are created as per multiple backend conf + + Here KMIP plugin is marked as global default plugin + """ + + store_plugin_names = ['store_crypto', 'kmip_plugin', 'store_crypto'] + crypto_plugin_names = ['p11_crypto', '', 'simple_crypto'] + + self.init_via_conf_file(store_plugin_names, + crypto_plugin_names, enabled=True, + global_default_index=1) + + with mock.patch('barbican.plugin.crypto.p11_crypto.P11CryptoPlugin.' + '_create_pkcs11') as m_pkcs11, \ + mock.patch('kmip.pie.client.ProxyKmipClient') as m_kmip: + + manager = str.SecretStorePluginManager() + + # check pkcs11 and kmip plugin instantiation call is invoked + m_pkcs11.called_once_with(mock.ANY, mock.ANY) + m_kmip.called_once_with(mock.ANY) + # check kmip store is matched as its global default store. + keySpec = str.KeySpec(str.KeyAlgorithm.AES, 128) + plugin_found = manager.get_plugin_store(keySpec) + self.assertIsInstance(plugin_found, kss.KMIPSecretStore) diff --git a/barbican/tests/plugin/util/test_multiple_backends.py b/barbican/tests/plugin/util/test_multiple_backends.py index c05696d7..461eae11 100644 --- a/barbican/tests/plugin/util/test_multiple_backends.py +++ b/barbican/tests/plugin/util/test_multiple_backends.py @@ -15,9 +15,19 @@ import collections import mock +import uuid from barbican.common import config from barbican.common import exception +from barbican.model import models +from barbican.model import repositories +from barbican.plugin.crypto import crypto +from barbican.plugin.crypto import manager as cm +from barbican.plugin.crypto import p11_crypto +from barbican.plugin.crypto import simple_crypto +from barbican.plugin.interface import secret_store +from barbican.plugin import kmip_secret_store as kss +from barbican.plugin import store_crypto from barbican.plugin.util import multiple_backends from barbican.tests import utils as test_utils @@ -26,7 +36,8 @@ class MockedManager(object): NAME_PREFIX = "friendly_" - def __init__(self, names): + def __init__(self, names, enabled=True, + plugin_lookup_field='store_plugin'): ExtTuple = collections.namedtuple('ExtTuple', ['name', 'obj']) self.extensions = [] for name in names: @@ -34,6 +45,8 @@ class MockedManager(object): m.get_plugin_name.return_value = self.NAME_PREFIX + name new_extension = ExtTuple(name, m) self.extensions.append(new_extension) + self.global_default_store_dict = None + self.parsed_stores = multiple_backends.read_multiple_backends_config() class WhenReadingMultipleBackendsConfig(test_utils.MultipleBackendsTestCase): @@ -99,6 +112,7 @@ class WhenReadingMultipleBackendsConfig(test_utils.MultipleBackendsTestCase): stores = multiple_backends.read_multiple_backends_config() self.assertEqual(len(ss_plugins), len(stores)) + self.assertEqual('', stores[1].crypto_plugin) def test_fail_when_global_default_not_specified(self): ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] @@ -136,3 +150,425 @@ class WhenReadingMultipleBackendsConfig(test_utils.MultipleBackendsTestCase): self.assertRaises(exception.MultipleStorePluginValueMissing, multiple_backends.read_multiple_backends_config) + + +class WhenInvokingSyncSecretStores(test_utils.MultipleBackendsTestCase): + + def setUp(self): + super(WhenInvokingSyncSecretStores, self).setUp() + + def test_successful_syncup_no_existing_secret_stores(self): + + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3', 'ss_p4', 'ss_p5'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3', 'cr_p4', 'cr_p5'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + default_secret_store = multiple_backends.\ + get_global_default_secret_store() + self.assertEqual('ss_p1', default_secret_store.store_plugin) + self.assertEqual('cr_p1', default_secret_store.crypto_plugin) + self.assertEqual(MockedManager.NAME_PREFIX + 'cr_p1', + default_secret_store.name) + + ss_db_entries = repositories.get_secret_stores_repository().get_all() + self.assertEqual(5, len(ss_db_entries)) + + def test_syncup_with_existing_secret_stores(self): + + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3', 'ss_p4', 'ss_p5'] + cr_plugins = ['cr_p1', '', 'cr_p3', 'cr_p4', 'cr_p5'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + ss_db_entries = repositories.get_secret_stores_repository().get_all() + self.assertEqual(5, len(ss_db_entries)) + + # check friendly name for the case when crypto plugin is not there + ss_db_entry = self._get_secret_store_entry('ss_p2', None) + self.assertIsNotNone(ss_db_entry) + self.assertEqual(MockedManager.NAME_PREFIX + 'ss_p2', + ss_db_entry.name) + + ss_plugins = ['ss_p3', 'ss_p4', 'ss_p5', 'ss_p6'] + cr_plugins = ['cr_p3', 'cr_p4', 'cr_p5', 'cr_p6'] + # update conf and re-run sync store + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + ss_db_entry = self._get_secret_store_entry('ss_p2', 'cr_p2') + self.assertIsNone(ss_db_entry) + + ss_db_entry = self._get_secret_store_entry('ss_p6', 'cr_p6') + self.assertIsNotNone(ss_db_entry) + + default_secret_store = multiple_backends.\ + get_global_default_secret_store() + self.assertEqual('ss_p3', default_secret_store.store_plugin) + self.assertEqual('cr_p3', default_secret_store.crypto_plugin) + self.assertEqual(MockedManager.NAME_PREFIX + 'cr_p3', + default_secret_store.name) + ss_db_entries = repositories.get_secret_stores_repository().get_all() + self.assertEqual(4, len(ss_db_entries)) + + def test_syncup_modify_global_default(self): + + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3', 'ss_p4', 'ss_p5'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3', 'cr_p4', 'cr_p5'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + global_secret_store = multiple_backends.\ + get_global_default_secret_store() + self.assertEqual('ss_p1', global_secret_store.store_plugin) + self.assertEqual('cr_p1', global_secret_store.crypto_plugin) + self.assertEqual(MockedManager.NAME_PREFIX + 'cr_p1', + global_secret_store.name) + + ss_plugins = ['ss_p9', 'ss_p4', 'ss_p5'] + cr_plugins = ['cr_p9', 'cr_p4', 'cr_p5'] + # update conf and re-run sync store + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + global_secret_store = multiple_backends.\ + get_global_default_secret_store() + self.assertEqual('ss_p9', global_secret_store.store_plugin) + self.assertEqual('cr_p9', global_secret_store.crypto_plugin) + self.assertEqual(MockedManager.NAME_PREFIX + 'cr_p9', + global_secret_store.name) + + def test_syncup_with_store_and_crypto_plugins_count_mismatch(self): + + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3', 'ss_p4'] + cr_plugins = ['cr_p1', '', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + # empty crypto_plugin name maps to None in database entry + ss_db_entry = self._get_secret_store_entry('ss_p2', None) + self.assertIsNotNone(ss_db_entry) + ss_db_entry = self._get_secret_store_entry('ss_p2', '') + self.assertIsNone(ss_db_entry) + + # missing crypto plugin name maps to None in database entry + ss_db_entry = self._get_secret_store_entry('ss_p4', None) + self.assertIsNotNone(ss_db_entry) + + def test_syncup_delete_secret_store_with_preferred_project_using_it(self): + """Removing secret store will fail if its defined as preferred store. + + """ + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3', 'ss_p4'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3', 'cr_p4'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + multiple_backends.sync_secret_stores(secretstore_manager, + crypto_manager) + + with mock.patch('barbican.model.repositories.' + 'get_project_secret_store_repository') as ps_repo: + # Mocking with 2 projects as using preferred secret store + ps_repo.get_count_by_secret_store.return_value = 2 + + ss_plugins = ['ss_p3', 'ss_p4'] + cr_plugins = ['cr_p3', 'cr_p4'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + secretstore_manager = MockedManager(ss_plugins) + crypto_manager = MockedManager(cr_plugins) + + self.assertRaises(exception.MultipleStorePluginStillInUse, + multiple_backends.sync_secret_stores, + secretstore_manager, crypto_manager) + + def test_get_global_default_store_when_multiple_backends_disabled(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=False) + + default_store = multiple_backends.get_global_default_secret_store() + self.assertIsNone(default_store) + + +class TestGetApplicablePlugins(test_utils.MultipleBackendsTestCase): + + def setUp(self): + super(TestGetApplicablePlugins, self).setUp() + + def test_get_when_project_preferred_plugin_is_set(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + ss_manager = MockedManager(ss_plugins) + project_id = uuid.uuid4().hex + + with mock.patch('barbican.model.repositories.ProjectSecretStoreRepo.' + 'get_secret_store_for_project') as pref_func: + + # set preferred secret store to one of value in config + m_dict = {'store_plugin': 'ss_p3'} + m_rec = mock.MagicMock() + m_rec.secret_store.to_dict_fields.return_value = m_dict + pref_func.return_value = m_rec + + objs = multiple_backends.get_applicable_store_plugins( + ss_manager, project_id, None) + self.assertIn(project_id, pref_func.call_args_list[0][0]) + self.assertIsInstance(objs, list) + self.assertEqual(1, len(objs)) + self.assertIn('ss_p3', objs[0].get_plugin_name()) + + def test_get_when_project_preferred_plugin_is_not_found_in_conf(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True) + ss_manager = MockedManager(ss_plugins) + + project_id = uuid.uuid4().hex + + with mock.patch('barbican.model.repositories.ProjectSecretStoreRepo.' + 'get_secret_store_for_project') as pref_func: + + # set preferred secret store value which is not defined in config + m_dict = {'store_plugin': 'old_preferred_plugin'} + m_rec = mock.MagicMock() + m_rec.secret_store.to_dict_fields.return_value = m_dict + pref_func.return_value = m_rec + + self.assertRaises(exception.MultipleStorePreferredPluginMissing, + multiple_backends.get_applicable_store_plugins, + ss_manager, project_id, None) + self.assertIn(project_id, pref_func.call_args_list[0][0]) + + def test_get_when_project_preferred_plugin_not_set_then_default_used(self): + ss_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + # setting second plugin to be global default + self.init_via_conf_file(ss_plugins, cr_plugins, enabled=True, + global_default_index=1) + cr_manager = MockedManager(cr_plugins, + plugin_lookup_field='crypto_plugin') + project_id = uuid.uuid4().hex + + with mock.patch('barbican.plugin.util.multiple_backends.' + 'get_global_default_secret_store') as gd_func: + + m_dict = {'crypto_plugin': 'cr_p2'} + gd_func.return_value.to_dict_fields.return_value = m_dict + objs = multiple_backends.get_applicable_crypto_plugins(cr_manager, + project_id, + None) + gd_func.assert_called_once_with() + self.assertIsInstance(objs, list) + self.assertEqual(1, len(objs)) + self.assertIn('cr_p2', objs[0].get_plugin_name()) + + # call again with no project_id set + objs = multiple_backends.get_applicable_crypto_plugins(cr_manager, + None, None) + gd_func.assert_called_once_with() + self.assertIsInstance(objs, list) + self.assertEqual(1, len(objs)) + self.assertIn('cr_p2', objs[0].get_plugin_name()) + + def test_get_applicable_store_plugins_when_multiple_backend_not_enabled( + self): + + ss_config = config.get_module_config('secretstore') + ss_plugins = ['ss_p11', 'ss_p22', 'ss_p33', 'ss_p44'] + ss_conf_plugins = ['ss_p1', 'ss_p2', 'ss_p3'] + cr_conf_plugins = ['cr_p1', 'cr_p2', 'cr_p3'] + self.init_via_conf_file(ss_conf_plugins, cr_conf_plugins, + enabled=False) + ss_manager = MockedManager(ss_plugins) + + ss_config.set_override("enabled_secretstore_plugins", + ss_plugins, group='secretstore', + enforce_type=True) + + objs = multiple_backends.get_applicable_store_plugins(ss_manager, None, + None) + self.assertEqual(4, len(objs)) + + +@test_utils.parameterized_test_case +class TestPluginsGenerateStoreAPIMultipleBackend( + test_utils.MultipleBackendsTestCase): + + backend_dataset = { + "db_backend": [{ + 'store_plugins': ['store_crypto', 'kmip_plugin', 'store_crypto'], + 'crypto_plugins': ['simple_crypto', '', 'p11_crypto'], + 'default_store_class': store_crypto.StoreCryptoAdapterPlugin, + 'default_crypto_class': simple_crypto.SimpleCryptoPlugin + }], + "kmip": [{ + 'store_plugins': ['kmip_plugin', 'store_crypto', 'store_crypto'], + 'crypto_plugins': ['', 'p11_crypto', 'simple_crypto'], + 'default_store_class': kss.KMIPSecretStore, + 'default_crypto_class': None + }], + "pkcs11": [{ + 'store_plugins': ['store_crypto', 'store_crypto', 'kmip_plugin'], + 'crypto_plugins': ['p11_crypto', 'simple_crypto', ''], + 'default_store_class': store_crypto.StoreCryptoAdapterPlugin, + 'default_crypto_class': p11_crypto.P11CryptoPlugin + }] + } + + def setUp(self): + super(TestPluginsGenerateStoreAPIMultipleBackend, self).setUp() + + def _create_project(self): + session = repositories.get_project_repository().get_session() + + project = models.Project() + project.external_id = "keystone_project_id" + uuid.uuid4().hex + project.save(session=session) + return project + + def _create_project_store(self, project_id, secret_store_id): + proj_store_repo = repositories.get_project_secret_store_repository() + session = proj_store_repo.get_session() + + proj_model = models.ProjectSecretStore(project_id, secret_store_id) + + proj_s_store = proj_store_repo.create_from(proj_model, session) + proj_s_store.save(session=session) + return proj_s_store + + @test_utils.parameterized_dataset(backend_dataset) + def test_no_preferred_default_plugin(self, dataset): + """Check name, plugin and crypto class used for default secret store + + Secret store name is crypto class plugin name if defined otherwise user + friendly name is derived from store class plugin name + """ + + self.init_via_conf_file(dataset['store_plugins'], + dataset['crypto_plugins'], + enabled=True) + + with mock.patch('barbican.plugin.crypto.p11_crypto.P11CryptoPlugin.' + '_create_pkcs11'), \ + mock.patch('kmip.pie.client.ProxyKmipClient'): + manager = secret_store.SecretStorePluginManager() + + keySpec = secret_store.KeySpec(secret_store.KeyAlgorithm.AES, 128) + + plugin_found = manager.get_plugin_store(keySpec) + self.assertIsInstance(plugin_found, + dataset['default_store_class']) + + global_secret_store = multiple_backends.\ + get_global_default_secret_store() + + if dataset['default_crypto_class']: + crypto_plugin = cm.get_manager().get_plugin_store_generate( + crypto.PluginSupportTypes.ENCRYPT_DECRYPT) + self.assertIsInstance(crypto_plugin, + dataset['default_crypto_class']) + + # make sure secret store name is same as crypto class friendly name + # as store_plugin class is not direct impl of SecretStoreBase + self.assertEqual(global_secret_store.name, + crypto_plugin.get_plugin_name()) + else: # crypto class is not used + # make sure secret store name is same as store plugin class + # friendly name + self.assertEqual(global_secret_store.name, + plugin_found.get_plugin_name()) + # error raised for no crypto plugin + self.assertRaises(crypto.CryptoPluginNotFound, + cm.get_manager().get_plugin_store_generate, + crypto.PluginSupportTypes.ENCRYPT_DECRYPT) + + @test_utils.parameterized_dataset(backend_dataset) + def test_project_preferred_default_plugin(self, dataset): + """Check project preferred behavior with different global default""" + + self.init_via_conf_file(dataset['store_plugins'], + dataset['crypto_plugins'], + enabled=True) + + with mock.patch('barbican.plugin.crypto.p11_crypto.P11CryptoPlugin.' + '_create_pkcs11'), \ + mock.patch('kmip.pie.client.ProxyKmipClient'): + manager = secret_store.SecretStorePluginManager() + + pkcs11_secret_store = self._get_secret_store_entry('store_crypto', + 'p11_crypto') + kmip_secret_store = self._get_secret_store_entry('kmip_plugin', None) + db_secret_store = self._get_secret_store_entry('store_crypto', + 'simple_crypto') + + project1 = self._create_project() + project2 = self._create_project() + project3 = self._create_project() + + # For project1 , make pkcs11 as preferred secret store + self._create_project_store(project1.id, pkcs11_secret_store.id) + # For project2 , make kmip as preferred secret store + self._create_project_store(project2.id, kmip_secret_store.id) + # For project3 , make db backend as preferred secret store + self._create_project_store(project3.id, db_secret_store.id) + + keySpec = secret_store.KeySpec(secret_store.KeyAlgorithm.AES, 128) + cm_manager = cm.get_manager() + + # For project1, verify store and crypto plugin instance used are pkcs11 + # specific + plugin_found = manager.get_plugin_store(keySpec, + project_id=project1.id) + self.assertIsInstance(plugin_found, + store_crypto.StoreCryptoAdapterPlugin) + crypto_plugin = cm.get_manager().get_plugin_store_generate( + crypto.PluginSupportTypes.ENCRYPT_DECRYPT, project_id=project1.id) + self.assertIsInstance(crypto_plugin, p11_crypto.P11CryptoPlugin) + + # For project2, verify store plugin instance is kmip specific + # and there is no crypto plugin instance + plugin_found = manager.get_plugin_store(keySpec, + project_id=project2.id) + self.assertIsInstance(plugin_found, kss.KMIPSecretStore) + + self.assertRaises( + crypto.CryptoPluginNotFound, cm_manager.get_plugin_store_generate, + crypto.PluginSupportTypes.ENCRYPT_DECRYPT, project_id=project2.id) + + # For project3, verify store and crypto plugin instance used are db + # backend specific + plugin_found = manager.get_plugin_store(keySpec, + project_id=project3.id) + self.assertIsInstance(plugin_found, + store_crypto.StoreCryptoAdapterPlugin) + crypto_plugin = cm.get_manager().get_plugin_store_generate( + crypto.PluginSupportTypes.ENCRYPT_DECRYPT, project_id=project3.id) + self.assertIsInstance(crypto_plugin, simple_crypto.SimpleCryptoPlugin) + + # Make sure for project with no preferred setting, uses global default + project4 = self._create_project() + plugin_found = manager.get_plugin_store(keySpec, + project_id=project4.id) + self.assertIsInstance(plugin_found, + dataset['default_store_class']) diff --git a/barbican/tests/utils.py b/barbican/tests/utils.py index 0e4dc048..be284e69 100644 --- a/barbican/tests/utils.py +++ b/barbican/tests/utils.py @@ -106,6 +106,9 @@ class BaseTestCase(oslotest.BaseTestCase): def tearDown(self): super(BaseTestCase, self).tearDown() + ss_conf = config.get_module_config('secretstore') + ss_conf.clear_override("enable_multiple_secret_stores", + group='secretstore') class MockModelRepositoryMixin(object): From 6535e559cdf6f388a3c8c6292ccc41aad93842dc Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Fri, 19 Aug 2016 12:56:11 -0700 Subject: [PATCH 18/20] Adding rest API for secret-stores resource (Part 4) Added tests to provide 100% coverage on API and policy logic. Change-Id: Icb43049250be1d78bdd3db8fbad0dc0381cccaf7 Partially-Implements: blueprint multiple-secret-backend --- barbican/api/controllers/secretstores.py | 214 ++++++++++++ barbican/api/controllers/versions.py | 2 + barbican/common/hrefs.py | 5 + .../api/controllers/test_secretstores.py | 325 ++++++++++++++++++ barbican/tests/api/test_resources_policy.py | 161 +++++++++ barbican/tests/utils.py | 26 ++ doc/source/api/reference/store_backends.rst | 4 +- etc/barbican/policy.json | 8 +- 8 files changed, 742 insertions(+), 3 deletions(-) create mode 100644 barbican/api/controllers/secretstores.py create mode 100644 barbican/tests/api/controllers/test_secretstores.py diff --git a/barbican/api/controllers/secretstores.py b/barbican/api/controllers/secretstores.py new file mode 100644 index 00000000..857d3b57 --- /dev/null +++ b/barbican/api/controllers/secretstores.py @@ -0,0 +1,214 @@ +# (c) Copyright 2015-2016 Hewlett Packard Enterprise Development LP +# +# 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 pecan + +from barbican.api import controllers +from barbican.common import hrefs +from barbican.common import resources as res +from barbican.common import utils +from barbican import i18n as u +from barbican.model import repositories as repo +from barbican.plugin.util import multiple_backends + +LOG = utils.getLogger(__name__) + + +def _secret_store_not_found(): + """Throw exception indicating secret store not found.""" + pecan.abort(404, u._('Not Found. Secret store not found.')) + + +def _preferred_secret_store_not_found(): + """Throw exception indicating secret store not found.""" + pecan.abort(404, u._('Not Found. No preferred secret store defined for ' + 'this project.')) + + +def _multiple_backends_not_enabled(): + """Throw exception indicating multiple backends support is not enabled.""" + pecan.abort(404, u._('Not Found. Multiple backends support is not enabled ' + 'in service configuration.')) + + +def convert_secret_store_to_response_format(secret_store): + data = secret_store.to_dict_fields() + data['secret_store_plugin'] = data.pop('store_plugin') + data['secret_store_ref'] = hrefs.convert_secret_stores_to_href( + data['secret_store_id']) + # no need to pass store id as secret_store_ref is returned + data.pop('secret_store_id', None) + return data + + +class PreferredSecretStoreController(controllers.ACLMixin): + """Handles preferred secret store set/removal requests.""" + + def __init__(self, secret_store): + LOG.debug(u._('=== Creating PreferredSecretStoreController ===')) + self.secret_store = secret_store + self.proj_store_repo = repo.get_project_secret_store_repository() + + @pecan.expose(generic=True) + def index(self, **kwargs): + pecan.abort(405) # HTTP 405 Method Not Allowed as default + + @index.when(method='DELETE', template='json') + @controllers.handle_exceptions(u._('Removing preferred secret store')) + @controllers.enforce_rbac('secretstore_preferred:delete') + def on_delete(self, external_project_id, **kw): + LOG.debug(u._('Start: Remove project preferred secret-store for store' + ' id %s'), self.secret_store.id) + + project = res.get_or_create_project(external_project_id) + + project_store = self.proj_store_repo.get_secret_store_for_project( + project.id, None, suppress_exception=True) + if project_store is None: + _preferred_secret_store_not_found() + + self.proj_store_repo.delete_entity_by_id( + entity_id=project_store.id, + external_project_id=external_project_id) + pecan.response.status = 204 + + @index.when(method='POST', template='json') + @controllers.handle_exceptions(u._('Setting preferred secret store')) + @controllers.enforce_rbac('secretstore_preferred:post') + def on_post(self, external_project_id, **kwargs): + LOG.debug(u._('Start: Set project preferred secret-store for store '), + 'id %s', self.secret_store.id) + + project = res.get_or_create_project(external_project_id) + + self.proj_store_repo.create_or_update_for_project(project.id, + self.secret_store.id) + + pecan.response.status = 204 + + +class SecretStoreController(controllers.ACLMixin): + """Handles secret store retrieval requests.""" + + def __init__(self, secret_store): + LOG.debug(u._('=== Creating SecretStoreController ===')) + self.secret_store = secret_store + + @pecan.expose() + def _lookup(self, action, *remainder): + if (action == 'preferred'): + return PreferredSecretStoreController(self.secret_store), remainder + else: + pecan.abort(405) + + @pecan.expose(generic=True) + def index(self, **kwargs): + pecan.abort(405) # HTTP 405 Method Not Allowed as default + + @index.when(method='GET', template='json') + @controllers.handle_exceptions(u._('Secret store retrieval')) + @controllers.enforce_rbac('secretstore:get') + def on_get(self, external_project_id): + LOG.debug("== Getting secret store for %s", self.secret_store.id) + return convert_secret_store_to_response_format(self.secret_store) + + +class SecretStoresController(controllers.ACLMixin): + """Handles secret-stores list requests.""" + + def __init__(self): + LOG.debug('Creating SecretStoresController') + self.secret_stores_repo = repo.get_secret_stores_repository() + self.proj_store_repo = repo.get_project_secret_store_repository() + + def __getattr__(self, name): + route_table = { + 'global-default': self.get_global_default, + 'preferred': self.get_preferred, + } + if name in route_table: + return route_table[name] + raise AttributeError + + @pecan.expose() + def _lookup(self, secret_store_id, *remainder): + if not utils.is_multiple_backends_enabled(): + _multiple_backends_not_enabled() + + secret_store = self.secret_stores_repo.get(entity_id=secret_store_id, + suppress_exception=True) + if not secret_store: + _secret_store_not_found() + return SecretStoreController(secret_store), remainder + + @pecan.expose(generic=True) + def index(self, **kwargs): + pecan.abort(405) # HTTP 405 Method Not Allowed as default + + @index.when(method='GET', template='json') + @controllers.handle_exceptions(u._('List available secret stores')) + @controllers.enforce_rbac('secretstores:get') + def on_get(self, external_project_id, **kw): + LOG.debug(u._('Start SecretStoresController on_get: listing secret ' + 'stores')) + if not utils.is_multiple_backends_enabled(): + _multiple_backends_not_enabled() + + res.get_or_create_project(external_project_id) + + secret_stores = self.secret_stores_repo.get_all() + + resp_list = [] + for store in secret_stores: + item = convert_secret_store_to_response_format(store) + resp_list.append(item) + + resp = {'secret_stores': resp_list} + + return resp + + @pecan.expose(generic=True, template='json') + @controllers.handle_exceptions(u._('Retrieve global default secret store')) + @controllers.enforce_rbac('secretstores:get_global_default') + def get_global_default(self, external_project_id, **kw): + LOG.debug(u._('Start secret-stores get global default secret store')) + + if not utils.is_multiple_backends_enabled(): + _multiple_backends_not_enabled() + + res.get_or_create_project(external_project_id) + + store = multiple_backends.get_global_default_secret_store() + + return convert_secret_store_to_response_format(store) + + @pecan.expose(generic=True, template='json') + @controllers.handle_exceptions(u._('Retrieve project preferred store')) + @controllers.enforce_rbac('secretstores:get_preferred') + def get_preferred(self, external_project_id, **kw): + LOG.debug(u._('Start secret-stores get preferred secret store')) + + if not utils.is_multiple_backends_enabled(): + _multiple_backends_not_enabled() + + project = res.get_or_create_project(external_project_id) + + project_store = self.proj_store_repo.get_secret_store_for_project( + project.id, None, suppress_exception=True) + + if project_store is None: + _preferred_secret_store_not_found() + + return convert_secret_store_to_response_format( + project_store.secret_store) diff --git a/barbican/api/controllers/versions.py b/barbican/api/controllers/versions.py index 1cc78285..bc84448a 100644 --- a/barbican/api/controllers/versions.py +++ b/barbican/api/controllers/versions.py @@ -19,6 +19,7 @@ from barbican.api.controllers import containers from barbican.api.controllers import orders from barbican.api.controllers import quotas from barbican.api.controllers import secrets +from barbican.api.controllers import secretstores from barbican.api.controllers import transportkeys from barbican.common import config from barbican.common import utils @@ -97,6 +98,7 @@ class V1Controller(BaseVersionController): self.cas = cas.CertificateAuthoritiesController() self.quotas = quotas.QuotasController() setattr(self, 'project-quotas', quotas.ProjectsQuotasController()) + setattr(self, 'secret-stores', secretstores.SecretStoresController()) @pecan.expose(generic=True) def index(self): diff --git a/barbican/common/hrefs.py b/barbican/common/hrefs.py index 80bcc66e..227f9945 100644 --- a/barbican/common/hrefs.py +++ b/barbican/common/hrefs.py @@ -56,6 +56,11 @@ def convert_certificate_authority_to_href(ca_id): return convert_resource_id_to_href('cas', ca_id) +def convert_secret_stores_to_href(secret_store_id): + """Convert the ca ID to a HATEOAS-style href.""" + return convert_resource_id_to_href('secret-stores', secret_store_id) + + # TODO(hgedikli) handle list of fields in here def convert_to_hrefs(fields): """Convert id's within a fields dict to HATEOAS-style hrefs.""" diff --git a/barbican/tests/api/controllers/test_secretstores.py b/barbican/tests/api/controllers/test_secretstores.py new file mode 100644 index 00000000..5953b1de --- /dev/null +++ b/barbican/tests/api/controllers/test_secretstores.py @@ -0,0 +1,325 @@ +# (c) Copyright 2015-2016 Hewlett Packard Enterprise Development LP +# +# 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 mock +import uuid + +from barbican.model import models +from barbican.model import repositories as repos +from barbican.plugin.interface import secret_store +from barbican.tests import utils + + +class SecretStoresMixin(utils.MultipleBackendsTestCase): + + def _create_project(self): + session = repos.get_project_repository().get_session() + + project = models.Project() + project.external_id = "keystone_project_id" + uuid.uuid4().hex + project.save(session=session) + return project + + def _create_project_store(self, project_id, secret_store_id): + proj_store_repo = repos.get_project_secret_store_repository() + session = proj_store_repo.get_session() + + proj_model = models.ProjectSecretStore(project_id, secret_store_id) + + proj_s_store = proj_store_repo.create_from(proj_model, session) + proj_s_store.save(session=session) + return proj_s_store + + def _init_multiple_backends(self, enabled=True, global_default_index=0): + + store_plugin_names = ['store_crypto', 'kmip_plugin', 'store_crypto'] + crypto_plugin_names = ['p11_crypto', '', 'simple_crypto'] + + self.init_via_conf_file(store_plugin_names, + crypto_plugin_names, enabled=enabled, + global_default_index=global_default_index) + + with mock.patch('barbican.plugin.crypto.p11_crypto.P11CryptoPlugin.' + '_create_pkcs11'), \ + mock.patch('kmip.pie.client.ProxyKmipClient'): + + secret_store.SecretStorePluginManager() + + +class WhenTestingSecretStores(utils.BarbicanAPIBaseTestCase, + SecretStoresMixin): + + def setUp(self): + super(WhenTestingSecretStores, self).setUp() + self.secret_store_repo = repos.get_secret_stores_repository() + + def test_should_get_all_secret_stores(self): + + g_index = 2 # global default index in plugins list + self._init_multiple_backends(global_default_index=g_index) + + resp = self.app.get('/secret-stores', expect_errors=False) + self.assertEqual(200, resp.status_int) + secret_stores_data = resp.json.get('secret_stores') + + self.assertEqual(3, len(secret_stores_data)) + + for i, secret_data in enumerate(secret_stores_data): + self.assertEqual(i == g_index, secret_data['global_default']) + self.assertIsNotNone(secret_data['secret_store_ref']) + self.assertIsNone(secret_data.get('id')) + self.assertIsNone(secret_data.get('secret_store_id')) + self.assertIsNotNone(secret_data['name']) + self.assertIsNotNone(secret_data['secret_store_plugin']) + self.assertIsNotNone(secret_data['created']) + self.assertIsNotNone(secret_data['updated']) + self.assertEqual(models.States.ACTIVE, secret_data['status']) + + def test_get_all_secret_stores_when_multiple_backends_not_enabled(self): + + self._init_multiple_backends(enabled=False) + + resp = self.app.get('/secret-stores', expect_errors=True) + self.assertEqual(404, resp.status_int) + + resp = self.app.get('/secret-stores/any_valid_id', + expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_get_all_secret_stores_with_unsupported_http_method(self): + + self._init_multiple_backends() + + resp = self.app.put('/secret-stores', expect_errors=True) + self.assertEqual(405, resp.status_int) + + resp = self.app.patch('/secret-stores', expect_errors=True) + self.assertEqual(405, resp.status_int) + + def test_should_get_global_default(self): + + self._init_multiple_backends(global_default_index=1) + + resp = self.app.get('/secret-stores/global-default', + expect_errors=False) + self.assertEqual(200, resp.status_int) + resp_data = resp.json + self.assertEqual(True, resp_data['global_default']) + self.assertIn('kmip', resp_data['name'].lower()) + self.assertIsNotNone(resp_data['secret_store_ref']) + self.assertIsNotNone(resp_data['secret_store_plugin']) + self.assertIsNone(resp_data['crypto_plugin']) + self.assertIsNotNone(resp_data['created']) + self.assertIsNotNone(resp_data['updated']) + self.assertEqual(models.States.ACTIVE, resp_data['status']) + + def test_get_global_default_when_multiple_backends_not_enabled(self): + + self._init_multiple_backends(enabled=False) + + with mock.patch('barbican.common.resources.' + 'get_or_create_project') as m1: + + resp = self.app.get('/secret-stores/global-default', + expect_errors=True) + + self.assertFalse(m1.called) + self.assertEqual(404, resp.status_int) + + def test_get_preferred_when_preferred_is_set(self): + self._init_multiple_backends(global_default_index=1) + + secret_stores = self.secret_store_repo.get_all() + project1 = self._create_project() + + self._create_project_store(project1.id, secret_stores[0].id) + + self.app.extra_environ = { + 'barbican.context': self._build_context(project1.external_id) + } + resp = self.app.get('/secret-stores/preferred', + expect_errors=False) + self.assertEqual(200, resp.status_int) + resp_data = resp.json + self.assertEqual(secret_stores[0].name, resp_data['name']) + self.assertEqual(secret_stores[0].global_default, + resp_data['global_default']) + self.assertIn('/secret-stores/{0}'.format(secret_stores[0].id), + resp_data['secret_store_ref']) + self.assertIsNotNone(resp_data['created']) + self.assertIsNotNone(resp_data['updated']) + self.assertEqual(models.States.ACTIVE, resp_data['status']) + + def test_get_preferred_when_preferred_is_not_set(self): + self._init_multiple_backends(global_default_index=1) + project1 = self._create_project() + + self.app.extra_environ = { + 'barbican.context': self._build_context(project1.external_id) + } + resp = self.app.get('/secret-stores/preferred', + expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_get_preferred_when_multiple_backends_not_enabled(self): + + self._init_multiple_backends(enabled=False) + + with mock.patch('barbican.common.resources.' + 'get_or_create_project') as m1: + + resp = self.app.get('/secret-stores/preferred', + expect_errors=True) + + self.assertFalse(m1.called) + self.assertEqual(404, resp.status_int) + + +class WhenTestingSecretStore(utils.BarbicanAPIBaseTestCase, + SecretStoresMixin): + + def setUp(self): + super(WhenTestingSecretStore, self).setUp() + self.secret_store_repo = repos.get_secret_stores_repository() + + def test_get_a_secret_store_when_no_error(self): + + self._init_multiple_backends() + + secret_stores = self.secret_store_repo.get_all() + + store = secret_stores[0] + + resp = self.app.get('/secret-stores/{0}'.format(store.id), + expect_errors=False) + self.assertEqual(200, resp.status_int) + data = resp.json + self.assertEqual(store.global_default, data['global_default']) + self.assertEqual(store.name, data['name']) + self.assertIn('/secret-stores/{0}'.format(store.id), + data['secret_store_ref']) + self.assertIsNotNone(data['secret_store_plugin']) + self.assertIsNotNone(data['created']) + self.assertIsNotNone(data['updated']) + self.assertEqual(models.States.ACTIVE, data['status']) + + def test_invalid_uri_for_secret_stores_subresource(self): + self._init_multiple_backends() + + resp = self.app.get('/secret-stores/invalid_uri', + expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_get_a_secret_store_with_unsupported_http_method(self): + self._init_multiple_backends() + + secret_stores = self.secret_store_repo.get_all() + store_id = secret_stores[0].id + + resp = self.app.put('/secret-stores/{0}'.format(store_id), + expect_errors=True) + self.assertEqual(405, resp.status_int) + + def test_invalid_uri_for_a_secret_store_subresource(self): + self._init_multiple_backends() + + secret_stores = self.secret_store_repo.get_all() + resp = self.app.get('/secret-stores/{0}/invalid_uri'. + format(secret_stores[0].id), expect_errors=True) + self.assertEqual(405, resp.status_int) + + +class WhenTestingProjectSecretStore(utils.BarbicanAPIBaseTestCase, + SecretStoresMixin): + + def setUp(self): + super(WhenTestingProjectSecretStore, self).setUp() + self.secret_store_repo = repos.get_secret_stores_repository() + self.proj_store_repo = repos.get_project_secret_store_repository() + + def test_set_a_preferred_secret_store_when_no_error(self): + + self._init_multiple_backends() + + stores = self.secret_store_repo.get_all() + + proj_external_id = uuid.uuid4().hex + # get ids as secret store are not bound to session after a rest call. + store_ids = [store.id for store in stores] + for store_id in store_ids: + self.app.extra_environ = { + 'barbican.context': self._build_context(proj_external_id) + } + resp = self.app.post('/secret-stores/{0}/preferred'. + format(store_id), expect_errors=False) + self.assertEqual(204, resp.status_int) + + # Now make sure preferred store is set to store id via get call + resp = self.app.get('/secret-stores/preferred') + self.assertIn(store_id, resp.json['secret_store_ref']) + + def test_unset_a_preferred_secret_store_when_no_error(self): + + self._init_multiple_backends() + + stores = self.secret_store_repo.get_all() + + proj_external_id = uuid.uuid4().hex + # get ids as secret store are not bound to session after a rest call. + store_ids = [store.id for store in stores] + for store_id in store_ids: + self.app.extra_environ = { + 'barbican.context': self._build_context(proj_external_id) + } + resp = self.app.post('/secret-stores/{0}/preferred'. + format(store_id), expect_errors=False) + self.assertEqual(204, resp.status_int) + + # unset preferred store here + resp = self.app.delete('/secret-stores/{0}/preferred'. + format(store_id), expect_errors=False) + self.assertEqual(204, resp.status_int) + + # Now make sure that there is no longer a preferred store set + resp = self.app.get('/secret-stores/preferred', + expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_unset_a_preferred_store_when_not_found_error(self): + self._init_multiple_backends() + + stores = self.secret_store_repo.get_all() + proj_external_id = uuid.uuid4().hex + self.app.extra_environ = { + 'barbican.context': self._build_context(proj_external_id) + } + resp = self.app.delete('/secret-stores/{0}/preferred'. + format(stores[0].id), expect_errors=True) + self.assertEqual(404, resp.status_int) + + def test_preferred_secret_store_call_with_unsupported_http_method(self): + self._init_multiple_backends() + + secret_stores = self.secret_store_repo.get_all() + store_id = secret_stores[0].id + + proj_external_id = uuid.uuid4().hex + + self.app.extra_environ = { + 'barbican.context': self._build_context(proj_external_id) + } + resp = self.app.put('/secret-stores/{0}/preferred'. + format(store_id), expect_errors=True) + + self.assertEqual(405, resp.status_int) diff --git a/barbican/tests/api/test_resources_policy.py b/barbican/tests/api/test_resources_policy.py index 8243f058..b7b6bd8b 100644 --- a/barbican/tests/api/test_resources_policy.py +++ b/barbican/tests/api/test_resources_policy.py @@ -27,6 +27,7 @@ from barbican.api.controllers import consumers from barbican.api.controllers import containers from barbican.api.controllers import orders from barbican.api.controllers import secrets +from barbican.api.controllers import secretstores from barbican.api.controllers import versions from barbican.common import config from barbican import context @@ -101,6 +102,18 @@ class ConsumerResource(TestableResource): controller_cls = consumers.ContainerConsumerController +class SecretStoresResource(TestableResource): + controller_cls = secretstores.SecretStoresController + + +class SecretStoreResource(TestableResource): + controller_cls = secretstores.SecretStoreController + + +class PreferredSecretStoreResource(TestableResource): + controller_cls = secretstores.PreferredSecretStoreController + + class BaseTestCase(utils.BaseTestCase, utils.MockModelRepositoryMixin): def setUp(self): @@ -1115,3 +1128,151 @@ class WhenTestingConsumerResource(BaseTestCase): def _invoke_on_get(self): self.resource.on_get(self.req, self.resp) + + +class WhenTestingSecretStoresResource(BaseTestCase): + """RBAC tests for the barbican.api.resources.SecretStoresResource class.""" + def setUp(self): + super(WhenTestingSecretStoresResource, self).setUp() + + self.external_project_id = '12345project' + + self.moc_enable_patcher = mock.patch( + 'barbican.common.utils.is_multiple_backends_enabled') + enable_check_method = self.moc_enable_patcher.start() + enable_check_method.return_value = True + self.addCleanup(self.moc_enable_patcher.stop) + + # Force an error on GET calls that pass RBAC, as we are not testing + # such flows in this test module. + self.project_repo = mock.MagicMock() + fail_method = mock.MagicMock(return_value=None, + side_effect=self._generate_get_error()) + self.project_repo.find_by_external_project_id = fail_method + self.setup_project_repository_mock(self.project_repo) + + self.resource = SecretStoresResource() + + def test_rules_should_be_loaded(self): + self.assertIsNotNone(self.policy_enforcer.rules) + + def test_should_pass_get_all_secret_stores(self): + self._assert_pass_rbac(['admin'], + self._invoke_on_get) + + def test_should_raise_get_all_secret_stores(self): + self._assert_fail_rbac([None, 'creator', 'observer', 'audit'], + self._invoke_on_get) + + def test_should_pass_get_global_default(self): + self._assert_pass_rbac(['admin'], + self._invoke_get_global_default) + + def test_should_raise_get_global_default(self): + self._assert_fail_rbac([None, 'creator', 'observer', 'audit'], + self._invoke_get_global_default) + + def test_should_pass_get_preferred(self): + self._assert_pass_rbac(['admin'], + self._invoke_get_preferred) + + def test_should_raise_get_preferred(self): + self._assert_fail_rbac([None, 'creator', 'observer', 'audit'], + self._invoke_get_preferred) + + def _invoke_on_get(self): + self.resource.on_get(self.req, self.resp) + + def _invoke_get_global_default(self): + with mock.patch('pecan.request', self.req): + with mock.patch('pecan.response', self.resp): + return self.resource.controller.get_global_default() + + def _invoke_get_preferred(self): + with mock.patch('pecan.request', self.req): + with mock.patch('pecan.response', self.resp): + return self.resource.controller.get_preferred() + + +class WhenTestingSecretStoreResource(BaseTestCase): + """RBAC tests for the barbican.api.resources.SecretStoreResource class.""" + def setUp(self): + super(WhenTestingSecretStoreResource, self).setUp() + + self.external_project_id = '12345project' + self.store_id = '123456SecretStoreId' + + self.moc_enable_patcher = mock.patch( + 'barbican.common.utils.is_multiple_backends_enabled') + enable_check_method = self.moc_enable_patcher.start() + enable_check_method.return_value = True + self.addCleanup(self.moc_enable_patcher.stop) + + # Force an error on GET calls that pass RBAC, as we are not testing + # such flows in this test module. + + self.project_repo = mock.MagicMock() + fail_method = mock.MagicMock(return_value=None, + side_effect=self._generate_get_error()) + + self.project_repo.find_by_external_project_id = fail_method + self.setup_project_repository_mock(self.project_repo) + + secret_store_res = mock.MagicMock() + secret_store_res.to_dict_fields = mock.MagicMock(side_effect=IOError) + secret_store_res.id = self.store_id + + self.resource = SecretStoreResource(secret_store_res) + + def test_rules_should_be_loaded(self): + self.assertIsNotNone(self.policy_enforcer.rules) + + def test_should_pass_get_a_secret_store(self): + self._assert_pass_rbac(['admin'], + self._invoke_on_get) + + def test_should_raise_get_a_secret_store(self): + self._assert_fail_rbac([None, 'creator', 'observer', 'audit'], + self._invoke_on_get) + + def _invoke_on_get(self): + self.resource.on_get(self.req, self.resp) + + +class WhenTestingPreferredSecretStoreResource(BaseTestCase): + """RBAC tests for barbican.api.resources.PreferredSecretStoreResource""" + def setUp(self): + super(WhenTestingPreferredSecretStoreResource, self).setUp() + + self.external_project_id = '12345project' + self.store_id = '123456SecretStoreId' + + self.moc_enable_patcher = mock.patch( + 'barbican.common.utils.is_multiple_backends_enabled') + enable_check_method = self.moc_enable_patcher.start() + enable_check_method.return_value = True + self.addCleanup(self.moc_enable_patcher.stop) + + # Force an error on POST/DELETE calls that pass RBAC, as we are not + # testing such flows in this test module. + self.project_repo = mock.MagicMock() + fail_method = mock.MagicMock(return_value=None, + side_effect=self._generate_get_error()) + self.project_repo.find_by_external_project_id = fail_method + self.setup_project_repository_mock(self.project_repo) + + self.resource = PreferredSecretStoreResource(mock.MagicMock()) + + def test_rules_should_be_loaded(self): + self.assertIsNotNone(self.policy_enforcer.rules) + + def test_should_pass_set_preferred_secret_store(self): + self._assert_pass_rbac(['admin'], + self._invoke_on_post) + + def test_should_raise_set_preferred_secret_store(self): + self._assert_fail_rbac([None, 'creator', 'observer', 'audit'], + self._invoke_on_post) + + def _invoke_on_post(self): + self.resource.on_post(self.req, self.resp) diff --git a/barbican/tests/utils.py b/barbican/tests/utils.py index be284e69..aa8c7d75 100644 --- a/barbican/tests/utils.py +++ b/barbican/tests/utils.py @@ -302,6 +302,32 @@ class MockModelRepositoryMixin(object): mock_repo_obj=mock_preferred_ca_repo, patcher_obj=self.mock_preferred_ca_repo_patcher) + def setup_secret_stores_repository_mock( + self, mock_secret_stores_repo=mock.MagicMock()): + """Mocks the project repository factory function + + :param mock_secret_stores_repo: The pre-configured mock secret stores + repo to be returned. + """ + self.mock_secret_stores_repo_patcher = None + self._setup_repository_mock( + repo_factory='get_secret_stores_repository', + mock_repo_obj=mock_secret_stores_repo, + patcher_obj=self.mock_secret_stores_repo_patcher) + + def setup_project_secret_store_repository_mock( + self, mock_project_secret_store_repo=mock.MagicMock()): + """Mocks the project repository factory function + + :param mock_project_secret_store_repo: The pre-configured mock project + secret store repo to be returned. + """ + self.mock_proj_secret_store_repo_patcher = None + self._setup_repository_mock( + repo_factory='get_project_secret_store_repository', + mock_repo_obj=mock_project_secret_store_repo, + patcher_obj=self.mock_proj_secret_store_repo_patcher) + def setup_project_ca_repository_mock( self, mock_project_ca_repo=mock.MagicMock()): """Mocks the project repository factory function diff --git a/doc/source/api/reference/store_backends.rst b/doc/source/api/reference/store_backends.rst index ddf44d9d..48586241 100644 --- a/doc/source/api/reference/store_backends.rst +++ b/doc/source/api/reference/store_backends.rst @@ -43,7 +43,7 @@ Request/Response: Content-Type: application/json { - "secret-stores":[ + "secret_stores":[ { "status": "ACTIVE", "updated": "2016-08-22T23:46:45.114283", @@ -85,7 +85,7 @@ Response Attributes +---------------+--------+---------------------------------------------+ | Name | Type | Description | +===============+========+=============================================+ -| secret-stores | list | A list of secret store references | +| secret_stores | list | A list of secret store references | +---------------+--------+---------------------------------------------+ | name | string | store and crypto plugin name delimited by + | | | | (plus) sign. | diff --git a/etc/barbican/policy.json b/etc/barbican/policy.json index 87e87cb5..723f1c17 100644 --- a/etc/barbican/policy.json +++ b/etc/barbican/policy.json @@ -80,5 +80,11 @@ "secret_meta:get": "rule:all_but_audit", "secret_meta:post": "rule:admin_or_creator", "secret_meta:put": "rule:admin_or_creator", - "secret_meta:delete": "rule:admin_or_creator" + "secret_meta:delete": "rule:admin_or_creator", + "secretstores:get": "rule:admin", + "secretstores:get_global_default": "rule:admin", + "secretstores:get_preferred": "rule:admin", + "secretstore_preferred:post": "rule:admin", + "secretstore_preferred:delete": "rule:admin", + "secretstore:get": "rule:admin" } From 845b3d045b17f3554c4a4db8010404801d3a7090 Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Tue, 23 Aug 2016 16:28:15 -0700 Subject: [PATCH 19/20] Adding functional tests for multiple backend changes (Part 5) Change-Id: Iaf02d446a178baaa3e61d6a7267717822bd957f8 Partially-Implements: blueprint multiple-secret-backend --- barbican/model/repositories.py | 13 ++ etc/barbican/barbican-functional.conf | 4 + functionaltests/api/base.py | 2 + .../v1/behaviors/secretstores_behaviors.py | 101 +++++++++ .../api/v1/functional/test_secrets.py | 124 ++++++++++ .../api/v1/functional/test_secretstores.py | 213 ++++++++++++++++++ functionaltests/common/config.py | 3 +- 7 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 functionaltests/api/v1/behaviors/secretstores_behaviors.py create mode 100644 functionaltests/api/v1/functional/test_secretstores.py diff --git a/barbican/model/repositories.py b/barbican/model/repositories.py index cb5e1715..451b5adf 100644 --- a/barbican/model/repositories.py +++ b/barbican/model/repositories.py @@ -106,6 +106,7 @@ def setup_database_engine_and_factory(): # session instance per thread. session_maker = sa_orm.sessionmaker(bind=_ENGINE) _SESSION_FACTORY = sqlalchemy.orm.scoped_session(session_maker) + _initialize_secret_stores_data() def start(): @@ -204,6 +205,18 @@ def _get_engine(engine): return engine +def _initialize_secret_stores_data(): + """Initializes secret stores data in database. + + This logic is executed only when database engine and factory is built. + Secret store get_manager internally reads secret store plugin configuration + from service configuration and saves it in secret_stores table in database. + """ + if utils.is_multiple_backends_enabled(): + from barbican.plugin.interface import secret_store + secret_store.get_manager() + + def is_db_connection_error(args): """Return True if error in connecting to db.""" # NOTE(adam_g): This is currently MySQL specific and needs to be extended diff --git a/etc/barbican/barbican-functional.conf b/etc/barbican/barbican-functional.conf index 214dfa75..ce329b33 100644 --- a/etc/barbican/barbican-functional.conf +++ b/etc/barbican/barbican-functional.conf @@ -69,6 +69,10 @@ auditor_b_password=barbican # Default value is True. server_host_href_set = True +# Flag to indicate if multiple backends support is enabled or not at barbican +# server side. Functional tests behavior changes depending on this flag value. +server_multiple_backends_enabled = False + [quotas] # For each resource, the default maximum number that can be used for # a project is set below. This value can be overridden for each diff --git a/functionaltests/api/base.py b/functionaltests/api/base.py index b4d2281f..12d55af0 100644 --- a/functionaltests/api/base.py +++ b/functionaltests/api/base.py @@ -31,6 +31,8 @@ from functionaltests.common import config CONF = config.get_config() conf_host_href_used = CONF.keymanager.server_host_href_set +conf_multiple_backends_enabled = CONF.keymanager.\ + server_multiple_backends_enabled class TestCase(oslotest.BaseTestCase): diff --git a/functionaltests/api/v1/behaviors/secretstores_behaviors.py b/functionaltests/api/v1/behaviors/secretstores_behaviors.py new file mode 100644 index 00000000..751feed2 --- /dev/null +++ b/functionaltests/api/v1/behaviors/secretstores_behaviors.py @@ -0,0 +1,101 @@ +# (c) Copyright 2016 Hewlett Packard Enterprise Development LP +# +# 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. + +from functionaltests.api.v1.behaviors import base_behaviors + + +class SecretStoresBehaviors(base_behaviors.BaseBehaviors): + + def get_all_secret_stores(self, extra_headers=None, use_auth=True, + user_name=None): + """Retrieves list of secret stores available in barbican""" + resp = self.client.get('secret-stores', + extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) + json_data = None + if resp.status_code == 200: + json_data = self.get_json(resp) + + return resp, json_data + + def get_global_default(self, extra_headers=None, use_auth=True, + user_name=None): + """Retrieves global default secret store.""" + + resp = self.client.get('secret-stores/global-default', + extra_headers=extra_headers, use_auth=use_auth, + user_name=user_name) + json_data = None + if resp.status_code == 200: + json_data = self.get_json(resp) + + return resp, json_data + + def get_project_preferred_store(self, extra_headers=None, use_auth=True, + user_name=None): + """Retrieve global default secret store.""" + + resp = self.client.get('secret-stores/preferred', + extra_headers=extra_headers, use_auth=use_auth, + user_name=user_name) + json_data = None + if resp.status_code == 200: + json_data = self.get_json(resp) + + return resp, json_data + + def get_a_secret_store(self, secret_store_ref, extra_headers=None, + use_auth=True, user_name=None): + """Retrieve a specific secret store.""" + + resp = self.client.get(secret_store_ref, extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) + json_data = None + if resp.status_code == 200: + json_data = self.get_json(resp) + + return resp, json_data + + def set_preferred_secret_store(self, secret_store_ref, + extra_headers=None, use_auth=True, + user_name=None): + """Set a preferred secret store.""" + project_id = None + try: + if user_name: + project_id = self.client.get_project_id_from_name(user_name) + except Exception: + pass + + resp = self.client.post(secret_store_ref + '/preferred', + extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) + if resp.status_code == 204 and project_id: + # add tuple of store ref, user_name to cleanup later + self.created_entities.append((secret_store_ref, user_name)) + + return resp + + def unset_preferred_secret_store(self, secret_store_ref, + extra_headers=None, use_auth=True, + user_name=None): + """Unset a preferred secret store.""" + + return self.client.delete(secret_store_ref + '/preferred', + extra_headers=extra_headers, + use_auth=use_auth, user_name=user_name) + + def cleanup_preferred_secret_store_entities(self): + for (store_ref, user_name) in self.created_entities: + self.unset_preferred_secret_store(store_ref, user_name=user_name) diff --git a/functionaltests/api/v1/functional/test_secrets.py b/functionaltests/api/v1/functional/test_secrets.py index da7c1358..c5408a0a 100644 --- a/functionaltests/api/v1/functional/test_secrets.py +++ b/functionaltests/api/v1/functional/test_secrets.py @@ -27,7 +27,14 @@ from barbican.tests import keys from barbican.tests import utils from functionaltests.api import base from functionaltests.api.v1.behaviors import secret_behaviors +from functionaltests.api.v1.behaviors import secretstores_behaviors from functionaltests.api.v1.models import secret_models +from functionaltests.common import config + + +CONF = config.get_config() +admin_a = CONF.rbac_users.admin_a +admin_b = CONF.rbac_users.admin_b def get_pem_content(pem): @@ -1440,3 +1447,120 @@ class SecretsUnauthedTestCase(base.TestCase): use_auth=False ) self.assertEqual(401, resp.status_code) + + +@utils.parameterized_test_case +class SecretsMultipleBackendTestCase(base.TestCase): + + def setUp(self): + super(SecretsMultipleBackendTestCase, self).setUp() + self.behaviors = secret_behaviors.SecretBehaviors(self.client) + self.ss_behaviors = secretstores_behaviors.SecretStoresBehaviors( + self.client) + self.default_secret_create_data = get_default_data() + if base.conf_multiple_backends_enabled: + resp, stores = self.ss_behaviors.get_all_secret_stores( + user_name=admin_a) + self.assertEqual(200, resp.status_code) + secret_store_ref = None + for store in stores['secret-stores']: + if not store['global_default']: + secret_store_ref = store['secret_store_ref'] + break + # set preferred secret store for admin_a (project a) user + # and don't set preferred secret store for admin_b (project b) user + self.ss_behaviors.set_preferred_secret_store(secret_store_ref, + user_name=admin_a) + + def tearDown(self): + self.behaviors.delete_all_created_secrets() + if base.conf_multiple_backends_enabled: + self.ss_behaviors.cleanup_preferred_secret_store_entities() + super(SecretsMultipleBackendTestCase, self).tearDown() + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset({ + 'symmetric_type_preferred_store': [ + admin_a, + 'symmetric', + base64.b64decode(get_default_payload()), + get_default_data() + ], + 'private_type_preferred_store': [ + admin_a, + 'private', + keys.get_private_key_pem(), + get_private_key_req() + ], + 'public_type_preferred_store': [ + admin_a, + 'public', + keys.get_public_key_pem(), + get_public_key_req() + ], + 'certificate_type_preferred_store': [ + admin_a, + 'certificate', + keys.get_certificate_pem(), + get_certificate_req() + ], + 'passphrase_type_preferred_store': [ + admin_a, + 'passphrase', + 'mysecretpassphrase', + get_passphrase_req() + ], + 'symmetric_type_no_preferred_store': [ + admin_b, + 'symmetric', + base64.b64decode(get_default_payload()), + get_default_data() + ], + 'private_type_no_preferred_store': [ + admin_b, + 'private', + keys.get_private_key_pem(), + get_private_key_req() + ], + 'public_type_no_preferred_store': [ + admin_b, + 'public', + keys.get_public_key_pem(), + get_public_key_req() + ], + 'certificate_type_no_preferred_store': [ + admin_b, + 'certificate', + keys.get_certificate_pem(), + get_certificate_req() + ], + 'passphrase_type_no_preferred_store': [ + admin_b, + 'passphrase', + 'mysecretpassphrase', + get_passphrase_req() + ], + }) + def test_secret_create_for(self, user_name, secret_type, expected, spec): + """Create secrets with various secret types with multiple backends.""" + test_model = secret_models.SecretModel(**spec) + test_model.secret_type = secret_type + + resp, secret_ref = self.behaviors.create_secret(test_model, + user_name=user_name, + admin=user_name) + self.assertEqual(201, resp.status_code) + + resp = self.behaviors.get_secret_metadata(secret_ref, + user_name=user_name) + secret_type_response = resp.model.secret_type + self.assertIsNotNone(secret_type_response) + self.assertEqual(secret_type, secret_type_response) + + content_type = spec['payload_content_type'] + get_resp = self.behaviors.get_secret(secret_ref, + content_type, + user_name=user_name) + self.assertEqual(expected, get_resp.content) diff --git a/functionaltests/api/v1/functional/test_secretstores.py b/functionaltests/api/v1/functional/test_secretstores.py new file mode 100644 index 00000000..5c10f342 --- /dev/null +++ b/functionaltests/api/v1/functional/test_secretstores.py @@ -0,0 +1,213 @@ +# (c) Copyright 2016 Hewlett Packard Enterprise Development LP +# +# 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. + +from testtools import testcase + +from barbican.tests import utils +from functionaltests.api import base +from functionaltests.api.v1.behaviors import secret_behaviors +from functionaltests.api.v1.behaviors import secretstores_behaviors +from functionaltests.common import config + +CONF = config.get_config() +admin_a = CONF.rbac_users.admin_a +creator_a = CONF.rbac_users.creator_a +observer_a = CONF.rbac_users.observer_a +auditor_a = CONF.rbac_users.auditor_a +admin_b = CONF.rbac_users.admin_b +observer_b = CONF.rbac_users.observer_b + +test_user_data_when_enabled = { + 'with_admin_a': {'user': admin_a, 'expected_return': 200}, + 'with_creator_a': {'user': creator_a, 'expected_return': 403}, + 'with_observer_a': {'user': observer_a, 'expected_return': 403}, + 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, +} + +test_user_data_when_not_enabled = { + 'with_admin_a': {'user': admin_a, 'expected_return': 404}, + 'with_creator_a': {'user': creator_a, 'expected_return': 403}, + 'with_observer_a': {'user': observer_a, 'expected_return': 403}, + 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, +} + + +@utils.parameterized_test_case +class SecretStoresTestCase(base.TestCase): + """Functional tests exercising ACL Features""" + def setUp(self): + super(SecretStoresTestCase, self).setUp() + self.secret_behaviors = secret_behaviors.SecretBehaviors(self.client) + self.ss_behaviors = secretstores_behaviors.SecretStoresBehaviors( + self.client) + + def tearDown(self): + self.ss_behaviors.cleanup_preferred_secret_store_entities() + self.secret_behaviors.delete_all_created_secrets() + super(SecretStoresTestCase, self).tearDown() + + def _validate_secret_store_fields(self, secret_store): + self.assertIsNotNone(secret_store['name']) + self.assertIsNotNone(secret_store['secret_store_ref']) + self.assertIsNotNone(secret_store['secret_store_plugin']) + self.assertIsNotNone(secret_store['global_default']) + self.assertIsNotNone(secret_store['created']) + self.assertIsNotNone(secret_store['updated']) + self.assertEqual("ACTIVE", secret_store['status']) + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset(test_user_data_when_enabled) + def test_get_all_secret_stores_multiple_enabled(self, user, + expected_return): + + resp, json_data = self.ss_behaviors.get_all_secret_stores( + user_name=user) + + self.assertEqual(expected_return, resp.status_code) + if expected_return == 200: + self.assertIsNotNone(json_data['secret-stores']) + stores = json_data['secret-stores'] + for secret_store in stores: + self._validate_secret_store_fields(secret_store) + + @testcase.skipIf(base.conf_multiple_backends_enabled, 'executed only when ' + 'multiple backends support is NOT enabled in barbican ' + 'server side') + @utils.parameterized_dataset(test_user_data_when_not_enabled) + def test_get_all_secret_stores_multiple_disabled(self, user, + expected_return): + + resp, _ = self.ss_behaviors.get_all_secret_stores(user_name=user) + self.assertEqual(expected_return, resp.status_code) + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset(test_user_data_when_enabled) + def test_get_global_default_multiple_enabled(self, user, expected_return): + + resp, json_data = self.ss_behaviors.get_global_default(user_name=user) + + self.assertEqual(expected_return, resp.status_code) + if expected_return == 200: + self._validate_secret_store_fields(json_data) + + @testcase.skipIf(base.conf_multiple_backends_enabled, 'executed only when ' + 'multiple backends support is NOT enabled in barbican ' + 'server side') + @utils.parameterized_dataset(test_user_data_when_not_enabled) + def test_get_global_default_multiple_disabled(self, user, expected_return): + + resp, _ = self.ss_behaviors.get_global_default(user_name=user) + self.assertEqual(expected_return, resp.status_code) + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset(test_user_data_when_enabled) + def test_get_project_preferred_multiple_enabled(self, user, + expected_return): + + resp, json_data = self.ss_behaviors.get_all_secret_stores( + user_name=admin_a) + self.assertEqual(200, resp.status_code) + + stores = json_data['secret-stores'] + + store = stores[len(stores)-1] + secret_store_ref = store['secret_store_ref'] + resp = self.ss_behaviors.set_preferred_secret_store(secret_store_ref, + user_name=user) + + if resp.status_code == 204: + resp, json_data = self.ss_behaviors.get_project_preferred_store( + user_name=user) + + self.assertEqual(expected_return, resp.status_code) + if expected_return == 200: + self._validate_secret_store_fields(json_data) + self.assertEqual(store['secret_store_ref'], + json_data['secret_store_ref']) + + @testcase.skipIf(base.conf_multiple_backends_enabled, 'executed only when ' + 'multiple backends support is NOT enabled in barbican ' + 'server side') + @utils.parameterized_dataset(test_user_data_when_not_enabled) + def test_get_project_preferred_multiple_disabled(self, user, + expected_return): + + resp, _ = self.ss_behaviors.get_project_preferred_store(user_name=user) + self.assertEqual(expected_return, resp.status_code) + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset(test_user_data_when_enabled) + def test_get_a_secret_store_multiple_enabled(self, user, expected_return): + # read global default secret store via admin user as this is not a + # global default API check. + resp, json_data = self.ss_behaviors.get_global_default( + user_name=admin_a) + self.assertEqual(200, resp.status_code) + + resp, json_data = self.ss_behaviors.get_a_secret_store( + json_data['secret_store_ref'], user_name=user) + + self.assertEqual(expected_return, resp.status_code) + if expected_return == 200: + self._validate_secret_store_fields(json_data) + self.assertEqual(True, json_data['global_default']) + + @testcase.skipIf(base.conf_multiple_backends_enabled, 'executed only when ' + 'multiple backends support is NOT enabled in barbican ' + 'server side') + @utils.parameterized_dataset(test_user_data_when_not_enabled) + def test_get_a_secret_store_multiple_disabled(self, user, expected_return): + + resp, _ = self.ss_behaviors.get_project_preferred_store(user_name=user) + self.assertEqual(expected_return, resp.status_code) + + @testcase.skipUnless(base.conf_multiple_backends_enabled, 'executed only ' + 'when multiple backends support is enabled in ' + 'barbican server side') + @utils.parameterized_dataset(test_user_data_when_enabled) + def test_unset_project_preferred_store_multiple_enabled(self, user, + expected_return): + + resp, json_data = self.ss_behaviors.get_all_secret_stores( + user_name=admin_a) + self.assertEqual(200, resp.status_code) + + stores = json_data['secret-stores'] + + store = stores[len(stores)-1] + secret_store_ref = store['secret_store_ref'] + resp = self.ss_behaviors.set_preferred_secret_store(secret_store_ref, + user_name=user) + + if resp.status_code == 204: + # after setting project preference, get preferred will return 200 + resp, json_data = self.ss_behaviors.get_project_preferred_store( + user_name=user) + self.assertEqual(200, resp.status_code) + + # now, remove project preferred secret store + self.ss_behaviors.unset_preferred_secret_store( + json_data['secret_store_ref'], user_name=user) + # get project preferred call should now return 404 + resp, json_data = self.ss_behaviors.get_project_preferred_store( + user_name=user) + self.assertEqual(404, resp.status_code) diff --git a/functionaltests/common/config.py b/functionaltests/common/config.py index 284e67d6..14be4d0d 100644 --- a/functionaltests/common/config.py +++ b/functionaltests/common/config.py @@ -76,7 +76,8 @@ def setup_config(config_file=''): cfg.StrOpt('override_url', default=''), cfg.StrOpt('override_url_version', default=''), cfg.BoolOpt('verify_ssl', default=True), - cfg.BoolOpt('server_host_href_set', default=True) + cfg.BoolOpt('server_host_href_set', default=True), + cfg.BoolOpt('server_multiple_backends_enabled', default=False) ] TEST_CONF.register_group(keymanager_group) TEST_CONF.register_opts(keymanager_options, group=keymanager_group) From 88db8ceb9edbfd393d52662cd93a7f503fef6cbb Mon Sep 17 00:00:00 2001 From: Arun Kant Date: Wed, 14 Sep 2016 12:11:30 -0700 Subject: [PATCH 20/20] Adding reno release notes for multiple backend feature Change-Id: I97dbe0a549ed428ff6e652928f11766d17e529b3 Partially-Implements: blueprint multiple-secret-backend --- .../multiple-backends-75f5b85c63b930b7.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 releasenotes/notes/multiple-backends-75f5b85c63b930b7.yaml diff --git a/releasenotes/notes/multiple-backends-75f5b85c63b930b7.yaml b/releasenotes/notes/multiple-backends-75f5b85c63b930b7.yaml new file mode 100644 index 00000000..a418bc3c --- /dev/null +++ b/releasenotes/notes/multiple-backends-75f5b85c63b930b7.yaml @@ -0,0 +1,17 @@ +--- +prelude: > + Now within a single deployment, multiple secret store plugin backends can + be configured and used. With this change, a project adminstrator can + pre-define a preferred plugin backend for storing their secrets. New APIs + are added to manage this project level secret store preference. +features: + - New feature to support multiple secret store plugin backends. This feature + is not enabled by default. To use this feature, the relevant feature flag + needs to be enabled and supporting configuration needs to be added in the + service configuration. Once enabled, a project adminstrator will be able + to specify one of the available secret store backends as a preferred + secret store for their project secrets. This secret store preference + applies only to new secrets (key material) created or stored within that + project. Existing secrets are not impacted. See http://docs.openstack.org/developer/barbican/setup/plugin_backends.html + for instructions on how to setup Barbican multiple backends, and the API + documentation for further details. \ No newline at end of file