aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/token/providers/fernet
diff options
context:
space:
mode:
authorWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
committerWuKong <rebirthmonkey@gmail.com>2015-06-30 18:47:29 +0200
commitb8c756ecdd7cced1db4300935484e8c83701c82e (patch)
tree87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/token/providers/fernet
parentc304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff)
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/token/providers/fernet')
-rw-r--r--keystone-moon/keystone/token/providers/fernet/__init__.py13
-rw-r--r--keystone-moon/keystone/token/providers/fernet/core.py267
-rw-r--r--keystone-moon/keystone/token/providers/fernet/token_formatters.py545
-rw-r--r--keystone-moon/keystone/token/providers/fernet/utils.py243
4 files changed, 1068 insertions, 0 deletions
diff --git a/keystone-moon/keystone/token/providers/fernet/__init__.py b/keystone-moon/keystone/token/providers/fernet/__init__.py
new file mode 100644
index 00000000..953ef624
--- /dev/null
+++ b/keystone-moon/keystone/token/providers/fernet/__init__.py
@@ -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.token.providers.fernet.core import * # noqa
diff --git a/keystone-moon/keystone/token/providers/fernet/core.py b/keystone-moon/keystone/token/providers/fernet/core.py
new file mode 100644
index 00000000..b1da263b
--- /dev/null
+++ b/keystone-moon/keystone/token/providers/fernet/core.py
@@ -0,0 +1,267 @@
+# 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 oslo_config import cfg
+from oslo_log import log
+
+from keystone.common import dependency
+from keystone.contrib import federation
+from keystone import exception
+from keystone.i18n import _
+from keystone.token import provider
+from keystone.token.providers import common
+from keystone.token.providers.fernet import token_formatters as tf
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('trust_api')
+class Provider(common.BaseProvider):
+ def __init__(self, *args, **kwargs):
+ super(Provider, self).__init__(*args, **kwargs)
+
+ self.token_formatter = tf.TokenFormatter()
+
+ def needs_persistence(self):
+ """Should the token be written to a backend."""
+ return False
+
+ def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None):
+ """Issue a V2 formatted token.
+
+ :param token_ref: reference describing the token
+ :param roles_ref: reference describing the roles for the token
+ :param catalog_ref: reference describing the token's catalog
+ :returns: tuple containing the ID of the token and the token data
+
+ """
+ # TODO(lbragstad): Currently, Fernet tokens don't support bind in the
+ # token format. Raise a 501 if we're dealing with bind.
+ if token_ref.get('bind'):
+ raise exception.NotImplemented()
+
+ user_id = token_ref['user']['id']
+ # Default to password since methods not provided by token_ref
+ method_names = ['password']
+ project_id = None
+ # Verify that tenant is not None in token_ref
+ if token_ref.get('tenant'):
+ project_id = token_ref['tenant']['id']
+
+ parent_audit_id = token_ref.get('parent_audit_id')
+ # If parent_audit_id is defined then a token authentication was made
+ if parent_audit_id:
+ method_names.append('token')
+
+ audit_ids = provider.audit_info(parent_audit_id)
+
+ # Get v3 token data and exclude building v3 specific catalog. This is
+ # due to the fact that the V2TokenDataHelper.format_token() method
+ # doesn't build any of the token_reference from other Keystone APIs.
+ # Instead, it builds it from what is persisted in the token reference.
+ # Here we are going to leverage the V3TokenDataHelper.get_token_data()
+ # method written for V3 because it goes through and populates the token
+ # reference dynamically. Once we have a V3 token reference, we can
+ # attempt to convert it to a V2 token response.
+ v3_token_data = self.v3_token_data_helper.get_token_data(
+ user_id,
+ method_names,
+ project_id=project_id,
+ token=token_ref,
+ include_catalog=False,
+ audit_info=audit_ids)
+
+ expires_at = v3_token_data['token']['expires_at']
+ token_id = self.token_formatter.create_token(user_id, expires_at,
+ audit_ids,
+ methods=method_names,
+ project_id=project_id)
+ # Convert v3 to v2 token data and build v2 catalog
+ token_data = self.v2_token_data_helper.v3_to_v2_token(token_id,
+ v3_token_data)
+
+ return token_id, token_data
+
+ def _build_federated_info(self, token_data):
+ """Extract everything needed for federated tokens.
+
+ This dictionary is passed to the FederatedPayload token formatter,
+ which unpacks the values and builds the Fernet token.
+
+ """
+ group_ids = token_data.get('user', {}).get(
+ federation.FEDERATION, {}).get('groups')
+ idp_id = token_data.get('user', {}).get(
+ federation.FEDERATION, {}).get('identity_provider', {}).get('id')
+ protocol_id = token_data.get('user', {}).get(
+ federation.FEDERATION, {}).get('protocol', {}).get('id')
+ if not group_ids:
+ group_ids = list()
+ federated_dict = dict(group_ids=group_ids, idp_id=idp_id,
+ protocol_id=protocol_id)
+ return federated_dict
+
+ def _rebuild_federated_info(self, federated_dict, user_id):
+ """Format federated information into the token reference.
+
+ The federated_dict is passed back from the FederatedPayload token
+ formatter. The responsibility of this method is to format the
+ information passed back from the token formatter into the token
+ reference before constructing the token data from the
+ V3TokenDataHelper.
+
+ """
+ g_ids = federated_dict['group_ids']
+ idp_id = federated_dict['idp_id']
+ protocol_id = federated_dict['protocol_id']
+ federated_info = dict(groups=g_ids,
+ identity_provider=dict(id=idp_id),
+ protocol=dict(id=protocol_id))
+ token_dict = {'user': {federation.FEDERATION: federated_info}}
+ token_dict['user']['id'] = user_id
+ token_dict['user']['name'] = user_id
+ return token_dict
+
+ def issue_v3_token(self, user_id, method_names, expires_at=None,
+ project_id=None, domain_id=None, auth_context=None,
+ trust=None, metadata_ref=None, include_catalog=True,
+ parent_audit_id=None):
+ """Issue a V3 formatted token.
+
+ Here is where we need to detect what is given to us, and what kind of
+ token the user is expecting. Depending on the outcome of that, we can
+ pass all the information to be packed to the proper token format
+ handler.
+
+ :param user_id: ID of the user
+ :param method_names: method of authentication
+ :param expires_at: token expiration time
+ :param project_id: ID of the project being scoped to
+ :param domain_id: ID of the domain being scoped to
+ :param auth_context: authentication context
+ :param trust: ID of the trust
+ :param metadata_ref: metadata reference
+ :param include_catalog: return the catalog in the response if True,
+ otherwise don't return the catalog
+ :param parent_audit_id: ID of the patent audit entity
+ :returns: tuple containing the id of the token and the token data
+
+ """
+ # TODO(lbragstad): Currently, Fernet tokens don't support bind in the
+ # token format. Raise a 501 if we're dealing with bind.
+ if auth_context.get('bind'):
+ raise exception.NotImplemented()
+
+ token_ref = None
+ # NOTE(lbragstad): This determines if we are dealing with a federated
+ # token or not. The groups for the user will be in the returned token
+ # reference.
+ federated_dict = None
+ if auth_context and self._is_mapped_token(auth_context):
+ token_ref = self._handle_mapped_tokens(
+ auth_context, project_id, domain_id)
+ federated_dict = self._build_federated_info(token_ref)
+
+ token_data = self.v3_token_data_helper.get_token_data(
+ user_id,
+ method_names,
+ auth_context.get('extras') if auth_context else None,
+ domain_id=domain_id,
+ project_id=project_id,
+ expires=expires_at,
+ trust=trust,
+ bind=auth_context.get('bind') if auth_context else None,
+ token=token_ref,
+ include_catalog=include_catalog,
+ audit_info=parent_audit_id)
+
+ token = self.token_formatter.create_token(
+ user_id,
+ token_data['token']['expires_at'],
+ token_data['token']['audit_ids'],
+ methods=method_names,
+ domain_id=domain_id,
+ project_id=project_id,
+ trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'),
+ federated_info=federated_dict)
+ return token, token_data
+
+ def validate_v2_token(self, token_ref):
+ """Validate a V2 formatted token.
+
+ :param token_ref: reference describing the token to validate
+ :returns: the token data
+ :raises keystone.exception.Unauthorized: if v3 token is used
+
+ """
+ (user_id, methods,
+ audit_ids, domain_id,
+ project_id, trust_id,
+ federated_info, created_at,
+ expires_at) = self.token_formatter.validate_token(token_ref)
+
+ if trust_id or domain_id or federated_info:
+ msg = _('This is not a v2.0 Fernet token. Use v3 for trust, '
+ 'domain, or federated tokens.')
+ raise exception.Unauthorized(msg)
+
+ v3_token_data = self.v3_token_data_helper.get_token_data(
+ user_id,
+ methods,
+ project_id=project_id,
+ expires=expires_at,
+ issued_at=created_at,
+ token=token_ref,
+ include_catalog=False,
+ audit_info=audit_ids)
+ return self.v2_token_data_helper.v3_to_v2_token(token_ref,
+ v3_token_data)
+
+ def validate_v3_token(self, token):
+ """Validate a V3 formatted token.
+
+ :param token: a string describing the token to validate
+ :returns: the token data
+ :raises keystone.exception.Unauthorized: if token format version isn't
+ supported
+
+ """
+ (user_id, methods, audit_ids, domain_id, project_id, trust_id,
+ federated_info, created_at, expires_at) = (
+ self.token_formatter.validate_token(token))
+
+ token_dict = None
+ if federated_info:
+ token_dict = self._rebuild_federated_info(federated_info, user_id)
+ trust_ref = self.trust_api.get_trust(trust_id)
+
+ return self.v3_token_data_helper.get_token_data(
+ user_id,
+ method_names=methods,
+ domain_id=domain_id,
+ project_id=project_id,
+ issued_at=created_at,
+ expires=expires_at,
+ trust=trust_ref,
+ token=token_dict,
+ audit_info=audit_ids)
+
+ def _get_token_id(self, token_data):
+ """Generate the token_id based upon the data in token_data.
+
+ :param token_data: token information
+ :type token_data: dict
+ :raises keystone.exception.NotImplemented: when called
+ """
+ raise exception.NotImplemented()
diff --git a/keystone-moon/keystone/token/providers/fernet/token_formatters.py b/keystone-moon/keystone/token/providers/fernet/token_formatters.py
new file mode 100644
index 00000000..50960923
--- /dev/null
+++ b/keystone-moon/keystone/token/providers/fernet/token_formatters.py
@@ -0,0 +1,545 @@
+# 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 base64
+import datetime
+import struct
+import uuid
+
+from cryptography import fernet
+import msgpack
+from oslo_config import cfg
+from oslo_log import log
+from oslo_utils import timeutils
+import six
+from six.moves import urllib
+
+from keystone.auth import plugins as auth_plugins
+from keystone import exception
+from keystone.i18n import _
+from keystone.token import provider
+from keystone.token.providers.fernet import utils
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+# Fernet byte indexes as as computed by pypi/keyless_fernet and defined in
+# https://github.com/fernet/spec
+TIMESTAMP_START = 1
+TIMESTAMP_END = 9
+
+
+class TokenFormatter(object):
+ """Packs and unpacks payloads into tokens for transport."""
+
+ @property
+ def crypto(self):
+ """Return a cryptography instance.
+
+ You can extend this class with a custom crypto @property to provide
+ your own token encoding / decoding. For example, using a different
+ cryptography library (e.g. ``python-keyczar``) or to meet arbitrary
+ security requirements.
+
+ This @property just needs to return an object that implements
+ ``encrypt(plaintext)`` and ``decrypt(ciphertext)``.
+
+ """
+ keys = utils.load_keys()
+
+ if not keys:
+ raise exception.KeysNotFound()
+
+ fernet_instances = [fernet.Fernet(key) for key in utils.load_keys()]
+ return fernet.MultiFernet(fernet_instances)
+
+ def pack(self, payload):
+ """Pack a payload for transport as a token."""
+ # base64 padding (if any) is not URL-safe
+ return urllib.parse.quote(self.crypto.encrypt(payload))
+
+ def unpack(self, token):
+ """Unpack a token, and validate the payload."""
+ token = urllib.parse.unquote(six.binary_type(token))
+
+ try:
+ return self.crypto.decrypt(token)
+ except fernet.InvalidToken as e:
+ raise exception.Unauthorized(six.text_type(e))
+
+ @classmethod
+ def creation_time(cls, fernet_token):
+ """Returns the creation time of a valid Fernet token."""
+ # tokens may be transmitted as Unicode, but they're just ASCII
+ # (pypi/cryptography will refuse to operate on Unicode input)
+ fernet_token = six.binary_type(fernet_token)
+
+ # the base64 padding on fernet tokens is made URL-safe
+ fernet_token = urllib.parse.unquote(fernet_token)
+
+ # fernet tokens are base64 encoded and the padding made URL-safe
+ token_bytes = base64.urlsafe_b64decode(fernet_token)
+
+ # slice into the byte array to get just the timestamp
+ timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END]
+
+ # convert those bytes to an integer
+ # (it's a 64-bit "unsigned long long int" in C)
+ timestamp_int = struct.unpack(">Q", timestamp_bytes)[0]
+
+ # and with an integer, it's trivial to produce a datetime object
+ created_at = datetime.datetime.utcfromtimestamp(timestamp_int)
+
+ return created_at
+
+ def create_token(self, user_id, expires_at, audit_ids, methods=None,
+ domain_id=None, project_id=None, trust_id=None,
+ federated_info=None):
+ """Given a set of payload attributes, generate a Fernet token."""
+ if trust_id:
+ version = TrustScopedPayload.version
+ payload = TrustScopedPayload.assemble(
+ user_id,
+ methods,
+ project_id,
+ expires_at,
+ audit_ids,
+ trust_id)
+ elif federated_info:
+ version = FederatedPayload.version
+ payload = FederatedPayload.assemble(
+ user_id,
+ methods,
+ expires_at,
+ audit_ids,
+ federated_info)
+ elif project_id:
+ version = ProjectScopedPayload.version
+ payload = ProjectScopedPayload.assemble(
+ user_id,
+ methods,
+ project_id,
+ expires_at,
+ audit_ids)
+ elif domain_id:
+ version = DomainScopedPayload.version
+ payload = DomainScopedPayload.assemble(
+ user_id,
+ methods,
+ domain_id,
+ expires_at,
+ audit_ids)
+ else:
+ version = UnscopedPayload.version
+ payload = UnscopedPayload.assemble(
+ user_id,
+ methods,
+ expires_at,
+ audit_ids)
+
+ versioned_payload = (version,) + payload
+ serialized_payload = msgpack.packb(versioned_payload)
+ token = self.pack(serialized_payload)
+
+ return token
+
+ def validate_token(self, token):
+ """Validates a Fernet token and returns the payload attributes."""
+ # Convert v2 unicode token to a string
+ if not isinstance(token, six.binary_type):
+ token = token.encode('ascii')
+
+ serialized_payload = self.unpack(token)
+ versioned_payload = msgpack.unpackb(serialized_payload)
+ version, payload = versioned_payload[0], versioned_payload[1:]
+
+ # depending on the formatter, these may or may not be defined
+ domain_id = None
+ project_id = None
+ trust_id = None
+ federated_info = None
+
+ if version == UnscopedPayload.version:
+ (user_id, methods, expires_at, audit_ids) = (
+ UnscopedPayload.disassemble(payload))
+ elif version == DomainScopedPayload.version:
+ (user_id, methods, domain_id, expires_at, audit_ids) = (
+ DomainScopedPayload.disassemble(payload))
+ elif version == ProjectScopedPayload.version:
+ (user_id, methods, project_id, expires_at, audit_ids) = (
+ ProjectScopedPayload.disassemble(payload))
+ elif version == TrustScopedPayload.version:
+ (user_id, methods, project_id, expires_at, audit_ids, trust_id) = (
+ TrustScopedPayload.disassemble(payload))
+ elif version == FederatedPayload.version:
+ (user_id, methods, expires_at, audit_ids, federated_info) = (
+ FederatedPayload.disassemble(payload))
+ else:
+ # If the token_format is not recognized, raise Unauthorized.
+ raise exception.Unauthorized(_(
+ 'This is not a recognized Fernet payload version: %s') %
+ version)
+
+ # rather than appearing in the payload, the creation time is encoded
+ # into the token format itself
+ created_at = TokenFormatter.creation_time(token)
+ created_at = timeutils.isotime(at=created_at, subsecond=True)
+ expires_at = timeutils.parse_isotime(expires_at)
+ expires_at = timeutils.isotime(at=expires_at, subsecond=True)
+
+ return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
+ federated_info, created_at, expires_at)
+
+
+class BasePayload(object):
+ # each payload variant should have a unique version
+ version = None
+
+ @classmethod
+ def assemble(cls, *args):
+ """Assemble the payload of a token.
+
+ :param args: whatever data should go into the payload
+ :returns: the payload of a token
+
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Disassemble an unscoped payload into the component data.
+
+ :param payload: this variant of payload
+ :returns: a tuple of the payloads component data
+
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ def convert_uuid_hex_to_bytes(cls, uuid_string):
+ """Compress UUID formatted strings to bytes.
+
+ :param uuid_string: uuid string to compress to bytes
+ :returns: a byte representation of the uuid
+
+ """
+ # TODO(lbragstad): Wrap this in an exception. Not sure what the case
+ # would be where we couldn't handle what we've been given but incase
+ # the integrity of the token has been compromised.
+ uuid_obj = uuid.UUID(uuid_string)
+ return uuid_obj.bytes
+
+ @classmethod
+ def convert_uuid_bytes_to_hex(cls, uuid_byte_string):
+ """Generate uuid.hex format based on byte string.
+
+ :param uuid_byte_string: uuid string to generate from
+ :returns: uuid hex formatted string
+
+ """
+ # TODO(lbragstad): Wrap this in an exception. Not sure what the case
+ # would be where we couldn't handle what we've been given but incase
+ # the integrity of the token has been compromised.
+ uuid_obj = uuid.UUID(bytes=uuid_byte_string)
+ return uuid_obj.hex
+
+ @classmethod
+ def _convert_time_string_to_int(cls, time_string):
+ """Convert a time formatted string to a timestamp integer.
+
+ :param time_string: time formatted string
+ :returns: an integer timestamp
+
+ """
+ time_object = timeutils.parse_isotime(time_string)
+ return (timeutils.normalize_time(time_object) -
+ datetime.datetime.utcfromtimestamp(0)).total_seconds()
+
+ @classmethod
+ def _convert_int_to_time_string(cls, time_int):
+ """Convert a timestamp integer to a string.
+
+ :param time_int: integer representing timestamp
+ :returns: a time formatted strings
+
+ """
+ time_object = datetime.datetime.utcfromtimestamp(int(time_int))
+ return timeutils.isotime(time_object)
+
+ @classmethod
+ def attempt_convert_uuid_hex_to_bytes(cls, value):
+ """Attempt to convert value to bytes or return value.
+
+ :param value: value to attempt to convert to bytes
+ :returns: uuid value in bytes or value
+
+ """
+ try:
+ return cls.convert_uuid_hex_to_bytes(value)
+ except ValueError:
+ # this might not be a UUID, depending on the situation (i.e.
+ # federation)
+ return value
+
+ @classmethod
+ def attempt_convert_uuid_bytes_to_hex(cls, value):
+ """Attempt to convert value to hex or return value.
+
+ :param value: value to attempt to convert to hex
+ :returns: uuid value in hex or value
+
+ """
+ try:
+ return cls.convert_uuid_bytes_to_hex(value)
+ except ValueError:
+ return value
+
+
+class UnscopedPayload(BasePayload):
+ version = 0
+
+ @classmethod
+ def assemble(cls, user_id, methods, expires_at, audit_ids):
+ """Assemble the payload of an unscoped token.
+
+ :param user_id: identifier of the user in the token request
+ :param methods: list of authentication methods used
+ :param expires_at: datetime of the token's expiration
+ :param audit_ids: list of the token's audit IDs
+ :returns: the payload of an unscoped token
+
+ """
+ b_user_id = cls.convert_uuid_hex_to_bytes(user_id)
+ methods = auth_plugins.convert_method_list_to_integer(methods)
+ expires_at_int = cls._convert_time_string_to_int(expires_at)
+ b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
+ audit_ids))
+ return (b_user_id, methods, expires_at_int, b_audit_ids)
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Disassemble an unscoped payload into the component data.
+
+ :param payload: the payload of an unscoped token
+ :return: a tuple containing the user_id, auth methods, expires_at, and
+ audit_ids
+
+ """
+ user_id = cls.convert_uuid_bytes_to_hex(payload[0])
+ methods = auth_plugins.convert_integer_to_method_list(payload[1])
+ expires_at_str = cls._convert_int_to_time_string(payload[2])
+ audit_ids = list(map(provider.base64_encode, payload[3]))
+ return (user_id, methods, expires_at_str, audit_ids)
+
+
+class DomainScopedPayload(BasePayload):
+ version = 1
+
+ @classmethod
+ def assemble(cls, user_id, methods, domain_id, expires_at, audit_ids):
+ """Assemble the payload of a domain-scoped token.
+
+ :param user_id: ID of the user in the token request
+ :param methods: list of authentication methods used
+ :param domain_id: ID of the domain to scope to
+ :param expires_at: datetime of the token's expiration
+ :param audit_ids: list of the token's audit IDs
+ :returns: the payload of a domain-scoped token
+
+ """
+ b_user_id = cls.convert_uuid_hex_to_bytes(user_id)
+ methods = auth_plugins.convert_method_list_to_integer(methods)
+ try:
+ b_domain_id = cls.convert_uuid_hex_to_bytes(domain_id)
+ except ValueError:
+ # the default domain ID is configurable, and probably isn't a UUID
+ if domain_id == CONF.identity.default_domain_id:
+ b_domain_id = domain_id
+ else:
+ raise
+ expires_at_int = cls._convert_time_string_to_int(expires_at)
+ b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
+ audit_ids))
+ return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids)
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Disassemble a payload into the component data.
+
+ :param payload: the payload of a token
+ :return: a tuple containing the user_id, auth methods, domain_id,
+ expires_at_str, and audit_ids
+
+ """
+ user_id = cls.convert_uuid_bytes_to_hex(payload[0])
+ methods = auth_plugins.convert_integer_to_method_list(payload[1])
+ try:
+ domain_id = cls.convert_uuid_bytes_to_hex(payload[2])
+ except ValueError:
+ # the default domain ID is configurable, and probably isn't a UUID
+ if payload[2] == CONF.identity.default_domain_id:
+ domain_id = payload[2]
+ else:
+ raise
+ expires_at_str = cls._convert_int_to_time_string(payload[3])
+ audit_ids = list(map(provider.base64_encode, payload[4]))
+
+ return (user_id, methods, domain_id, expires_at_str, audit_ids)
+
+
+class ProjectScopedPayload(BasePayload):
+ version = 2
+
+ @classmethod
+ def assemble(cls, user_id, methods, project_id, expires_at, audit_ids):
+ """Assemble the payload of a project-scoped token.
+
+ :param user_id: ID of the user in the token request
+ :param methods: list of authentication methods used
+ :param project_id: ID of the project to scope to
+ :param expires_at: datetime of the token's expiration
+ :param audit_ids: list of the token's audit IDs
+ :returns: the payload of a project-scoped token
+
+ """
+ b_user_id = cls.convert_uuid_hex_to_bytes(user_id)
+ methods = auth_plugins.convert_method_list_to_integer(methods)
+ b_project_id = cls.convert_uuid_hex_to_bytes(project_id)
+ expires_at_int = cls._convert_time_string_to_int(expires_at)
+ b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
+ audit_ids))
+ return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Disassemble a payload into the component data.
+
+ :param payload: the payload of a token
+ :return: a tuple containing the user_id, auth methods, project_id,
+ expires_at_str, and audit_ids
+
+ """
+ user_id = cls.convert_uuid_bytes_to_hex(payload[0])
+ methods = auth_plugins.convert_integer_to_method_list(payload[1])
+ project_id = cls.convert_uuid_bytes_to_hex(payload[2])
+ expires_at_str = cls._convert_int_to_time_string(payload[3])
+ audit_ids = list(map(provider.base64_encode, payload[4]))
+
+ return (user_id, methods, project_id, expires_at_str, audit_ids)
+
+
+class TrustScopedPayload(BasePayload):
+ version = 3
+
+ @classmethod
+ def assemble(cls, user_id, methods, project_id, expires_at, audit_ids,
+ trust_id):
+ """Assemble the payload of a trust-scoped token.
+
+ :param user_id: ID of the user in the token request
+ :param methods: list of authentication methods used
+ :param project_id: ID of the project to scope to
+ :param expires_at: datetime of the token's expiration
+ :param audit_ids: list of the token's audit IDs
+ :param trust_id: ID of the trust in effect
+ :returns: the payload of a trust-scoped token
+
+ """
+ b_user_id = cls.convert_uuid_hex_to_bytes(user_id)
+ methods = auth_plugins.convert_method_list_to_integer(methods)
+ b_project_id = cls.convert_uuid_hex_to_bytes(project_id)
+ b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id)
+ expires_at_int = cls._convert_time_string_to_int(expires_at)
+ b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
+ audit_ids))
+
+ return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids,
+ b_trust_id)
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Validate a trust-based payload.
+
+ :param token_string: a string representing the token
+ :returns: a tuple containing the user_id, auth methods, project_id,
+ expires_at_str, audit_ids, and trust_id
+
+ """
+ user_id = cls.convert_uuid_bytes_to_hex(payload[0])
+ methods = auth_plugins.convert_integer_to_method_list(payload[1])
+ project_id = cls.convert_uuid_bytes_to_hex(payload[2])
+ expires_at_str = cls._convert_int_to_time_string(payload[3])
+ audit_ids = list(map(provider.base64_encode, payload[4]))
+ trust_id = cls.convert_uuid_bytes_to_hex(payload[5])
+
+ return (user_id, methods, project_id, expires_at_str, audit_ids,
+ trust_id)
+
+
+class FederatedPayload(BasePayload):
+ version = 4
+
+ @classmethod
+ def assemble(cls, user_id, methods, expires_at, audit_ids, federated_info):
+ """Assemble the payload of a federated token.
+
+ :param user_id: ID of the user in the token request
+ :param methods: list of authentication methods used
+ :param expires_at: datetime of the token's expiration
+ :param audit_ids: list of the token's audit IDs
+ :param federated_info: dictionary containing group IDs, the identity
+ provider ID, protocol ID, and federated domain
+ ID
+ :returns: the payload of a federated token
+
+ """
+ def pack_group_ids(group_dict):
+ return cls.convert_uuid_hex_to_bytes(group_dict['id'])
+
+ b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
+ methods = auth_plugins.convert_method_list_to_integer(methods)
+ b_group_ids = map(pack_group_ids, federated_info['group_ids'])
+ b_idp_id = cls.attempt_convert_uuid_hex_to_bytes(
+ federated_info['idp_id'])
+ protocol_id = federated_info['protocol_id']
+ expires_at_int = cls._convert_time_string_to_int(expires_at)
+ b_audit_ids = map(provider.random_urlsafe_str_to_bytes, audit_ids)
+
+ return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id,
+ expires_at_int, b_audit_ids)
+
+ @classmethod
+ def disassemble(cls, payload):
+ """Validate a federated paylod.
+
+ :param token_string: a string representing the token
+ :return: a tuple containing the user_id, auth methods, audit_ids, and
+ a dictionary containing federated information such as the the
+ group IDs, the identity provider ID, the protocol ID, and the
+ federated domain ID
+
+ """
+ def unpack_group_ids(group_id_in_bytes):
+ group_id = cls.convert_uuid_bytes_to_hex(group_id_in_bytes)
+ return {'id': group_id}
+
+ user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0])
+ methods = auth_plugins.convert_integer_to_method_list(payload[1])
+ group_ids = map(unpack_group_ids, payload[2])
+ idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[3])
+ protocol_id = payload[4]
+ expires_at_str = cls._convert_int_to_time_string(payload[5])
+ audit_ids = map(provider.base64_encode, payload[6])
+ federated_info = dict(group_ids=group_ids, idp_id=idp_id,
+ protocol_id=protocol_id)
+ return (user_id, methods, expires_at_str, audit_ids, federated_info)
diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py
new file mode 100644
index 00000000..56624ee5
--- /dev/null
+++ b/keystone-moon/keystone/token/providers/fernet/utils.py
@@ -0,0 +1,243 @@
+# 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 os
+import stat
+
+from cryptography import fernet
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone.i18n import _LE, _LW, _LI
+
+
+LOG = log.getLogger(__name__)
+
+CONF = cfg.CONF
+
+
+def validate_key_repository():
+ """Validate permissions on the key repository directory."""
+ # NOTE(lbragstad): We shouldn't need to check if the directory was passed
+ # in as None because we don't set allow_no_values to True.
+
+ # ensure current user has full access to the key repository
+ if (not os.access(CONF.fernet_tokens.key_repository, os.R_OK) or not
+ os.access(CONF.fernet_tokens.key_repository, os.W_OK) or not
+ os.access(CONF.fernet_tokens.key_repository, os.X_OK)):
+ LOG.error(
+ _LE('Either [fernet_tokens] key_repository does not exist or '
+ 'Keystone does not have sufficient permission to access it: '
+ '%s'), CONF.fernet_tokens.key_repository)
+ return False
+
+ # ensure the key repository isn't world-readable
+ stat_info = os.stat(CONF.fernet_tokens.key_repository)
+ if stat_info.st_mode & stat.S_IROTH or stat_info.st_mode & stat.S_IXOTH:
+ LOG.warning(_LW(
+ '[fernet_tokens] key_repository is world readable: %s'),
+ CONF.fernet_tokens.key_repository)
+
+ return True
+
+
+def _convert_to_integers(id_value):
+ """Cast user and group system identifiers to integers."""
+ # NOTE(lbragstad) os.chown() will raise a TypeError here if
+ # keystone_user_id and keystone_group_id are not integers. Let's
+ # cast them to integers if we can because it's possible to pass non-integer
+ # values into the fernet_setup utility.
+ try:
+ id_int = int(id_value)
+ except ValueError as e:
+ msg = ('Unable to convert Keystone user or group ID. Error: %s', e)
+ LOG.error(msg)
+ raise
+
+ return id_int
+
+
+def create_key_directory(keystone_user_id=None, keystone_group_id=None):
+ """If the configured key directory does not exist, attempt to create it."""
+ if not os.access(CONF.fernet_tokens.key_repository, os.F_OK):
+ LOG.info(_LI(
+ '[fernet_tokens] key_repository does not appear to exist; '
+ 'attempting to create it'))
+
+ try:
+ os.makedirs(CONF.fernet_tokens.key_repository, 0o700)
+ except OSError:
+ LOG.error(_LE(
+ 'Failed to create [fernet_tokens] key_repository: either it '
+ 'already exists or you don\'t have sufficient permissions to '
+ 'create it'))
+
+ if keystone_user_id and keystone_group_id:
+ os.chown(
+ CONF.fernet_tokens.key_repository,
+ keystone_user_id,
+ keystone_group_id)
+ elif keystone_user_id or keystone_group_id:
+ LOG.warning(_LW(
+ 'Unable to change the ownership of [fernet_tokens] '
+ 'key_repository without a keystone user ID and keystone group '
+ 'ID both being provided: %s') %
+ CONF.fernet_tokens.key_repository)
+
+
+def _create_new_key(keystone_user_id, keystone_group_id):
+ """Securely create a new encryption key.
+
+ Create a new key that is readable by the Keystone group and Keystone user.
+ """
+ key = fernet.Fernet.generate_key()
+
+ # This ensures the key created is not world-readable
+ old_umask = os.umask(0o177)
+ if keystone_user_id and keystone_group_id:
+ old_egid = os.getegid()
+ old_euid = os.geteuid()
+ os.setegid(keystone_group_id)
+ os.seteuid(keystone_user_id)
+ elif keystone_user_id or keystone_group_id:
+ LOG.warning(_LW(
+ 'Unable to change the ownership of the new key without a keystone '
+ 'user ID and keystone group ID both being provided: %s') %
+ CONF.fernet_tokens.key_repository)
+ # Determine the file name of the new key
+ key_file = os.path.join(CONF.fernet_tokens.key_repository, '0')
+ try:
+ with open(key_file, 'w') as f:
+ f.write(key)
+ finally:
+ # After writing the key, set the umask back to it's original value. Do
+ # the same with group and user identifiers if a Keystone group or user
+ # was supplied.
+ os.umask(old_umask)
+ if keystone_user_id and keystone_group_id:
+ os.seteuid(old_euid)
+ os.setegid(old_egid)
+
+ LOG.info(_LI('Created a new key: %s'), key_file)
+
+
+def initialize_key_repository(keystone_user_id=None, keystone_group_id=None):
+ """Create a key repository and bootstrap it with a key.
+
+ :param keystone_user_id: User ID of the Keystone user.
+ :param keystone_group_id: Group ID of the Keystone user.
+
+ """
+ # make sure we have work to do before proceeding
+ if os.access(os.path.join(CONF.fernet_tokens.key_repository, '0'),
+ os.F_OK):
+ LOG.info(_LI('Key repository is already initialized; aborting.'))
+ return
+
+ # bootstrap an existing key
+ _create_new_key(keystone_user_id, keystone_group_id)
+
+ # ensure that we end up with a primary and secondary key
+ rotate_keys(keystone_user_id, keystone_group_id)
+
+
+def rotate_keys(keystone_user_id=None, keystone_group_id=None):
+ """Create a new primary key and revoke excess active keys.
+
+ :param keystone_user_id: User ID of the Keystone user.
+ :param keystone_group_id: Group ID of the Keystone user.
+
+ Key rotation utilizes the following behaviors:
+
+ - The highest key number is used as the primary key (used for encryption).
+ - All keys can be used for decryption.
+ - New keys are always created as key "0," which serves as a placeholder
+ before promoting it to be the primary key.
+
+ This strategy allows you to safely perform rotation on one node in a
+ cluster, before syncing the results of the rotation to all other nodes
+ (during both key rotation and synchronization, all nodes must recognize all
+ primary keys).
+
+ """
+ # read the list of key files
+ key_files = dict()
+ for filename in os.listdir(CONF.fernet_tokens.key_repository):
+ path = os.path.join(CONF.fernet_tokens.key_repository, str(filename))
+ if os.path.isfile(path):
+ key_files[int(filename)] = path
+
+ LOG.info(_LI('Starting key rotation with %(count)s key files: %(list)s'), {
+ 'count': len(key_files),
+ 'list': key_files.values()})
+
+ # determine the number of the new primary key
+ current_primary_key = max(key_files.keys())
+ LOG.info(_LI('Current primary key is: %s'), current_primary_key)
+ new_primary_key = current_primary_key + 1
+ LOG.info(_LI('Next primary key will be: %s'), new_primary_key)
+
+ # promote the next primary key to be the primary
+ os.rename(
+ os.path.join(CONF.fernet_tokens.key_repository, '0'),
+ os.path.join(CONF.fernet_tokens.key_repository, str(new_primary_key)))
+ key_files.pop(0)
+ key_files[new_primary_key] = os.path.join(
+ CONF.fernet_tokens.key_repository,
+ str(new_primary_key))
+ LOG.info(_LI('Promoted key 0 to be the primary: %s'), new_primary_key)
+
+ # add a new key to the rotation, which will be the *next* primary
+ _create_new_key(keystone_user_id, keystone_group_id)
+
+ # check for bad configuration
+ if CONF.fernet_tokens.max_active_keys < 1:
+ LOG.warning(_LW(
+ '[fernet_tokens] max_active_keys must be at least 1 to maintain a '
+ 'primary key.'))
+ CONF.fernet_tokens.max_active_keys = 1
+
+ # purge excess keys
+ keys = sorted(key_files.keys())
+ excess_keys = (
+ keys[:len(key_files) - CONF.fernet_tokens.max_active_keys + 1])
+ LOG.info(_LI('Excess keys to purge: %s'), excess_keys)
+ for i in excess_keys:
+ os.remove(key_files[i])
+
+
+def load_keys():
+ """Load keys from disk into a list.
+
+ The first key in the list is the primary key used for encryption. All
+ other keys are active secondary keys that can be used for decrypting
+ tokens.
+
+ """
+ if not validate_key_repository():
+ return []
+
+ # build a dictionary of key_number:encryption_key pairs
+ keys = dict()
+ for filename in os.listdir(CONF.fernet_tokens.key_repository):
+ path = os.path.join(CONF.fernet_tokens.key_repository, str(filename))
+ if os.path.isfile(path):
+ with open(path, 'r') as key_file:
+ keys[int(filename)] = key_file.read()
+
+ LOG.info(_LI(
+ 'Loaded %(count)s encryption keys from: %(dir)s'), {
+ 'count': len(keys),
+ 'dir': CONF.fernet_tokens.key_repository})
+
+ # return the encryption_keys, sorted by key number, descending
+ return [keys[x] for x in sorted(keys.keys(), reverse=True)]