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/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 fc47fa1..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 @@ -80,3 +81,7 @@ 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 44fb05b..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 @@ -80,3 +81,7 @@ 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 b26f230..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 @@ -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 66f0f34..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 @@ -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 2d064d6..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 @@ -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 09b74e9..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 @@ -80,3 +81,7 @@ 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/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.