Merge "Create a fernet credential provider"
This commit is contained in:
commit
4d5bcb1ee8
@ -14,3 +14,4 @@
|
||||
|
||||
from keystone.credential import controllers # noqa
|
||||
from keystone.credential.core import * # noqa
|
||||
from keystone.credential import provider # noqa
|
||||
|
27
keystone/credential/provider.py
Normal file
27
keystone/credential/provider.py
Normal file
@ -0,0 +1,27 @@
|
||||
# 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 keystone.common import dependency
|
||||
from keystone.common import manager
|
||||
import keystone.conf
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
|
||||
|
||||
@dependency.provider('credential_provider_api')
|
||||
class Manager(manager.Manager):
|
||||
|
||||
driver_namespace = 'keystone.credential.provider'
|
||||
|
||||
def __init__(self):
|
||||
super(Manager, self).__init__(CONF.credential.provider)
|
0
keystone/credential/providers/__init__.py
Normal file
0
keystone/credential/providers/__init__.py
Normal file
38
keystone/credential/providers/core.py
Normal file
38
keystone/credential/providers/core.py
Normal file
@ -0,0 +1,38 @@
|
||||
# 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 abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Provider(object):
|
||||
"""Interface for credential providers that support encryption."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def encrypt(self, credential):
|
||||
"""Encrypt a credential.
|
||||
|
||||
:param str credential: credential to encrypt
|
||||
:returns: encrypted credential str
|
||||
:raises: keystone.exception.CredentialEncryptionError
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def decrypt(self, credential):
|
||||
"""Decrypt a credential.
|
||||
|
||||
:param str credential: credential to decrypt
|
||||
:returns: credential str as plaintext
|
||||
:raises: keystone.exception.CredentialEncryptionError
|
||||
"""
|
13
keystone/credential/providers/fernet/__init__.py
Normal file
13
keystone/credential/providers/fernet/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# 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 keystone.credential.providers.fernet.core import * # noqa
|
80
keystone/credential/providers/fernet/core.py
Normal file
80
keystone/credential/providers/fernet/core.py
Normal file
@ -0,0 +1,80 @@
|
||||
# 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 cryptography import fernet
|
||||
from oslo_log import log
|
||||
|
||||
from keystone.common import fernet_utils
|
||||
import keystone.conf
|
||||
from keystone.credential.providers import core
|
||||
from keystone import exception
|
||||
from keystone.i18n import _
|
||||
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
# NOTE(lbragstad): Credential key rotation operates slightly different than
|
||||
# Fernet key rotation. Each credential holds a hash of the key that encrypted
|
||||
# it. This is important for credential key rotation because it helps us make
|
||||
# sure we don't over-rotate credential keys. During a rotation of credential
|
||||
# keys, if any credential has not been re-encrypted with the current primary
|
||||
# key, we can abandon the key rotation until all credentials have been migrated
|
||||
# to the new primary key. If we don't take this step, it is possible that we
|
||||
# could remove a key used to encrypt credentials, leaving them recoverable.
|
||||
# This also means that we don't need to expose a `[credential] max_active_keys`
|
||||
# option through configuration. Instead we will use a global configuration and
|
||||
# share that across all places that need to use FernetUtils for credential
|
||||
# encryption.
|
||||
MAX_ACTIVE_KEYS = 3
|
||||
|
||||
|
||||
class Provider(core.Provider):
|
||||
|
||||
@property
|
||||
def crypto(self):
|
||||
keys = [fernet.Fernet(key) for key in self._get_encryption_keys()]
|
||||
return fernet.MultiFernet(keys)
|
||||
|
||||
def _get_encryption_keys(self):
|
||||
self.key_utils = fernet_utils.FernetUtils(
|
||||
CONF.credential.key_repository, MAX_ACTIVE_KEYS
|
||||
)
|
||||
return self.key_utils.load_keys()
|
||||
|
||||
def encrypt(self, credential):
|
||||
"""Attempt to encrypt a plaintext credential.
|
||||
|
||||
:param credential: a plaintext representation of a credential
|
||||
:returns: an encrypted credential
|
||||
"""
|
||||
try:
|
||||
return self.crypto.encrypt(credential.encode('utf-8'))
|
||||
except (TypeError, ValueError):
|
||||
msg = _('Credential could not be encrypted. Please contact the'
|
||||
' administrator')
|
||||
LOG.error(msg)
|
||||
raise exception.CredentialEncryptionError(msg)
|
||||
|
||||
def decrypt(self, credential):
|
||||
"""Attempt to decrypt a credential.
|
||||
|
||||
:param credential: an encrypted credential string
|
||||
:returns: a decrypted credential
|
||||
"""
|
||||
try:
|
||||
return self.crypto.decrypt(bytes(credential)).decode('utf-8')
|
||||
except (fernet.InvalidToken, TypeError, ValueError):
|
||||
msg = _('Credential could not be decrypted. Please contact the'
|
||||
' administrator')
|
||||
LOG.error(msg)
|
||||
raise exception.CredentialEncryptionError(msg)
|
0
keystone/tests/unit/credential/__init__.py
Normal file
0
keystone/tests/unit/credential/__init__.py
Normal file
67
keystone/tests/unit/credential/test_fernet_provider.py
Normal file
67
keystone/tests/unit/credential/test_fernet_provider.py
Normal file
@ -0,0 +1,67 @@
|
||||
# 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 shutil
|
||||
import uuid
|
||||
|
||||
import keystone.conf
|
||||
from keystone.credential.providers import fernet
|
||||
from keystone import exception
|
||||
from keystone.tests import unit
|
||||
from keystone.tests.unit import ksfixtures
|
||||
from keystone.tests.unit.ksfixtures import database
|
||||
|
||||
CONF = keystone.conf.CONF
|
||||
|
||||
|
||||
class TestFernetCredentialProvider(unit.TestCase):
|
||||
def setUp(self):
|
||||
super(TestFernetCredentialProvider, self).setUp()
|
||||
self.provider = fernet.Provider()
|
||||
self.useFixture(database.Database())
|
||||
self.useFixture(
|
||||
ksfixtures.KeyRepository(
|
||||
self.config_fixture,
|
||||
'credential',
|
||||
fernet.MAX_ACTIVE_KEYS
|
||||
)
|
||||
)
|
||||
|
||||
def config_overrides(self):
|
||||
super(TestFernetCredentialProvider, self).config_overrides()
|
||||
|
||||
def test_valid_data_encryption(self):
|
||||
blob = uuid.uuid4().hex
|
||||
encrypted_blob = self.provider.encrypt(blob)
|
||||
decrypted_blob = self.provider.decrypt(encrypted_blob)
|
||||
|
||||
self.assertNotEqual(blob, encrypted_blob)
|
||||
self.assertEqual(blob, decrypted_blob)
|
||||
|
||||
def test_encrypt_with_invalid_key_raises_exception(self):
|
||||
shutil.rmtree(CONF.credential.key_repository)
|
||||
blob = uuid.uuid4().hex
|
||||
self.assertRaises(
|
||||
exception.CredentialEncryptionError,
|
||||
self.provider.encrypt,
|
||||
blob
|
||||
)
|
||||
|
||||
def test_decrypt_with_invalid_key_raises_exception(self):
|
||||
blob = uuid.uuid4().hex
|
||||
encrypted_blob = self.provider.encrypt(blob)
|
||||
shutil.rmtree(CONF.credential.key_repository)
|
||||
self.assertRaises(
|
||||
exception.CredentialEncryptionError,
|
||||
self.provider.decrypt,
|
||||
encrypted_blob
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user