diff --git a/.zuul.yaml b/.zuul.yaml index 77dbdfe..76c9c62 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,6 +1,6 @@ - job: name: castellan-functional-vault - parent: openstack-tox-py27 + parent: openstack-tox-py36 description: | Run tox functional-vault target required-projects: @@ -51,16 +51,16 @@ jobs: - castellan-functional-vault - castellan-functional-devstack - - barbican-simple-crypto-devstack-tempest-castellan-from-git + - barbican-tempest-plugin-simple-crypto-castellan-src gate: jobs: - castellan-functional-vault - castellan-functional-devstack - - barbican-simple-crypto-devstack-tempest-castellan-from-git + - barbican-tempest-plugin-simple-crypto-castellan-src templates: - check-requirements - openstack-lower-constraints-jobs - - openstack-python3-ussuri-jobs + - openstack-python3-victoria-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index 15cd6cb..0000000 --- a/babel.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[python: **.py] - diff --git a/castellan/common/exception.py b/castellan/common/exception.py index 962a8d2..665b530 100644 --- a/castellan/common/exception.py +++ b/castellan/common/exception.py @@ -64,6 +64,14 @@ message = _("Key not found, uuid: %(uuid)s") +class InvalidManagedObjectDictError(CastellanException): + message = _("Dict has no field '%(field)s'.") + + +class UnknownManagedObjectTypeError(CastellanException): + message = _("Type not found, type: %(type)s") + + class AuthTypeInvalidError(CastellanException): message = _("Invalid auth_type was specified, auth_type: %(type)s") diff --git a/castellan/common/objects/__init__.py b/castellan/common/objects/__init__.py index e69de29..900a129 100644 --- a/castellan/common/objects/__init__.py +++ b/castellan/common/objects/__init__.py @@ -0,0 +1,49 @@ +# 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 castellan.common import exception +from castellan.common.objects import opaque_data +from castellan.common.objects import passphrase +from castellan.common.objects import private_key +from castellan.common.objects import public_key +from castellan.common.objects import symmetric_key +from castellan.common.objects import x_509 + +_managed_objects_by_type = { + cls.managed_type(): cls for cls in [ + opaque_data.OpaqueData, + passphrase.Passphrase, + private_key.PrivateKey, + public_key.PublicKey, + symmetric_key.SymmetricKey, + x_509.X509, + ] +} + + +def from_dict(obj, id=None): + try: + managed_object_type = obj["type"] + except KeyError: + raise exception.InvalidManagedObjectDictError(field="type") + + try: + cls = _managed_objects_by_type[managed_object_type] + except KeyError: + raise exception.UnknownManagedObjectTypeError(type=managed_object_type) + + try: + managed_object = cls.from_dict(obj, id) + except KeyError as e: + raise exception.InvalidManagedObjectDictError(field=str(e)) + + return managed_object diff --git a/castellan/common/objects/key.py b/castellan/common/objects/key.py index 448fd9f..850d5d9 100644 --- a/castellan/common/objects/key.py +++ b/castellan/common/objects/key.py @@ -22,14 +22,17 @@ """ import abc +import binascii +from castellan.common.objects import exception from castellan.common.objects import managed_object class Key(managed_object.ManagedObject): """Base class to represent all keys.""" - @abc.abstractproperty + @property + @abc.abstractmethod def algorithm(self): """Returns the key's algorithm. @@ -38,7 +41,8 @@ """ pass - @abc.abstractproperty + @property + @abc.abstractmethod def bit_length(self): """Returns the key's bit length. @@ -47,3 +51,33 @@ the length of the modulus. """ pass + + def to_dict(self): + dict_fields = super().to_dict() + + dict_fields["algorithm"] = self.algorithm + dict_fields["bit_length"] = self.bit_length + + return dict_fields + + @classmethod + def from_dict(cls, dict_fields, id=None, metadata_only=False): + try: + value = None + + # NOTE(moguimar): the managed object's value is exported as + # a hex string. For now, this is a compatibility thing with + # the already existent vault_key_manager backend. + if not metadata_only and dict_fields["value"] is not None: + value = binascii.unhexlify(dict_fields["value"]) + + return cls( + algorithm=dict_fields["algorithm"], + bit_length=dict_fields["bit_length"], + key=value, + name=dict_fields["name"], + created=dict_fields["created"], + id=id, + ) + except KeyError as e: + raise exception.InvalidManagedObjectDictError(field=str(e)) diff --git a/castellan/common/objects/managed_object.py b/castellan/common/objects/managed_object.py index a620c1b..660d097 100644 --- a/castellan/common/objects/managed_object.py +++ b/castellan/common/objects/managed_object.py @@ -19,7 +19,11 @@ This module defines the ManagedObject class. The ManagedObject class is the base class to represent all objects managed by the key manager. """ + import abc +import binascii + +from castellan.common import exception class ManagedObject(object, metaclass=abc.ABCMeta): @@ -69,7 +73,8 @@ """ return self._created - @abc.abstractproperty + @property + @abc.abstractmethod def format(self): """Returns the encoding format. @@ -77,6 +82,11 @@ encoded. """ pass + + @property + def value(self): + """Returns the managed object value.""" + return self.get_encoded() @abc.abstractmethod def get_encoded(self): @@ -90,3 +100,63 @@ def is_metadata_only(self): """Returns if the associated object is only metadata or not.""" return self.get_encoded() is None + + @classmethod + @abc.abstractmethod + def managed_type(cls): + """Returns the managed object type identifier. + + Returns the object's type identifier for serialization purpose. + """ + pass + + @classmethod + def from_dict(cls, dict_fields, id=None, metadata_only=False): + """Returns an instance of this class based on a dict object. + + :param dict_fields: The dictionary containing all necessary params + to create one instance. + :param id: The optional param 'id' to be passed to the constructor. + :param metadata_only: A switch to create an instance with metadata + only, without the secret itself. + """ + try: + value = None + + # NOTE(moguimar): the managed object's value is exported as + # a hex string. For now, this is a compatibility thing with + # the already existent vault_key_manager backend. + if not metadata_only and dict_fields["value"] is not None: + value = binascii.unhexlify(dict_fields["value"]) + + return cls( + value, + name=dict_fields["name"], + created=dict_fields["created"], + id=id, + ) + except KeyError as e: + raise exception.InvalidManagedObjectDictError(field=str(e)) + + def to_dict(self, metadata_only=False): + """Returns a dict that can be used with the from_dict() method. + + :param metadata_only: A switch to create an dictionary with metadata + only, without the secret itself. + + :rtype: dict + """ + value = None + + # NOTE(moguimar): the managed object's value is exported as + # a hex string. For now, this is a compatibility thing with + # the already existent vault_key_manager backend. + if not metadata_only and self.value is not None: + value = binascii.hexlify(self.value).decode("utf-8") + + return { + "type": self.managed_type(), + "name": self.name, + "created": self.created, + "value": value, + } diff --git a/castellan/common/objects/opaque_data.py b/castellan/common/objects/opaque_data.py index 9512ba2..201536f 100644 --- a/castellan/common/objects/opaque_data.py +++ b/castellan/common/objects/opaque_data.py @@ -31,15 +31,17 @@ Expected type for data is a bytestring. """ self._data = data - super(OpaqueData, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "opaque" @property def format(self): - """This method returns 'Opaque'.""" return "Opaque" def get_encoded(self): - """Returns the data in its original format.""" return self._data def __eq__(self, other): diff --git a/castellan/common/objects/passphrase.py b/castellan/common/objects/passphrase.py index e0441b5..86f1c92 100644 --- a/castellan/common/objects/passphrase.py +++ b/castellan/common/objects/passphrase.py @@ -31,15 +31,17 @@ The expected type for the passphrase is a bytestring. """ self._passphrase = passphrase - super(Passphrase, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "passphrase" @property def format(self): - """This method returns 'RAW'.""" return "RAW" def get_encoded(self): - """Returns the data in a bytestring.""" return self._passphrase def __eq__(self, other): diff --git a/castellan/common/objects/private_key.py b/castellan/common/objects/private_key.py index 6472ef8..f2525be 100644 --- a/castellan/common/objects/private_key.py +++ b/castellan/common/objects/private_key.py @@ -35,25 +35,25 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(PrivateKey, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "private" @property def algorithm(self): - """Returns the algorithm for asymmetric encryption.""" return self._alg @property def format(self): - """This method returns 'PKCS8'.""" return "PKCS8" @property def bit_length(self): - """Returns the key length.""" return self._bit_length def get_encoded(self): - """Returns the key in DER encoded format.""" return self._key def __eq__(self, other): diff --git a/castellan/common/objects/public_key.py b/castellan/common/objects/public_key.py index efcc6b5..680abd7 100644 --- a/castellan/common/objects/public_key.py +++ b/castellan/common/objects/public_key.py @@ -36,25 +36,25 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(PublicKey, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "public" @property def algorithm(self): - """Returns the algorithm for asymmetric encryption.""" return self._alg @property def format(self): - """This method returns 'SubjectPublicKeyInfo'.""" return "SubjectPublicKeyInfo" def get_encoded(self): - """Returns the key in its encoded format.""" return self._key @property def bit_length(self): - """Returns the key length.""" return self._bit_length def __eq__(self, other): diff --git a/castellan/common/objects/symmetric_key.py b/castellan/common/objects/symmetric_key.py index f9cefeb..c12c717 100644 --- a/castellan/common/objects/symmetric_key.py +++ b/castellan/common/objects/symmetric_key.py @@ -35,25 +35,25 @@ self._alg = algorithm self._bit_length = bit_length self._key = key - super(SymmetricKey, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "symmetric" @property def algorithm(self): - """Returns the algorithm for symmetric encryption.""" return self._alg @property def format(self): - """This method returns 'RAW'.""" return "RAW" def get_encoded(self): - """Returns the key in its encoded format.""" return self._key @property def bit_length(self): - """Returns the key length.""" return self._bit_length def __eq__(self, other): diff --git a/castellan/common/objects/x_509.py b/castellan/common/objects/x_509.py index aba1a88..82d0ca3 100644 --- a/castellan/common/objects/x_509.py +++ b/castellan/common/objects/x_509.py @@ -31,15 +31,17 @@ The data should be in a bytestring. """ self._data = data - super(X509, self).__init__(name=name, created=created, id=id) + super().__init__(name=name, created=created, id=id) + + @classmethod + def managed_type(cls): + return "certificate" @property def format(self): - """This method returns 'X.509'.""" return "X.509" def get_encoded(self): - """Returns the data in its encoded format.""" return self._data def __eq__(self, other): diff --git a/castellan/key_manager/barbican_key_manager.py b/castellan/key_manager/barbican_key_manager.py index 0fe63b8..9d01c84 100644 --- a/castellan/key_manager/barbican_key_manager.py +++ b/castellan/key_manager/barbican_key_manager.py @@ -193,7 +193,8 @@ return barbican.barbican_endpoint elif getattr(auth, 'service_catalog', None): endpoint_data = auth.service_catalog.endpoint_data_for( - service_type='key-manager') + service_type='key-manager', + interface=barbican.barbican_endpoint_type) return endpoint_data.url else: service_parameters = {'service_type': 'key-manager', diff --git a/castellan/key_manager/vault_key_manager.py b/castellan/key_manager/vault_key_manager.py index ad1424f..b0f8edf 100644 --- a/castellan/key_manager/vault_key_manager.py +++ b/castellan/key_manager/vault_key_manager.py @@ -41,6 +41,7 @@ _DEFAULT_VAULT_URL = "http://127.0.0.1:8200" _DEFAULT_MOUNTPOINT = "secret" +_DEFAULT_VERSION = 2 _vault_opts = [ cfg.StrOpt('root_token_id', @@ -53,6 +54,10 @@ default=_DEFAULT_MOUNTPOINT, help='Mountpoint of KV store in Vault to use, for example: ' '{}'.format(_DEFAULT_MOUNTPOINT)), + cfg.IntOpt('kv_version', + default=_DEFAULT_VERSION, + help='Version of KV store in Vault to use, for example: ' + '{}'.format(_DEFAULT_VERSION)), cfg.StrOpt('vault_url', default=_DEFAULT_VAULT_URL, help='Use this endpoint to connect to Vault, for example: ' @@ -92,41 +97,24 @@ self._approle_token_ttl = None self._approle_token_issue = None self._kv_mountpoint = self._conf.vault.kv_mountpoint + self._kv_version = self._conf.vault.kv_version self._vault_url = self._conf.vault.vault_url if self._vault_url.startswith("https://"): self._verify_server = self._conf.vault.ssl_ca_crt_file or True else: self._verify_server = False - self._vault_kv_version = None def _get_url(self): if not self._vault_url.endswith('/'): self._vault_url += '/' return self._vault_url - def _get_api_version(self): - if self._vault_kv_version: - return self._vault_kv_version - - resource_url = '{}v1/sys/internal/ui/mounts/{}'.format( - self._get_url(), - self._kv_mountpoint - ) - resp = self._do_http_request(self._session.get, resource_url) - - if resp.status_code == requests.codes['not_found']: - self._vault_kv_version = '1' - else: - self._vault_kv_version = resp.json()['data']['options']['version'] - - return self._vault_kv_version - def _get_resource_url(self, key_id=None): return '{}v1/{}/{}{}'.format( self._get_url(), self._kv_mountpoint, - '' if self._get_api_version() == '1' else + '' if self._kv_version == 1 else 'data/' if key_id else 'metadata/', # no key_id is for listing and 'data/' doesn't works @@ -173,10 +161,14 @@ if resp.status_code == requests.codes['forbidden']: raise exception.Forbidden() - resp = resp.json() - self._cached_approle_token_id = resp['auth']['client_token'] + resp_data = resp.json() + + if resp.status_code == requests.codes['bad_request']: + raise exception.KeyManagerError(', '.join(resp_data['errors'])) + + self._cached_approle_token_id = resp_data['auth']['client_token'] self._approle_token_issue = token_issue_utc - self._approle_token_ttl = resp['auth']['lease_duration'] + self._approle_token_ttl = resp_data['auth']['lease_duration'] return {'X-Vault-Token': self._approle_token_id} return {} @@ -264,7 +256,7 @@ 'name': value.name, 'created': value.created } - if self._get_api_version() != '1': + if self._kv_version > 1: record = {'data': record} self._do_http_request(self._session.post, @@ -309,7 +301,7 @@ raise exception.ManagedObjectNotFoundError(uuid=key_id) record = resp.json()['data'] - if self._get_api_version() != '1': + if self._kv_version > 1: record = record['data'] key = None if metadata_only else binascii.unhexlify(record['value']) diff --git a/castellan/tests/unit/credentials/test_keystone_password.py b/castellan/tests/unit/credentials/test_keystone_password.py index 81874c7..46e8452 100644 --- a/castellan/tests/unit/credentials/test_keystone_password.py +++ b/castellan/tests/unit/credentials/test_keystone_password.py @@ -117,7 +117,7 @@ self.ks_password_credential) self.assertFalse(self.ks_password_credential is None) - self.assertFalse(None == self.ks_password_credential) + self.assertFalse(None == self.ks_password_credential) # noqa: E711 other_ks_password_credential = keystone_password.KeystonePassword( self.password, @@ -140,7 +140,7 @@ def test___ne___none(self): self.assertTrue(self.ks_password_credential is not None) - self.assertTrue(None != self.ks_password_credential) + self.assertTrue(None != self.ks_password_credential) # noqa: E711 def test___ne___password(self): other_password = "wheresmyCat??" diff --git a/castellan/tests/unit/credentials/test_keystone_token.py b/castellan/tests/unit/credentials/test_keystone_token.py index b487847..fff44dc 100644 --- a/castellan/tests/unit/credentials/test_keystone_token.py +++ b/castellan/tests/unit/credentials/test_keystone_token.py @@ -93,7 +93,7 @@ self.ks_token_credential) self.assertFalse(self.ks_token_credential is None) - self.assertFalse(None == self.ks_token_credential) + self.assertFalse(None == self.ks_token_credential) # noqa: E711 other_ks_token_credential = keystone_token.KeystoneToken( self.token, @@ -112,7 +112,7 @@ def test___ne___none(self): self.assertTrue(self.ks_token_credential is not None) - self.assertTrue(None != self.ks_token_credential) + self.assertTrue(None != self.ks_token_credential) # noqa: E711 def test___ne___token(self): other_token = "5c59e3217d3d4dd297589b297aee2a6f" diff --git a/castellan/tests/unit/credentials/test_password.py b/castellan/tests/unit/credentials/test_password.py index 26e2ea6..8485390 100644 --- a/castellan/tests/unit/credentials/test_password.py +++ b/castellan/tests/unit/credentials/test_password.py @@ -46,7 +46,7 @@ self.assertTrue(self.password_credential is self.password_credential) self.assertFalse(self.password_credential is None) - self.assertFalse(None == self.password_credential) + self.assertFalse(None == self.password_credential) # noqa: E711 other_password_credential = password.Password(self.username, self.password) @@ -55,7 +55,7 @@ def test___ne___none(self): self.assertTrue(self.password_credential is not None) - self.assertTrue(None != self.password_credential) + self.assertTrue(None != self.password_credential) # noqa: E711 def test___ne___username(self): other_username = "service" diff --git a/castellan/tests/unit/credentials/test_token.py b/castellan/tests/unit/credentials/test_token.py index 8e40a7e..177c29d 100644 --- a/castellan/tests/unit/credentials/test_token.py +++ b/castellan/tests/unit/credentials/test_token.py @@ -39,7 +39,7 @@ self.assertTrue(self.token_credential is self.token_credential) self.assertFalse(self.token_credential is None) - self.assertFalse(None == self.token_credential) + self.assertFalse(None == self.token_credential) # noqa: E711 other_token_credential = token.Token(self.token) self.assertTrue(self.token_credential == other_token_credential) @@ -47,7 +47,7 @@ def test___ne___none(self): self.assertTrue(self.token_credential is not None) - self.assertTrue(None != self.token_credential) + self.assertTrue(None != self.token_credential) # noqa: E711 def test___ne___token(self): other_token = "fe32af1fe47e4744a48254e60ae80012" diff --git a/castellan/tests/unit/key_manager/test_barbican_key_manager.py b/castellan/tests/unit/key_manager/test_barbican_key_manager.py index 36e842d..4d6c9bb 100644 --- a/castellan/tests/unit/key_manager/test_barbican_key_manager.py +++ b/castellan/tests/unit/key_manager/test_barbican_key_manager.py @@ -17,9 +17,9 @@ Test cases for the barbican key manager. """ import calendar +from unittest import mock from barbicanclient import exceptions as barbican_exceptions -import mock from oslo_utils import timeutils from castellan.common import exception diff --git a/castellan/tests/unit/key_manager/test_migration_key_manager.py b/castellan/tests/unit/key_manager/test_migration_key_manager.py index e5c8ba6..ffba620 100644 --- a/castellan/tests/unit/key_manager/test_migration_key_manager.py +++ b/castellan/tests/unit/key_manager/test_migration_key_manager.py @@ -18,7 +18,7 @@ """ import binascii -import mock +from unittest import mock from oslo_config import cfg diff --git a/castellan/tests/unit/objects/__init__.py b/castellan/tests/unit/objects/__init__.py index e69de29..da8240a 100644 --- a/castellan/tests/unit/objects/__init__.py +++ b/castellan/tests/unit/objects/__init__.py @@ -0,0 +1,37 @@ +# Copyright 2020 Red Hat, Inc. +# All Rights Reserved. +# +# 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. + +""" +Test cases for Managed Objects. +""" +from castellan.common import exception +from castellan.common import objects +from castellan.tests import base + + +class ManagedObjectFromDictTestCase(base.TestCase): + def test_invalid_dict(self): + self.assertRaises( + exception.InvalidManagedObjectDictError, + objects.from_dict, + {}, + ) + + def test_unknown_type(self): + self.assertRaises( + exception.UnknownManagedObjectTypeError, + objects.from_dict, + {"type": "non-existing-managed-object-type"}, + ) diff --git a/castellan/tests/unit/objects/test_opaque.py b/castellan/tests/unit/objects/test_opaque.py index 48efe88..d7235c7 100644 --- a/castellan/tests/unit/objects/test_opaque.py +++ b/castellan/tests/unit/objects/test_opaque.py @@ -16,6 +16,7 @@ """ Test cases for the opaque data class. """ +from castellan.common import objects from castellan.common.objects import opaque_data from castellan.tests import base @@ -67,7 +68,7 @@ self.assertTrue(self.opaque_data is self.opaque_data) self.assertFalse(self.opaque_data is None) - self.assertFalse(None == self.opaque_data) + self.assertFalse(None == self.opaque_data) # noqa: E711 other_opaque_data = opaque_data.OpaqueData(self.data) self.assertTrue(self.opaque_data == other_opaque_data) @@ -75,8 +76,12 @@ def test___ne___none(self): self.assertTrue(self.opaque_data is not None) - self.assertTrue(None != self.opaque_data) + self.assertTrue(None != self.opaque_data) # noqa: E711 def test___ne___data(self): other_opaque = opaque_data.OpaqueData(b'other data', self.name) self.assertTrue(self.opaque_data != other_opaque) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.opaque_data.to_dict()) + self.assertEqual(self.opaque_data, other) diff --git a/castellan/tests/unit/objects/test_passphrase.py b/castellan/tests/unit/objects/test_passphrase.py index d33f790..7e0cdd4 100644 --- a/castellan/tests/unit/objects/test_passphrase.py +++ b/castellan/tests/unit/objects/test_passphrase.py @@ -16,6 +16,7 @@ """ Test cases for the passphrase class. """ +from castellan.common import objects from castellan.common.objects import passphrase from castellan.tests import base @@ -67,7 +68,7 @@ self.assertTrue(self.passphrase is self.passphrase) self.assertFalse(self.passphrase is None) - self.assertFalse(None == self.passphrase) + self.assertFalse(None == self.passphrase) # noqa: E711 other_passphrase = passphrase.Passphrase(self.passphrase_data) self.assertTrue(self.passphrase == other_passphrase) @@ -75,8 +76,12 @@ def test___ne___none(self): self.assertTrue(self.passphrase is not None) - self.assertTrue(None != self.passphrase) + self.assertTrue(None != self.passphrase) # noqa: E711 def test___ne___data(self): other_phrase = passphrase.Passphrase(b"other passphrase", self.name) self.assertTrue(self.passphrase != other_phrase) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.passphrase.to_dict()) + self.assertEqual(self.passphrase, other) diff --git a/castellan/tests/unit/objects/test_private_key.py b/castellan/tests/unit/objects/test_private_key.py index 8eeda83..8c734df 100644 --- a/castellan/tests/unit/objects/test_private_key.py +++ b/castellan/tests/unit/objects/test_private_key.py @@ -16,6 +16,7 @@ """ Test cases for the private key class. """ +from castellan.common import objects from castellan.common.objects import private_key from castellan.tests import base from castellan.tests import utils @@ -83,7 +84,7 @@ self.assertTrue(self.key is self.key) self.assertFalse(self.key is None) - self.assertFalse(None == self.key) + self.assertFalse(None == self.key) # noqa: E711 other_key = private_key.PrivateKey(self.algorithm, self.bit_length, @@ -93,7 +94,7 @@ def test___ne___none(self): self.assertTrue(self.key is not None) - self.assertTrue(None != self.key) + self.assertTrue(None != self.key) # noqa: E711 def test___ne___algorithm(self): other_key = private_key.PrivateKey('DSA', @@ -116,3 +117,7 @@ different_encoded, self.name) self.assertTrue(self.key != other_key) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.key.to_dict()) + self.assertEqual(self.key, other) diff --git a/castellan/tests/unit/objects/test_public_key.py b/castellan/tests/unit/objects/test_public_key.py index aa5bd08..1c68e88 100644 --- a/castellan/tests/unit/objects/test_public_key.py +++ b/castellan/tests/unit/objects/test_public_key.py @@ -16,6 +16,7 @@ """ Test cases for the public key class. """ +from castellan.common import objects from castellan.common.objects import public_key from castellan.tests import base from castellan.tests import utils @@ -83,7 +84,7 @@ self.assertTrue(self.key is self.key) self.assertFalse(self.key is None) - self.assertFalse(None == self.key) + self.assertFalse(None == self.key) # noqa: E711 other_key = public_key.PublicKey(self.algorithm, self.bit_length, @@ -93,7 +94,7 @@ def test___ne___none(self): self.assertTrue(self.key is not None) - self.assertTrue(None != self.key) + self.assertTrue(None != self.key) # noqa: E711 def test___ne___algorithm(self): other_key = public_key.PublicKey('DSA', @@ -116,3 +117,7 @@ different_encoded, self.name) self.assertTrue(self.key != other_key) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.key.to_dict()) + self.assertEqual(self.key, other) diff --git a/castellan/tests/unit/objects/test_symmetric_key.py b/castellan/tests/unit/objects/test_symmetric_key.py index b1410d7..7b701ef 100644 --- a/castellan/tests/unit/objects/test_symmetric_key.py +++ b/castellan/tests/unit/objects/test_symmetric_key.py @@ -16,6 +16,7 @@ """ Test cases for the symmetric key class. """ +from castellan.common import objects from castellan.common.objects import symmetric_key as sym_key from castellan.tests import base @@ -82,7 +83,7 @@ self.assertTrue(self.key is self.key) self.assertFalse(self.key is None) - self.assertFalse(None == self.key) + self.assertFalse(None == self.key) # noqa: E711 other_key = sym_key.SymmetricKey(self.algorithm, self.bit_length, @@ -92,7 +93,7 @@ def test___ne___none(self): self.assertTrue(self.key is not None) - self.assertTrue(None != self.key) + self.assertTrue(None != self.key) # noqa: E711 def test___ne___algorithm(self): other_key = sym_key.SymmetricKey('DES', @@ -115,3 +116,7 @@ different_encoded, self.name) self.assertTrue(self.key != other_key) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.key.to_dict()) + self.assertEqual(self.key, other) diff --git a/castellan/tests/unit/objects/test_x_509.py b/castellan/tests/unit/objects/test_x_509.py index af06c51..d841805 100644 --- a/castellan/tests/unit/objects/test_x_509.py +++ b/castellan/tests/unit/objects/test_x_509.py @@ -16,6 +16,7 @@ """ Test cases for the X.509 certificate class. """ +from castellan.common import objects from castellan.common.objects import x_509 from castellan.tests import base from castellan.tests import utils @@ -67,7 +68,7 @@ self.assertTrue(self.cert is self.cert) self.assertFalse(self.cert is None) - self.assertFalse(None == self.cert) + self.assertFalse(None == self.cert) # noqa: E711 other_x_509 = x_509.X509(self.data) self.assertTrue(self.cert == other_x_509) @@ -75,8 +76,12 @@ def test___ne___none(self): self.assertTrue(self.cert is not None) - self.assertTrue(None != self.cert) + self.assertTrue(None != self.cert) # noqa: E711 def test___ne___data(self): other_x509 = x_509.X509(b'\x00\x00\x00', self.name) self.assertTrue(self.cert != other_x509) + + def test_to_and_from_dict(self): + other = objects.from_dict(self.cert.to_dict()) + self.assertEqual(self.cert, other) diff --git a/doc/requirements.txt b/doc/requirements.txt index 6f8e380..2123e2e 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -sphinx>=1.8.0,!=2.1.0 # BSD +sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD -reno>=2.5.0 # Apache-2.0 -openstackdocstheme>=1.18.1 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 +openstackdocstheme>=2.2.1 # Apache-2.0 diff --git a/doc/source/conf.py b/doc/source/conf.py index 6e6d89f..29813ad 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -48,7 +48,7 @@ add_module_names = True # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- @@ -59,11 +59,6 @@ # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' -html_last_updated_fmt = '%Y-%m-%d %H:%M' # Add any paths that contain "extra" files, such as .htaccess or # robots.txt. @@ -90,6 +85,8 @@ #intersphinx_mapping = {'https://docs.python.org/3/': None} # -- Options for openstackdocstheme ------------------------------------------- -repository_name = 'openstack/castellan' -bug_project = 'castellan' -bug_tag = '' +openstackdocs_repo_name = 'openstack/castellan' +openstackdocs_pdf_link = True +openstackdocs_auto_name = False +openstackdocs_bug_project = 'castellan' +openstackdocs_bug_tag = '' diff --git a/lower-constraints.txt b/lower-constraints.txt index 20fbd11..ad7996f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -1,31 +1,23 @@ -alabaster==0.7.10 appdirs==1.3.0 asn1crypto==0.23.0 -Babel==2.3.4 -bandit==1.1.0 -cffi==1.7.0 +certifi==2020.4.5.2 +cffi==1.13.2 +chardet==3.0.4 cliff==2.8.0 cmd2==0.8.0 coverage==4.0 cryptography==2.1 debtcollector==1.2.0 -docutils==0.11 -dulwich==0.15.0 +entrypoints==0.3 extras==1.0.0 fixtures==3.0.0 -flake8==2.5.5 +future==0.18.2 gitdb==0.6.4 GitPython==1.0.1 -hacking==0.12.0 idna==2.5 -imagesize==0.7.1 iso8601==0.1.11 -Jinja2==2.10 keystoneauth1==3.4.0 linecache2==1.0.0 -MarkupSafe==1.0 -mccabe==0.2.1 -mock==2.0.0 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 @@ -40,12 +32,9 @@ oslo.utils==3.33.0 oslotest==3.2.0 pbr==2.0.0 -pep8==1.5.7 pifpaf==0.10.0 prettytable==0.7.2 pycparser==2.18 -pyflakes==0.8.1 -Pygments==2.2.0 pyinotify==0.9.6 pyparsing==2.1.0 pyperclip==1.5.27 @@ -58,13 +47,16 @@ requests==2.18.0 requestsexceptions==1.2.0 rfc3986==0.3.1 +six==1.15.0 smmap==0.9.0 -snowballstemmer==1.2.1 +stestr==2.0.0 stevedore==1.20.0 -stestr==2.0.0 +testrepository==0.0.20 testscenarios==0.4 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 +urllib3==1.21.1 +voluptuous==0.11.7 wrapt==1.7.0 xattr==0.9.2 diff --git a/releasenotes/notes/add-to-dict-and-from-dict-conversions-to-managed-objects-95a9f0fdbd371a87.yaml b/releasenotes/notes/add-to-dict-and-from-dict-conversions-to-managed-objects-95a9f0fdbd371a87.yaml new file mode 100644 index 0000000..baaaade --- /dev/null +++ b/releasenotes/notes/add-to-dict-and-from-dict-conversions-to-managed-objects-95a9f0fdbd371a87.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Historically, the vault key manager backend converts its managed objects + to dictionaries in order to send them as a json object. To promote + cross-backend compatibility, suck feature should be migrated to managed + objects. Methods from_dict() and to_dict() added to class ManagedObject. + The Method from_dict() is a class method to create instances based on a + dictionary while the method to_dict() is an instance method to translate + an instance to a dictionary. diff --git a/releasenotes/notes/fix-vault-flaky-kv-api-version-b0cd9d62a39d2907.yaml b/releasenotes/notes/fix-vault-flaky-kv-api-version-b0cd9d62a39d2907.yaml new file mode 100644 index 0000000..1b0467d --- /dev/null +++ b/releasenotes/notes/fix-vault-flaky-kv-api-version-b0cd9d62a39d2907.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + In some situations, vault will not provide KV API version in the options + structure. Vault documentation [1] doesn't cover cases when KV API version + is not provided. A new configuration option, with default value equivalent + to the latest KV API version available (kv_version=2) was added to allow + precise configuration of the KV API being used. + + [1] https://learn.hashicorp.com/vault/secrets-management/sm-versioned-kv diff --git a/releasenotes/notes/use-barbican-endpoint-type-config-option-e583d30930cc22ba.yaml b/releasenotes/notes/use-barbican-endpoint-type-config-option-e583d30930cc22ba.yaml new file mode 100644 index 0000000..11baafd --- /dev/null +++ b/releasenotes/notes/use-barbican-endpoint-type-config-option-e583d30930cc22ba.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + ``barbican_endpoint_type`` is now used to retrieve Barbican endpoint URL + from service catalog. This config option is set to 'public' by default so + it will not change the current behaviour. diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index cbbe930..1dde6ae 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -52,9 +52,10 @@ master_doc = 'index' # General information about the project. -repository_name = 'openstack/castellan' -bug_project = 'castellan' -bug_tag = 'doc' +openstackdocs_repo_name = 'openstack/castellan' +openstackdocs_auto_name = False +openstackdocs_bug_project = 'castellan' +openstackdocs_bug_tag = 'doc' project = u'Castellan Release Notes' copyright = u'2017, Castellan Developers' @@ -95,7 +96,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -143,10 +144,6 @@ # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -html_last_updated_fmt = '%Y-%m-%d %H:%M' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index f44a132..cebc814 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + ussuri train stein rocky diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 0000000..e21e50e --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri diff --git a/requirements.txt b/requirements.txt index 5bec0f2..369de27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -Babel!=2.4.0,>=2.3.4 # BSD cryptography>=2.1 # BSD/Apache-2.0 python-barbicanclient>=4.5.2 # Apache-2.0 oslo.config>=6.4.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 84a3348..1c1ab27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython @@ -35,17 +36,3 @@ castellan.drivers = barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager vault = castellan.key_manager.vault_key_manager:VaultKeyManager - -[compile_catalog] -directory = castellan/locale -domain = castellan - -[update_catalog] -domain = castellan -output_dir = castellan/locale -input_file = castellan/locale/castellan.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = castellan/locale/castellan.pot diff --git a/setup.py b/setup.py index 566d844..cd35c3c 100644 --- a/setup.py +++ b/setup.py @@ -13,16 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools - -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass setuptools.setup( setup_requires=['pbr>=2.0.0'], diff --git a/test-requirements.txt b/test-requirements.txt index b7cd592..1a0b2f5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,11 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +hacking>=3.0.1,<3.1.0 # Apache-2.0 +# remove this pyflakes from here once you bump the +# hacking to 3.2.0 or above. hacking 3.2.0 takes +# care of pyflakes version compatibilty. +pyflakes>=2.1.1 coverage!=4.4,>=4.0 # Apache-2.0 python-barbicanclient>=4.5.2 # Apache-2.0 @@ -11,5 +15,5 @@ fixtures>=3.0.0 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT -bandit>=1.1.0,<1.6.0 # Apache-2.0 +bandit>=1.6.0,<1.7.0 # Apache-2.0 pifpaf>=0.10.0 # Apache-2.0 diff --git a/tools/setup-vault-env.sh b/tools/setup-vault-env.sh index 0c79924..c9d3ad3 100755 --- a/tools/setup-vault-env.sh +++ b/tools/setup-vault-env.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eux if [ -z "$(which vault)" ]; then - VAULT_VERSION=0.10.4 + VAULT_VERSION=1.4.2 SUFFIX=zip case `uname -s` in Darwin) diff --git a/tox.ini b/tox.ini index 9790df8..1458e59 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.1.1 -envlist = py37,pep8 +envlist = py38,pep8 ignore_basepython_conflict = True skipsdist = True @@ -11,7 +11,7 @@ VIRTUAL_ENV={envdir} OS_TEST_PATH=./castellan/tests/unit deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/ussuri} + -c{env:TOX_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} @@ -78,7 +78,6 @@ [testenv:functional] usedevelop = True -install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_TEST_PATH=./castellan/tests/functional @@ -87,7 +86,6 @@ [testenv:functional-vault] passenv = HOME usedevelop = True -install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} OS_TEST_PATH=./castellan/tests/functional