diff options
Diffstat (limited to 'keystone-moon/keystone/token/providers/fernet')
3 files changed, 354 insertions, 469 deletions
diff --git a/keystone-moon/keystone/token/providers/fernet/core.py b/keystone-moon/keystone/token/providers/fernet/core.py index a71c375b..ff6fe9cc 100644 --- a/keystone-moon/keystone/token/providers/fernet/core.py +++ b/keystone-moon/keystone/token/providers/fernet/core.py @@ -11,23 +11,18 @@ # under the License. from oslo_config import cfg -from oslo_log import log from keystone.common import dependency from keystone.common import utils as ks_utils -from keystone.contrib.federation import constants as federation_constants -from keystone import exception -from keystone.i18n import _ -from keystone.token import provider +from keystone.federation import constants as federation_constants 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') +@dependency.requires('trust_api', 'oauth_api') class Provider(common.BaseProvider): def __init__(self, *args, **kwargs): super(Provider, self).__init__(*args, **kwargs) @@ -38,65 +33,10 @@ class Provider(common.BaseProvider): """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'] - - # maintain expiration time across rescopes - expires = token_ref.get('expires') - - 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=expires) - - 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) - self._build_issued_at_info(token_id, v3_token_data) - # Convert v3 to v2 token data and build v2 catalog - token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) - token_data['access']['token']['id'] = token_id - + def issue_v2_token(self, *args, **kwargs): + token_id, token_data = super(Provider, self).issue_v2_token( + *args, **kwargs) + self._build_issued_at_info(token_id, token_data) return token_id, token_data def issue_v3_token(self, *args, **kwargs): @@ -117,8 +57,12 @@ class Provider(common.BaseProvider): # that we have to rely on when we validate the token. fernet_creation_datetime_obj = self.token_formatter.creation_time( token_id) - token_data['token']['issued_at'] = ks_utils.isotime( - at=fernet_creation_datetime_obj, subsecond=True) + if token_data.get('access'): + token_data['access']['token']['issued_at'] = ks_utils.isotime( + at=fernet_creation_datetime_obj, subsecond=True) + else: + token_data['token']['issued_at'] = ks_utils.isotime( + at=fernet_creation_datetime_obj, subsecond=True) def _build_federated_info(self, token_data): """Extract everything needed for federated tokens. @@ -127,18 +71,18 @@ class Provider(common.BaseProvider): the values and build federated Fernet tokens. """ - idp_id = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get( - 'identity_provider', {}).get('id') - protocol_id = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get('protocol', {}).get('id') - # If we don't have an identity provider ID and a protocol ID, it's safe - # to assume we aren't dealing with a federated token. - if not (idp_id and protocol_id): - return None - - group_ids = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get('groups') + token_data = token_data['token'] + try: + user = token_data['user'] + federation = user[federation_constants.FEDERATION] + idp_id = federation['identity_provider']['id'] + protocol_id = federation['protocol']['id'] + except KeyError: + # The token data doesn't have federated info, so we aren't dealing + # with a federated token and no federated info to build. + return + + group_ids = federation.get('groups') return {'group_ids': group_ids, 'idp_id': idp_id, @@ -195,96 +139,66 @@ class Provider(common.BaseProvider): self.v3_token_data_helper.populate_roles_for_groups( token_dict, group_ids, project_id, domain_id, user_id) - 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.TokenNotFound: if token format is invalid - :raises keystone.exception.Unauthorized: if v3 token is used - - """ - try: - (user_id, methods, - audit_ids, domain_id, - project_id, trust_id, - federated_info, created_at, - expires_at) = self.token_formatter.validate_token(token_ref) - except exception.ValidationError as e: - raise exception.TokenNotFound(e) - - 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) - token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) - token_data['access']['token']['id'] = token_ref - return 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.TokenNotFound: if token format version isn't - supported - - """ - try: - (user_id, methods, audit_ids, domain_id, project_id, trust_id, - federated_info, created_at, expires_at) = ( - self.token_formatter.validate_token(token)) - except exception.ValidationError as e: - raise exception.TokenNotFound(e) - - token_dict = None - trust_ref = None - if federated_info: - token_dict = self._rebuild_federated_info(federated_info, user_id) - if project_id or domain_id: - self._rebuild_federated_token_roles(token_dict, federated_info, - user_id, project_id, - domain_id) - if trust_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 _extract_v2_token_data(self, token_data): + user_id = token_data['access']['user']['id'] + expires_at = token_data['access']['token']['expires'] + audit_ids = token_data['access']['token'].get('audit_ids') + methods = ['password'] + if len(audit_ids) > 1: + methods.append('token') + project_id = token_data['access']['token'].get('tenant', {}).get('id') + domain_id = None + trust_id = None + access_token_id = None + federated_info = None + return (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) + + def _extract_v3_token_data(self, token_data): + """Extract information from a v3 token reference.""" + user_id = token_data['token']['user']['id'] + expires_at = token_data['token']['expires_at'] + audit_ids = token_data['token']['audit_ids'] + methods = token_data['token'].get('methods') + domain_id = token_data['token'].get('domain', {}).get('id') + project_id = token_data['token'].get('project', {}).get('id') + trust_id = token_data['token'].get('OS-TRUST:trust', {}).get('id') + access_token_id = token_data['token'].get('OS-OAUTH1', {}).get( + 'access_token_id') + federated_info = self._build_federated_info(token_data) + + return (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) 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 + :rtype: six.text_type + """ + # NOTE(lbragstad): Only v2.0 token responses include an 'access' + # attribute. + if token_data.get('access'): + (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) = ( + self._extract_v2_token_data(token_data)) + else: + (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) = ( + self._extract_v3_token_data(token_data)) + return self.token_formatter.create_token( - token_data['token']['user']['id'], - token_data['token']['expires_at'], - token_data['token']['audit_ids'], - methods=token_data['token'].get('methods'), - domain_id=token_data['token'].get('domain', {}).get('id'), - project_id=token_data['token'].get('project', {}).get('id'), - trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'), - federated_info=self._build_federated_info(token_data) + user_id, + expires_at, + audit_ids, + methods=methods, + domain_id=domain_id, + project_id=project_id, + trust_id=trust_id, + federated_info=federated_info, + access_token_id=access_token_id ) @property @@ -292,5 +206,6 @@ class Provider(common.BaseProvider): """Return if the token provider supports bind authentication methods. :returns: False + """ return False diff --git a/keystone-moon/keystone/token/providers/fernet/token_formatters.py b/keystone-moon/keystone/token/providers/fernet/token_formatters.py index dbfee6dd..dfdd06e8 100644 --- a/keystone-moon/keystone/token/providers/fernet/token_formatters.py +++ b/keystone-moon/keystone/token/providers/fernet/token_formatters.py @@ -20,7 +20,6 @@ import msgpack from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils -import six from six.moves import map from six.moves import urllib @@ -66,14 +65,22 @@ class TokenFormatter(object): return fernet.MultiFernet(fernet_instances) def pack(self, payload): - """Pack a payload for transport as a token.""" + """Pack a payload for transport as a token. + + :type payload: six.binary_type + :rtype: six.text_type + + """ # base64 padding (if any) is not URL-safe - return self.crypto.encrypt(payload).rstrip('=') + return self.crypto.encrypt(payload).rstrip(b'=').decode('utf-8') def unpack(self, token): - """Unpack a token, and validate the payload.""" - token = six.binary_type(token) + """Unpack a token, and validate the payload. + :type token: six.text_type + :rtype: six.binary_type + + """ # TODO(lbragstad): Restore padding on token before decoding it. # Initially in Kilo, Fernet tokens were returned to the user with # padding appended to the token. Later in Liberty this padding was @@ -89,16 +96,17 @@ class TokenFormatter(object): token = TokenFormatter.restore_padding(token) try: - return self.crypto.decrypt(token) + return self.crypto.decrypt(token.encode('utf-8')) except fernet.InvalidToken: raise exception.ValidationError( - _('This is not a recognized Fernet token')) + _('This is not a recognized Fernet token %s') % token) @classmethod def restore_padding(cls, token): """Restore padding based on token size. :param token: token to restore padding on + :type token: six.text_type :returns: token with correct padding """ @@ -106,21 +114,22 @@ class TokenFormatter(object): mod_returned = len(token) % 4 if mod_returned: missing_padding = 4 - mod_returned - token += b'=' * missing_padding + token += '=' * missing_padding return token @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) + """Returns the creation time of a valid Fernet token. - # Restore padding on token before decoding it + :type fernet_token: six.text_type + + """ fernet_token = TokenFormatter.restore_padding(fernet_token) + # fernet_token is six.text_type - # fernet tokens are base64 encoded, so we need to unpack them first - token_bytes = base64.urlsafe_b64decode(fernet_token) + # Fernet tokens are base64 encoded, so we need to unpack them first + # urlsafe_b64decode() requires six.binary_type + token_bytes = base64.urlsafe_b64decode(fernet_token.encode('utf-8')) # slice into the byte array to get just the timestamp timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END] @@ -136,66 +145,20 @@ class TokenFormatter(object): def create_token(self, user_id, expires_at, audit_ids, methods=None, domain_id=None, project_id=None, trust_id=None, - federated_info=None): + federated_info=None, access_token_id=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 project_id and federated_info: - version = FederatedProjectScopedPayload.version - payload = FederatedProjectScopedPayload.assemble( - user_id, - methods, - project_id, - expires_at, - audit_ids, - federated_info) - elif domain_id and federated_info: - version = FederatedDomainScopedPayload.version - payload = FederatedDomainScopedPayload.assemble( - user_id, - methods, - domain_id, - expires_at, - audit_ids, - federated_info) - elif federated_info: - version = FederatedUnscopedPayload.version - payload = FederatedUnscopedPayload.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) + for payload_class in PAYLOAD_CLASSES: + if payload_class.create_arguments_apply( + project_id=project_id, domain_id=domain_id, + trust_id=trust_id, federated_info=federated_info, + access_token_id=access_token_id): + break + + version = payload_class.version + payload = payload_class.assemble( + user_id, methods, project_id, domain_id, expires_at, audit_ids, + trust_id, federated_info, access_token_id + ) versioned_payload = (version,) + payload serialized_payload = msgpack.packb(versioned_payload) @@ -215,44 +178,21 @@ class TokenFormatter(object): 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') + """Validates a Fernet token and returns the payload attributes. + :type token: six.text_type + + """ 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 == FederatedUnscopedPayload.version: - (user_id, methods, expires_at, audit_ids, federated_info) = ( - FederatedUnscopedPayload.disassemble(payload)) - elif version == FederatedProjectScopedPayload.version: - (user_id, methods, project_id, expires_at, audit_ids, - federated_info) = FederatedProjectScopedPayload.disassemble( - payload) - elif version == FederatedDomainScopedPayload.version: - (user_id, methods, domain_id, expires_at, audit_ids, - federated_info) = FederatedDomainScopedPayload.disassemble( - payload) + for payload_class in PAYLOAD_CLASSES: + if version == payload_class.version: + (user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id) = ( + payload_class.disassemble(payload)) + break else: # If the token_format is not recognized, raise ValidationError. raise exception.ValidationError(_( @@ -267,7 +207,7 @@ class TokenFormatter(object): expires_at = ks_utils.isotime(at=expires_at, subsecond=True) return (user_id, methods, audit_ids, domain_id, project_id, trust_id, - federated_info, created_at, expires_at) + federated_info, access_token_id, created_at, expires_at) class BasePayload(object): @@ -275,10 +215,32 @@ class BasePayload(object): version = None @classmethod - def assemble(cls, *args): + def create_arguments_apply(cls, **kwargs): + """Check the arguments to see if they apply to this payload variant. + + :returns: True if the arguments indicate that this payload class is + needed for the token otherwise returns False. + :rtype: bool + + """ + raise NotImplementedError() + + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): """Assemble the payload of a token. - :param args: whatever data should go into the payload + :param user_id: identifier 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 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 + :param trust_id: ID of the trust in effect + :param federated_info: dictionary containing group IDs, the identity + provider ID, protocol ID, and federated domain + ID + :param access_token_id: ID of the secret in OAuth1 authentication :returns: the payload of a token """ @@ -288,6 +250,17 @@ class BasePayload(object): def disassemble(cls, payload): """Disassemble an unscoped payload into the component data. + The tuple consists of:: + + (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) + + * ``methods`` are the auth methods. + * federated_info is a dict contains the group IDs, the identity + provider ID, the protocol ID, and the federated domain ID + + Fields will be set to None if they didn't apply to this payload type. + :param payload: this variant of payload :returns: a tuple of the payloads component data @@ -302,9 +275,6 @@ class BasePayload(object): :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 @@ -316,18 +286,15 @@ class BasePayload(object): :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. + def _convert_time_string_to_float(cls, time_string): + """Convert a time formatted string to a float. :param time_string: time formatted string - :returns: an integer timestamp + :returns: a timestamp as a float """ time_object = timeutils.parse_isotime(time_string) @@ -335,14 +302,14 @@ class BasePayload(object): datetime.datetime.utcfromtimestamp(0)).total_seconds() @classmethod - def _convert_int_to_time_string(cls, time_int): - """Convert a timestamp integer to a string. + def _convert_float_to_time_string(cls, time_float): + """Convert a floating point timestamp to a string. - :param time_int: integer representing timestamp + :param time_float: integer representing timestamp :returns: a time formatted strings """ - time_object = datetime.datetime.utcfromtimestamp(time_int) + time_object = datetime.datetime.utcfromtimestamp(time_float) return ks_utils.isotime(time_object, subsecond=True) @classmethod @@ -361,74 +328,51 @@ class BasePayload(object): # federation) return (False, 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 + def create_arguments_apply(cls, **kwargs): + return True - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_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) + expires_at_int = cls._convert_time_string_to_float(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 - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) - expires_at_str = cls._convert_int_to_time_string(payload[2]) + expires_at_str = cls._convert_float_to_time_string(payload[2]) audit_ids = list(map(provider.base64_encode, payload[3])) - return (user_id, methods, expires_at_str, audit_ids) + project_id = None + domain_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) 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. + def create_arguments_apply(cls, **kwargs): + return kwargs['domain_id'] - :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 - - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) try: @@ -439,23 +383,16 @@ class DomainScopedPayload(BasePayload): b_domain_id = domain_id else: raise - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(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 - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) try: domain_id = cls.convert_uuid_bytes_to_hex(payload[2]) @@ -465,79 +402,68 @@ class DomainScopedPayload(BasePayload): domain_id = payload[2] else: raise - expires_at_str = cls._convert_int_to_time_string(payload[3]) + expires_at_str = cls._convert_float_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) + project_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) 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. + def create_arguments_apply(cls, **kwargs): + return kwargs['project_id'] - :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 - - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(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 - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, project_id) = payload[2] if is_stored_as_bytes: - project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id) - expires_at_str = cls._convert_int_to_time_string(payload[3]) + project_id = cls.convert_uuid_bytes_to_hex(project_id) + expires_at_str = cls._convert_float_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) + domain_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) 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 + def create_arguments_apply(cls, **kwargs): + return kwargs['trust_id'] - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) b_project_id = cls.attempt_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) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -546,32 +472,31 @@ class TrustScopedPayload(BasePayload): @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 - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, project_id) = payload[2] if is_stored_as_bytes: - project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id) - expires_at_str = cls._convert_int_to_time_string(payload[3]) + project_id = cls.convert_uuid_bytes_to_hex(project_id) + expires_at_str = cls._convert_float_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) + domain_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedUnscopedPayload(BasePayload): version = 4 @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['federated_info'] + + @classmethod def pack_group_id(cls, group_dict): return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id']) @@ -579,24 +504,12 @@ class FederatedUnscopedPayload(BasePayload): def unpack_group_id(cls, group_id_in_bytes): (is_stored_as_bytes, group_id) = group_id_in_bytes if is_stored_as_bytes: - group_id = cls.attempt_convert_uuid_bytes_to_hex(group_id) + group_id = cls.convert_uuid_bytes_to_hex(group_id) return {'id': group_id} @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 assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_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 = list(map(cls.pack_group_id, @@ -604,7 +517,7 @@ class FederatedUnscopedPayload(BasePayload): 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) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -613,59 +526,43 @@ class FederatedUnscopedPayload(BasePayload): @classmethod def disassemble(cls, payload): - """Validate a federated payload. - - :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 group - IDs, the identity provider ID, the protocol ID, and the - federated domain ID - - """ - (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) group_ids = list(map(cls.unpack_group_id, payload[2])) (is_stored_as_bytes, idp_id) = payload[3] if is_stored_as_bytes: - idp_id = cls.attempt_convert_uuid_bytes_to_hex(idp_id) + idp_id = cls.convert_uuid_bytes_to_hex(idp_id) protocol_id = payload[4] - expires_at_str = cls._convert_int_to_time_string(payload[5]) + expires_at_str = cls._convert_float_to_time_string(payload[5]) audit_ids = list(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) + project_id = None + domain_id = None + trust_id = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedScopedPayload(FederatedUnscopedPayload): version = None @classmethod - def assemble(cls, user_id, methods, scope_id, expires_at, audit_ids, - federated_info): - """Assemble the project-scoped payload of a federated token. - - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param scope_id: ID of the project or domain ID to scope to - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :param federated_info: dictionary containing the identity provider ID, - protocol ID, federated domain ID and group IDs - :returns: the payload of a federated token - - """ + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(scope_id) + b_scope_id = cls.attempt_convert_uuid_hex_to_bytes( + project_id or domain_id) b_group_ids = list(map(cls.pack_group_id, 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) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -674,39 +571,107 @@ class FederatedScopedPayload(FederatedUnscopedPayload): @classmethod def disassemble(cls, payload): - """Validate a project-scoped federated payload. - - :param token_string: a string representing the token - :returns: a tuple containing the user_id, auth methods, scope_id, - expiration time (as str), audit_ids, and a dictionary - containing federated information such as the the identity - provider ID, the protocol ID, the federated domain ID and - group IDs - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, scope_id) = payload[2] if is_stored_as_bytes: - scope_id = cls.attempt_convert_uuid_bytes_to_hex(scope_id) + scope_id = cls.convert_uuid_bytes_to_hex(scope_id) + project_id = ( + scope_id + if cls.version == FederatedProjectScopedPayload.version else None) + domain_id = ( + scope_id + if cls.version == FederatedDomainScopedPayload.version else None) group_ids = list(map(cls.unpack_group_id, payload[3])) (is_stored_as_bytes, idp_id) = payload[4] if is_stored_as_bytes: - idp_id = cls.attempt_convert_uuid_bytes_to_hex(idp_id) + idp_id = cls.convert_uuid_bytes_to_hex(idp_id) protocol_id = payload[5] - expires_at_str = cls._convert_int_to_time_string(payload[6]) + expires_at_str = cls._convert_float_to_time_string(payload[6]) audit_ids = list(map(provider.base64_encode, payload[7])) federated_info = dict(idp_id=idp_id, protocol_id=protocol_id, group_ids=group_ids) - return (user_id, methods, scope_id, expires_at_str, audit_ids, - federated_info) + trust_id = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedProjectScopedPayload(FederatedScopedPayload): version = 5 + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['project_id'] and kwargs['federated_info'] + class FederatedDomainScopedPayload(FederatedScopedPayload): version = 6 + + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['domain_id'] and kwargs['federated_info'] + + +class OauthScopedPayload(BasePayload): + version = 7 + + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['access_token_id'] + + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) + expires_at_int = cls._convert_time_string_to_float(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes( + access_token_id) + return (b_user_id, methods, b_project_id, b_access_token_id, + expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + (is_stored_as_bytes, user_id) = payload[0] + if is_stored_as_bytes: + user_id = cls.convert_uuid_bytes_to_hex(user_id) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + (is_stored_as_bytes, project_id) = payload[2] + if is_stored_as_bytes: + project_id = cls.convert_uuid_bytes_to_hex(project_id) + (is_stored_as_bytes, access_token_id) = payload[3] + if is_stored_as_bytes: + access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id) + expires_at_str = cls._convert_float_to_time_string(payload[4]) + audit_ids = list(map(provider.base64_encode, payload[5])) + domain_id = None + trust_id = None + federated_info = None + + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) + + +# For now, the order of the classes in the following list is important. This +# is because the way they test that the payload applies to them in +# the create_arguments_apply method requires that the previous ones rejected +# the payload arguments. For example, UnscopedPayload must be last since it's +# the catch-all after all the other payloads have been checked. +# TODO(blk-u): Clean up the create_arguments_apply methods so that they don't +# depend on the previous classes then these can be in any order. +PAYLOAD_CLASSES = [ + OauthScopedPayload, + TrustScopedPayload, + FederatedProjectScopedPayload, + FederatedDomainScopedPayload, + FederatedUnscopedPayload, + ProjectScopedPayload, + DomainScopedPayload, + UnscopedPayload, +] diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py index 4235eda8..1c3552d4 100644 --- a/keystone-moon/keystone/token/providers/fernet/utils.py +++ b/keystone-moon/keystone/token/providers/fernet/utils.py @@ -25,29 +25,33 @@ LOG = log.getLogger(__name__) CONF = cfg.CONF -def validate_key_repository(): +def validate_key_repository(requires_write=False): """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)): + # ensure current user has sufficient access to the key repository + is_valid = (os.access(CONF.fernet_tokens.key_repository, os.R_OK) and + os.access(CONF.fernet_tokens.key_repository, os.X_OK)) + if requires_write: + is_valid = (is_valid and + os.access(CONF.fernet_tokens.key_repository, os.W_OK)) + + if not is_valid: 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) + else: + # 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 + return is_valid def _convert_to_integers(id_value): @@ -99,7 +103,7 @@ def _create_new_key(keystone_user_id, keystone_group_id): Create a new key that is readable by the Keystone group and Keystone user. """ - key = fernet.Fernet.generate_key() + key = fernet.Fernet.generate_key() # key is bytes # This ensures the key created is not world-readable old_umask = os.umask(0o177) @@ -117,7 +121,7 @@ def _create_new_key(keystone_user_id, keystone_group_id): key_file = os.path.join(CONF.fernet_tokens.key_repository, '0') try: with open(key_file, 'w') as f: - f.write(key) + f.write(key.decode('utf-8')) # convert key to str for the file. 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 @@ -176,7 +180,7 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None): if os.path.isfile(path): try: key_id = int(filename) - except ValueError: + except ValueError: # nosec : name isn't a number, ignore the file. pass else: key_files[key_id] = path @@ -243,7 +247,8 @@ def load_keys(): with open(path, 'r') as key_file: try: key_id = int(filename) - except ValueError: + except ValueError: # nosec : filename isn't a number, ignore + # this file since it's not a key. pass else: keys[key_id] = key_file.read() |