diff options
Diffstat (limited to 'keystone-moon/keystone/contrib/federation')
21 files changed, 0 insertions, 2578 deletions
diff --git a/keystone-moon/keystone/contrib/federation/__init__.py b/keystone-moon/keystone/contrib/federation/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/contrib/federation/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/contrib/federation/backends/__init__.py b/keystone-moon/keystone/contrib/federation/backends/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/contrib/federation/backends/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/contrib/federation/backends/sql.py b/keystone-moon/keystone/contrib/federation/backends/sql.py deleted file mode 100644 index 3c24d9c0..00000000 --- a/keystone-moon/keystone/contrib/federation/backends/sql.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# -# 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_log import versionutils - -from keystone.federation.backends import sql - -_OLD = "keystone.contrib.federation.backends.sql.Federation" -_NEW = "sql" - - -class Federation(sql.Federation): - - @versionutils.deprecated(versionutils.deprecated.MITAKA, - in_favor_of=_NEW, - what=_OLD) - def __init__(self, *args, **kwargs): - super(Federation, self).__init__(*args, **kwargs) diff --git a/keystone-moon/keystone/contrib/federation/constants.py b/keystone-moon/keystone/contrib/federation/constants.py deleted file mode 100644 index afb38494..00000000 --- a/keystone-moon/keystone/contrib/federation/constants.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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. - -FEDERATION = 'OS-FEDERATION' -IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider' -PROTOCOL = 'OS-FEDERATION:protocol' diff --git a/keystone-moon/keystone/contrib/federation/controllers.py b/keystone-moon/keystone/contrib/federation/controllers.py deleted file mode 100644 index d0bd2bce..00000000 --- a/keystone-moon/keystone/contrib/federation/controllers.py +++ /dev/null @@ -1,520 +0,0 @@ -# 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. - -"""Workflow logic for the Federation service.""" - -import string - -from oslo_config import cfg -from oslo_log import log -import six -from six.moves import urllib -import webob - -from keystone.auth import controllers as auth_controllers -from keystone.common import authorization -from keystone.common import controller -from keystone.common import dependency -from keystone.common import validation -from keystone.common import wsgi -from keystone.contrib.federation import idp as keystone_idp -from keystone.contrib.federation import schema -from keystone.contrib.federation import utils -from keystone import exception -from keystone.i18n import _ -from keystone.models import token_model - - -CONF = cfg.CONF -LOG = log.getLogger(__name__) - - -class _ControllerBase(controller.V3Controller): - """Base behaviors for federation controllers.""" - - @classmethod - def base_url(cls, context, path=None): - """Construct a path and pass it to V3Controller.base_url method.""" - - path = '/OS-FEDERATION/' + cls.collection_name - return super(_ControllerBase, cls).base_url(context, path=path) - - -@dependency.requires('federation_api') -class IdentityProvider(_ControllerBase): - """Identity Provider representation.""" - collection_name = 'identity_providers' - member_name = 'identity_provider' - - _mutable_parameters = frozenset(['description', 'enabled', 'remote_ids']) - _public_parameters = frozenset(['id', 'enabled', 'description', - 'remote_ids', 'links' - ]) - - @classmethod - def _add_related_links(cls, context, ref): - """Add URLs for entities related with Identity Provider. - - Add URLs pointing to: - - protocols tied to the Identity Provider - - """ - ref.setdefault('links', {}) - base_path = ref['links'].get('self') - if base_path is None: - base_path = '/'.join([IdentityProvider.base_url(context), - ref['id']]) - for name in ['protocols']: - ref['links'][name] = '/'.join([base_path, name]) - - @classmethod - def _add_self_referential_link(cls, context, ref): - id = ref.get('id') - self_path = '/'.join([cls.base_url(context), id]) - ref.setdefault('links', {}) - ref['links']['self'] = self_path - - @classmethod - def wrap_member(cls, context, ref): - cls._add_self_referential_link(context, ref) - cls._add_related_links(context, ref) - ref = cls.filter_params(ref) - return {cls.member_name: ref} - - @controller.protected() - def create_identity_provider(self, context, idp_id, identity_provider): - identity_provider = self._normalize_dict(identity_provider) - identity_provider.setdefault('enabled', False) - IdentityProvider.check_immutable_params(identity_provider) - idp_ref = self.federation_api.create_idp(idp_id, identity_provider) - response = IdentityProvider.wrap_member(context, idp_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - def list_identity_providers(self, context): - ref = self.federation_api.list_idps() - ref = [self.filter_params(x) for x in ref] - return IdentityProvider.wrap_collection(context, ref) - - @controller.protected() - def get_identity_provider(self, context, idp_id): - ref = self.federation_api.get_idp(idp_id) - return IdentityProvider.wrap_member(context, ref) - - @controller.protected() - def delete_identity_provider(self, context, idp_id): - self.federation_api.delete_idp(idp_id) - - @controller.protected() - def update_identity_provider(self, context, idp_id, identity_provider): - identity_provider = self._normalize_dict(identity_provider) - IdentityProvider.check_immutable_params(identity_provider) - idp_ref = self.federation_api.update_idp(idp_id, identity_provider) - return IdentityProvider.wrap_member(context, idp_ref) - - -@dependency.requires('federation_api') -class FederationProtocol(_ControllerBase): - """A federation protocol representation. - - See IdentityProvider docstring for explanation on _mutable_parameters - and _public_parameters class attributes. - - """ - collection_name = 'protocols' - member_name = 'protocol' - - _public_parameters = frozenset(['id', 'mapping_id', 'links']) - _mutable_parameters = frozenset(['mapping_id']) - - @classmethod - def _add_self_referential_link(cls, context, ref): - """Add 'links' entry to the response dictionary. - - Calls IdentityProvider.base_url() class method, as it constructs - proper URL along with the 'identity providers' part included. - - :param ref: response dictionary - - """ - ref.setdefault('links', {}) - base_path = ref['links'].get('identity_provider') - if base_path is None: - base_path = [IdentityProvider.base_url(context), ref['idp_id']] - base_path = '/'.join(base_path) - self_path = [base_path, 'protocols', ref['id']] - self_path = '/'.join(self_path) - ref['links']['self'] = self_path - - @classmethod - def _add_related_links(cls, context, ref): - """Add new entries to the 'links' subdictionary in the response. - - Adds 'identity_provider' key with URL pointing to related identity - provider as a value. - - :param ref: response dictionary - - """ - ref.setdefault('links', {}) - base_path = '/'.join([IdentityProvider.base_url(context), - ref['idp_id']]) - ref['links']['identity_provider'] = base_path - - @classmethod - def wrap_member(cls, context, ref): - cls._add_related_links(context, ref) - cls._add_self_referential_link(context, ref) - ref = cls.filter_params(ref) - return {cls.member_name: ref} - - @controller.protected() - def create_protocol(self, context, idp_id, protocol_id, protocol): - ref = self._normalize_dict(protocol) - FederationProtocol.check_immutable_params(ref) - ref = self.federation_api.create_protocol(idp_id, protocol_id, ref) - response = FederationProtocol.wrap_member(context, ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - def update_protocol(self, context, idp_id, protocol_id, protocol): - ref = self._normalize_dict(protocol) - FederationProtocol.check_immutable_params(ref) - ref = self.federation_api.update_protocol(idp_id, protocol_id, - protocol) - return FederationProtocol.wrap_member(context, ref) - - @controller.protected() - def get_protocol(self, context, idp_id, protocol_id): - ref = self.federation_api.get_protocol(idp_id, protocol_id) - return FederationProtocol.wrap_member(context, ref) - - @controller.protected() - def list_protocols(self, context, idp_id): - protocols_ref = self.federation_api.list_protocols(idp_id) - protocols = list(protocols_ref) - return FederationProtocol.wrap_collection(context, protocols) - - @controller.protected() - def delete_protocol(self, context, idp_id, protocol_id): - self.federation_api.delete_protocol(idp_id, protocol_id) - - -@dependency.requires('federation_api') -class MappingController(_ControllerBase): - collection_name = 'mappings' - member_name = 'mapping' - - @controller.protected() - def create_mapping(self, context, mapping_id, mapping): - ref = self._normalize_dict(mapping) - utils.validate_mapping_structure(ref) - mapping_ref = self.federation_api.create_mapping(mapping_id, ref) - response = MappingController.wrap_member(context, mapping_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - def list_mappings(self, context): - ref = self.federation_api.list_mappings() - return MappingController.wrap_collection(context, ref) - - @controller.protected() - def get_mapping(self, context, mapping_id): - ref = self.federation_api.get_mapping(mapping_id) - return MappingController.wrap_member(context, ref) - - @controller.protected() - def delete_mapping(self, context, mapping_id): - self.federation_api.delete_mapping(mapping_id) - - @controller.protected() - def update_mapping(self, context, mapping_id, mapping): - mapping = self._normalize_dict(mapping) - utils.validate_mapping_structure(mapping) - mapping_ref = self.federation_api.update_mapping(mapping_id, mapping) - return MappingController.wrap_member(context, mapping_ref) - - -@dependency.requires('federation_api') -class Auth(auth_controllers.Auth): - - def _get_sso_origin_host(self, context): - """Validate and return originating dashboard URL. - - Make sure the parameter is specified in the request's URL as well its - value belongs to a list of trusted dashboards. - - :param context: request's context - :raises: exception.ValidationError: ``origin`` query parameter was not - specified. The URL is deemed invalid. - :raises: exception.Unauthorized: URL specified in origin query - parameter does not exist in list of websso trusted dashboards. - :returns: URL with the originating dashboard - - """ - if 'origin' in context['query_string']: - origin = context['query_string'].get('origin') - host = urllib.parse.unquote_plus(origin) - else: - msg = _('Request must have an origin query parameter') - LOG.error(msg) - raise exception.ValidationError(msg) - - if host not in CONF.federation.trusted_dashboard: - msg = _('%(host)s is not a trusted dashboard host') - msg = msg % {'host': host} - LOG.error(msg) - raise exception.Unauthorized(msg) - - return host - - def federated_authentication(self, context, identity_provider, protocol): - """Authenticate from dedicated url endpoint. - - Build HTTP request body for federated authentication and inject - it into the ``authenticate_for_token`` function. - - """ - auth = { - 'identity': { - 'methods': [protocol], - protocol: { - 'identity_provider': identity_provider, - 'protocol': protocol - } - } - } - - return self.authenticate_for_token(context, auth=auth) - - def federated_sso_auth(self, context, protocol_id): - try: - remote_id_name = utils.get_remote_id_parameter(protocol_id) - remote_id = context['environment'][remote_id_name] - except KeyError: - msg = _('Missing entity ID from environment') - LOG.error(msg) - raise exception.Unauthorized(msg) - - host = self._get_sso_origin_host(context) - - ref = self.federation_api.get_idp_from_remote_id(remote_id) - # NOTE(stevemar): the returned object is a simple dict that - # contains the idp_id and remote_id. - identity_provider = ref['idp_id'] - res = self.federated_authentication(context, identity_provider, - protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def federated_idp_specific_sso_auth(self, context, idp_id, protocol_id): - host = self._get_sso_origin_host(context) - - # NOTE(lbragstad): We validate that the Identity Provider actually - # exists in the Mapped authentication plugin. - res = self.federated_authentication(context, idp_id, protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def render_html_response(self, host, token_id): - """Forms an HTML Form from a template with autosubmit.""" - - headers = [('Content-Type', 'text/html')] - - with open(CONF.federation.sso_callback_template) as template: - src = string.Template(template.read()) - - subs = {'host': host, 'token': token_id} - body = src.substitute(subs) - return webob.Response(body=body, status='200', - headerlist=headers) - - def _create_base_saml_assertion(self, context, auth): - issuer = CONF.saml.idp_entity_id - sp_id = auth['scope']['service_provider']['id'] - service_provider = self.federation_api.get_sp(sp_id) - utils.assert_enabled_service_provider_object(service_provider) - sp_url = service_provider.get('sp_url') - - token_id = auth['identity']['token']['id'] - token_data = self.token_provider_api.validate_token(token_id) - token_ref = token_model.KeystoneToken(token_id, token_data) - - if not token_ref.project_scoped: - action = _('Use a project scoped token when attempting to create ' - 'a SAML assertion') - raise exception.ForbiddenAction(action=action) - - subject = token_ref.user_name - roles = token_ref.role_names - project = token_ref.project_name - # NOTE(rodrigods): the domain name is necessary in order to distinguish - # between projects and users with the same name in different domains. - project_domain_name = token_ref.project_domain_name - subject_domain_name = token_ref.user_domain_name - - generator = keystone_idp.SAMLGenerator() - response = generator.samlize_token( - issuer, sp_url, subject, subject_domain_name, - roles, project, project_domain_name) - return (response, service_provider) - - def _build_response_headers(self, service_provider): - return [('Content-Type', 'text/xml'), - ('X-sp-url', six.binary_type(service_provider['sp_url'])), - ('X-auth-url', six.binary_type(service_provider['auth_url']))] - - @validation.validated(schema.saml_create, 'auth') - def create_saml_assertion(self, context, auth): - """Exchange a scoped token for a SAML assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: SAML Assertion based on properties from the token - """ - - t = self._create_base_saml_assertion(context, auth) - (response, service_provider) = t - - headers = self._build_response_headers(service_provider) - return wsgi.render_response(body=response.to_string(), - status=('200', 'OK'), - headers=headers) - - @validation.validated(schema.saml_create, 'auth') - def create_ecp_assertion(self, context, auth): - """Exchange a scoped token for an ECP assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: ECP Assertion based on properties from the token - """ - - t = self._create_base_saml_assertion(context, auth) - (saml_assertion, service_provider) = t - relay_state_prefix = service_provider.get('relay_state_prefix') - - generator = keystone_idp.ECPGenerator() - ecp_assertion = generator.generate_ecp(saml_assertion, - relay_state_prefix) - - headers = self._build_response_headers(service_provider) - return wsgi.render_response(body=ecp_assertion.to_string(), - status=('200', 'OK'), - headers=headers) - - -@dependency.requires('assignment_api', 'resource_api') -class DomainV3(controller.V3Controller): - collection_name = 'domains' - member_name = 'domain' - - def __init__(self): - super(DomainV3, self).__init__() - self.get_member_from_driver = self.resource_api.get_domain - - @controller.protected() - def list_domains_for_groups(self, context): - """List all domains available to an authenticated user's groups. - - :param context: request context - :returns: list of accessible domains - - """ - auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] - domains = self.assignment_api.list_domains_for_groups( - auth_context['group_ids']) - return DomainV3.wrap_collection(context, domains) - - -@dependency.requires('assignment_api', 'resource_api') -class ProjectAssignmentV3(controller.V3Controller): - collection_name = 'projects' - member_name = 'project' - - def __init__(self): - super(ProjectAssignmentV3, self).__init__() - self.get_member_from_driver = self.resource_api.get_project - - @controller.protected() - def list_projects_for_groups(self, context): - """List all projects available to an authenticated user's groups. - - :param context: request context - :returns: list of accessible projects - - """ - auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] - projects = self.assignment_api.list_projects_for_groups( - auth_context['group_ids']) - return ProjectAssignmentV3.wrap_collection(context, projects) - - -@dependency.requires('federation_api') -class ServiceProvider(_ControllerBase): - """Service Provider representation.""" - - collection_name = 'service_providers' - member_name = 'service_provider' - - _mutable_parameters = frozenset(['auth_url', 'description', 'enabled', - 'relay_state_prefix', 'sp_url']) - _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', - 'links', 'relay_state_prefix', 'sp_url']) - - @controller.protected() - @validation.validated(schema.service_provider_create, 'service_provider') - def create_service_provider(self, context, sp_id, service_provider): - service_provider = self._normalize_dict(service_provider) - service_provider.setdefault('enabled', False) - service_provider.setdefault('relay_state_prefix', - CONF.saml.relay_state_prefix) - ServiceProvider.check_immutable_params(service_provider) - sp_ref = self.federation_api.create_sp(sp_id, service_provider) - response = ServiceProvider.wrap_member(context, sp_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - def list_service_providers(self, context): - ref = self.federation_api.list_sps() - ref = [self.filter_params(x) for x in ref] - return ServiceProvider.wrap_collection(context, ref) - - @controller.protected() - def get_service_provider(self, context, sp_id): - ref = self.federation_api.get_sp(sp_id) - return ServiceProvider.wrap_member(context, ref) - - @controller.protected() - def delete_service_provider(self, context, sp_id): - self.federation_api.delete_sp(sp_id) - - @controller.protected() - @validation.validated(schema.service_provider_update, 'service_provider') - def update_service_provider(self, context, sp_id, service_provider): - service_provider = self._normalize_dict(service_provider) - ServiceProvider.check_immutable_params(service_provider) - sp_ref = self.federation_api.update_sp(sp_id, service_provider) - return ServiceProvider.wrap_member(context, sp_ref) - - -class SAMLMetadataV3(_ControllerBase): - member_name = 'metadata' - - def get_metadata(self, context): - metadata_path = CONF.saml.idp_metadata_path - try: - with open(metadata_path, 'r') as metadata_handler: - metadata = metadata_handler.read() - except IOError as e: - # Raise HTTP 500 in case Metadata file cannot be read. - raise exception.MetadataFileError(reason=e) - return wsgi.render_response(body=metadata, status=('200', 'OK'), - headers=[('Content-Type', 'text/xml')]) diff --git a/keystone-moon/keystone/contrib/federation/core.py b/keystone-moon/keystone/contrib/federation/core.py deleted file mode 100644 index 1595be1d..00000000 --- a/keystone-moon/keystone/contrib/federation/core.py +++ /dev/null @@ -1,355 +0,0 @@ -# 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. - -"""Main entry point into the Federation service.""" - -import abc - -from oslo_config import cfg -from oslo_log import log as logging -import six - -from keystone.common import dependency -from keystone.common import extension -from keystone.common import manager -from keystone.contrib.federation import utils -from keystone import exception - - -CONF = cfg.CONF -LOG = logging.getLogger(__name__) -EXTENSION_DATA = { - 'name': 'OpenStack Federation APIs', - 'namespace': 'http://docs.openstack.org/identity/api/ext/' - 'OS-FEDERATION/v1.0', - 'alias': 'OS-FEDERATION', - 'updated': '2013-12-17T12:00:0-00:00', - 'description': 'OpenStack Identity Providers Mechanism.', - 'links': [{ - 'rel': 'describedby', - 'type': 'text/html', - 'href': 'https://github.com/openstack/identity-api' - }]} -extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) -extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) - - -@dependency.provider('federation_api') -class Manager(manager.Manager): - """Default pivot point for the Federation backend. - - See :mod:`keystone.common.manager.Manager` for more details on how this - dynamically calls the backend. - - """ - - driver_namespace = 'keystone.federation' - - def __init__(self): - super(Manager, self).__init__(CONF.federation.driver) - - def get_enabled_service_providers(self): - """List enabled service providers for Service Catalog - - Service Provider in a catalog contains three attributes: ``id``, - ``auth_url``, ``sp_url``, where: - - - id is an unique, user defined identifier for service provider object - - auth_url is a authentication URL of remote Keystone - - sp_url a URL accessible at the remote service provider where SAML - assertion is transmitted. - - :returns: list of dictionaries with enabled service providers - :rtype: list of dicts - - """ - def normalize(sp): - ref = { - 'auth_url': sp.auth_url, - 'id': sp.id, - 'sp_url': sp.sp_url - } - return ref - - service_providers = self.driver.get_enabled_service_providers() - return [normalize(sp) for sp in service_providers] - - def evaluate(self, idp_id, protocol_id, assertion_data): - mapping = self.get_mapping_from_idp_and_protocol(idp_id, protocol_id) - rules = mapping['rules'] - rule_processor = utils.RuleProcessor(rules) - mapped_properties = rule_processor.process(assertion_data) - return mapped_properties, mapping['id'] - - -@six.add_metaclass(abc.ABCMeta) -class FederationDriverV8(object): - - @abc.abstractmethod - def create_idp(self, idp_id, idp): - """Create an identity provider. - - :returns: idp_ref - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_idp(self, idp_id): - """Delete an identity provider. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_idps(self): - """List all identity providers. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_idp(self, idp_id): - """Get an identity provider by ID. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_idp_from_remote_id(self, remote_id): - """Get an identity provider by remote ID. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_idp(self, idp_id, idp): - """Update an identity provider by ID. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_protocol(self, idp_id, protocol_id, protocol): - """Add an IdP-Protocol configuration. - - :raises: keystone.exception.IdentityProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_protocol(self, idp_id, protocol_id, protocol): - """Change an IdP-Protocol configuration. - - :raises: keystone.exception.IdentityProviderNotFound, - keystone.exception.FederatedProtocolNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_protocol(self, idp_id, protocol_id): - """Get an IdP-Protocol configuration. - - :raises: keystone.exception.IdentityProviderNotFound, - keystone.exception.FederatedProtocolNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_protocols(self, idp_id): - """List an IdP's supported protocols. - - :raises: keystone.exception.IdentityProviderNotFound, - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_protocol(self, idp_id, protocol_id): - """Delete an IdP-Protocol configuration. - - :raises: keystone.exception.IdentityProviderNotFound, - keystone.exception.FederatedProtocolNotFound, - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_mapping(self, mapping_ref): - """Create a mapping. - - :param mapping_ref: mapping ref with mapping name - :type mapping_ref: dict - :returns: mapping_ref - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_mapping(self, mapping_id): - """Delete a mapping. - - :param mapping_id: id of mapping to delete - :type mapping_ref: string - :returns: None - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_mapping(self, mapping_id, mapping_ref): - """Update a mapping. - - :param mapping_id: id of mapping to update - :type mapping_id: string - :param mapping_ref: new mapping ref - :type mapping_ref: dict - :returns: mapping_ref - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_mappings(self): - """List all mappings. - - returns: list of mappings - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_mapping(self, mapping_id): - """Get a mapping, returns the mapping based - on mapping_id. - - :param mapping_id: id of mapping to get - :type mapping_ref: string - :returns: mapping_ref - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): - """Get mapping based on idp_id and protocol_id. - - :param idp_id: id of the identity provider - :type idp_id: string - :param protocol_id: id of the protocol - :type protocol_id: string - :raises: keystone.exception.IdentityProviderNotFound, - keystone.exception.FederatedProtocolNotFound, - :returns: mapping_ref - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_sp(self, sp_id, sp): - """Create a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - :param sp: service prvider object - :type sp: dict - - :returns: sp_ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_sp(self, sp_id): - """Delete a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - - :raises: keystone.exception.ServiceProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_sps(self): - """List all service providers. - - :returns List of sp_ref objects - :rtype: list of dicts - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_sp(self, sp_id): - """Get a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - - :returns: sp_ref - :raises: keystone.exception.ServiceProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_sp(self, sp_id, sp): - """Update a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - :param sp: service prvider object - :type sp: dict - - :returns: sp_ref - :rtype: dict - - :raises: keystone.exception.ServiceProviderNotFound - - """ - raise exception.NotImplemented() # pragma: no cover - - def get_enabled_service_providers(self): - """List enabled service providers for Service Catalog - - Service Provider in a catalog contains three attributes: ``id``, - ``auth_url``, ``sp_url``, where: - - - id is an unique, user defined identifier for service provider object - - auth_url is a authentication URL of remote Keystone - - sp_url a URL accessible at the remote service provider where SAML - assertion is transmitted. - - :returns: list of dictionaries with enabled service providers - :rtype: list of dicts - - """ - raise exception.NotImplemented() # pragma: no cover - - -Driver = manager.create_legacy_driver(FederationDriverV8) diff --git a/keystone-moon/keystone/contrib/federation/idp.py b/keystone-moon/keystone/contrib/federation/idp.py deleted file mode 100644 index 51689989..00000000 --- a/keystone-moon/keystone/contrib/federation/idp.py +++ /dev/null @@ -1,609 +0,0 @@ -# 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 datetime -import os -import uuid - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import fileutils -from oslo_utils import importutils -from oslo_utils import timeutils -import saml2 -from saml2 import client_base -from saml2 import md -from saml2.profile import ecp -from saml2 import saml -from saml2 import samlp -from saml2.schema import soapenv -from saml2 import sigver -xmldsig = importutils.try_import("saml2.xmldsig") -if not xmldsig: - xmldsig = importutils.try_import("xmldsig") - -from keystone.common import environment -from keystone.common import utils -from keystone import exception -from keystone.i18n import _, _LE - - -subprocess = environment.subprocess - -LOG = log.getLogger(__name__) -CONF = cfg.CONF - - -class SAMLGenerator(object): - """A class to generate SAML assertions.""" - - def __init__(self): - self.assertion_id = uuid.uuid4().hex - - def samlize_token(self, issuer, recipient, user, user_domain_name, roles, - project, project_domain_name, expires_in=None): - """Convert Keystone attributes to a SAML assertion. - - :param issuer: URL of the issuing party - :type issuer: string - :param recipient: URL of the recipient - :type recipient: string - :param user: User name - :type user: string - :param user_domain_name: User Domain name - :type user_domain_name: string - :param roles: List of role names - :type roles: list - :param project: Project name - :type project: string - :param project_domain_name: Project Domain name - :type project_domain_name: string - :param expires_in: Sets how long the assertion is valid for, in seconds - :type expires_in: int - - :return: XML <Response> object - - """ - expiration_time = self._determine_expiration_time(expires_in) - status = self._create_status() - saml_issuer = self._create_issuer(issuer) - subject = self._create_subject(user, expiration_time, recipient) - attribute_statement = self._create_attribute_statement( - user, user_domain_name, roles, project, project_domain_name) - authn_statement = self._create_authn_statement(issuer, expiration_time) - signature = self._create_signature() - - assertion = self._create_assertion(saml_issuer, signature, - subject, authn_statement, - attribute_statement) - - assertion = _sign_assertion(assertion) - - response = self._create_response(saml_issuer, status, assertion, - recipient) - return response - - def _determine_expiration_time(self, expires_in): - if expires_in is None: - expires_in = CONF.saml.assertion_expiration_time - now = timeutils.utcnow() - future = now + datetime.timedelta(seconds=expires_in) - return utils.isotime(future, subsecond=True) - - def _create_status(self): - """Create an object that represents a SAML Status. - - <ns0:Status xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol"> - <ns0:StatusCode - Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> - </ns0:Status> - - :return: XML <Status> object - - """ - status = samlp.Status() - status_code = samlp.StatusCode() - status_code.value = samlp.STATUS_SUCCESS - status_code.set_text('') - status.status_code = status_code - return status - - def _create_issuer(self, issuer_url): - """Create an object that represents a SAML Issuer. - - <ns0:Issuer - xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"> - https://acme.com/FIM/sps/openstack/saml20</ns0:Issuer> - - :return: XML <Issuer> object - - """ - issuer = saml.Issuer() - issuer.format = saml.NAMEID_FORMAT_ENTITY - issuer.set_text(issuer_url) - return issuer - - def _create_subject(self, user, expiration_time, recipient): - """Create an object that represents a SAML Subject. - - <ns0:Subject> - <ns0:NameID> - john@smith.com</ns0:NameID> - <ns0:SubjectConfirmation - Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> - <ns0:SubjectConfirmationData - NotOnOrAfter="2014-08-19T11:53:57.243106Z" - Recipient="http://beta.com/Shibboleth.sso/SAML2/POST" /> - </ns0:SubjectConfirmation> - </ns0:Subject> - - :return: XML <Subject> object - - """ - name_id = saml.NameID() - name_id.set_text(user) - subject_conf_data = saml.SubjectConfirmationData() - subject_conf_data.recipient = recipient - subject_conf_data.not_on_or_after = expiration_time - subject_conf = saml.SubjectConfirmation() - subject_conf.method = saml.SCM_BEARER - subject_conf.subject_confirmation_data = subject_conf_data - subject = saml.Subject() - subject.subject_confirmation = subject_conf - subject.name_id = name_id - return subject - - def _create_attribute_statement(self, user, user_domain_name, roles, - project, project_domain_name): - """Create an object that represents a SAML AttributeStatement. - - <ns0:AttributeStatement> - <ns0:Attribute Name="openstack_user"> - <ns0:AttributeValue - xsi:type="xs:string">test_user</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_user_domain"> - <ns0:AttributeValue - xsi:type="xs:string">Default</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_roles"> - <ns0:AttributeValue - xsi:type="xs:string">admin</ns0:AttributeValue> - <ns0:AttributeValue - xsi:type="xs:string">member</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_project"> - <ns0:AttributeValue - xsi:type="xs:string">development</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_project_domain"> - <ns0:AttributeValue - xsi:type="xs:string">Default</ns0:AttributeValue> - </ns0:Attribute> - </ns0:AttributeStatement> - - :return: XML <AttributeStatement> object - - """ - - def _build_attribute(attribute_name, attribute_values): - attribute = saml.Attribute() - attribute.name = attribute_name - - for value in attribute_values: - attribute_value = saml.AttributeValue() - attribute_value.set_text(value) - attribute.attribute_value.append(attribute_value) - - return attribute - - user_attribute = _build_attribute('openstack_user', [user]) - roles_attribute = _build_attribute('openstack_roles', roles) - project_attribute = _build_attribute('openstack_project', [project]) - project_domain_attribute = _build_attribute( - 'openstack_project_domain', [project_domain_name]) - user_domain_attribute = _build_attribute( - 'openstack_user_domain', [user_domain_name]) - - attribute_statement = saml.AttributeStatement() - attribute_statement.attribute.append(user_attribute) - attribute_statement.attribute.append(roles_attribute) - attribute_statement.attribute.append(project_attribute) - attribute_statement.attribute.append(project_domain_attribute) - attribute_statement.attribute.append(user_domain_attribute) - return attribute_statement - - def _create_authn_statement(self, issuer, expiration_time): - """Create an object that represents a SAML AuthnStatement. - - <ns0:AuthnStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - AuthnInstant="2014-07-30T03:04:25Z" SessionIndex="47335964efb" - SessionNotOnOrAfter="2014-07-30T03:04:26Z"> - <ns0:AuthnContext> - <ns0:AuthnContextClassRef> - urn:oasis:names:tc:SAML:2.0:ac:classes:Password - </ns0:AuthnContextClassRef> - <ns0:AuthenticatingAuthority> - https://acme.com/FIM/sps/openstack/saml20 - </ns0:AuthenticatingAuthority> - </ns0:AuthnContext> - </ns0:AuthnStatement> - - :return: XML <AuthnStatement> object - - """ - authn_statement = saml.AuthnStatement() - authn_statement.authn_instant = utils.isotime() - authn_statement.session_index = uuid.uuid4().hex - authn_statement.session_not_on_or_after = expiration_time - - authn_context = saml.AuthnContext() - authn_context_class = saml.AuthnContextClassRef() - authn_context_class.set_text(saml.AUTHN_PASSWORD) - - authn_authority = saml.AuthenticatingAuthority() - authn_authority.set_text(issuer) - authn_context.authn_context_class_ref = authn_context_class - authn_context.authenticating_authority = authn_authority - - authn_statement.authn_context = authn_context - - return authn_statement - - def _create_assertion(self, issuer, signature, subject, authn_statement, - attribute_statement): - """Create an object that represents a SAML Assertion. - - <ns0:Assertion - ID="35daed258ba647ba8962e9baff4d6a46" - IssueInstant="2014-06-11T15:45:58Z" - Version="2.0"> - <ns0:Issuer> ... </ns0:Issuer> - <ns1:Signature> ... </ns1:Signature> - <ns0:Subject> ... </ns0:Subject> - <ns0:AuthnStatement> ... </ns0:AuthnStatement> - <ns0:AttributeStatement> ... </ns0:AttributeStatement> - </ns0:Assertion> - - :return: XML <Assertion> object - - """ - assertion = saml.Assertion() - assertion.id = self.assertion_id - assertion.issue_instant = utils.isotime() - assertion.version = '2.0' - assertion.issuer = issuer - assertion.signature = signature - assertion.subject = subject - assertion.authn_statement = authn_statement - assertion.attribute_statement = attribute_statement - return assertion - - def _create_response(self, issuer, status, assertion, recipient): - """Create an object that represents a SAML Response. - - <ns0:Response - Destination="http://beta.com/Shibboleth.sso/SAML2/POST" - ID="c5954543230e4e778bc5b92923a0512d" - IssueInstant="2014-07-30T03:19:45Z" - Version="2.0" /> - <ns0:Issuer> ... </ns0:Issuer> - <ns0:Assertion> ... </ns0:Assertion> - <ns0:Status> ... </ns0:Status> - </ns0:Response> - - :return: XML <Response> object - - """ - response = samlp.Response() - response.id = uuid.uuid4().hex - response.destination = recipient - response.issue_instant = utils.isotime() - response.version = '2.0' - response.issuer = issuer - response.status = status - response.assertion = assertion - return response - - def _create_signature(self): - """Create an object that represents a SAML <Signature>. - - This must be filled with algorithms that the signing binary will apply - in order to sign the whole message. - Currently we enforce X509 signing. - Example of the template:: - - <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> - <SignedInfo> - <CanonicalizationMethod - Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> - <SignatureMethod - Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> - <Reference URI="#<Assertion ID>"> - <Transforms> - <Transform - Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> - <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> - </Transforms> - <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> - <DigestValue /> - </Reference> - </SignedInfo> - <SignatureValue /> - <KeyInfo> - <X509Data /> - </KeyInfo> - </Signature> - - :return: XML <Signature> object - - """ - canonicalization_method = xmldsig.CanonicalizationMethod() - canonicalization_method.algorithm = xmldsig.ALG_EXC_C14N - signature_method = xmldsig.SignatureMethod( - algorithm=xmldsig.SIG_RSA_SHA1) - - transforms = xmldsig.Transforms() - envelope_transform = xmldsig.Transform( - algorithm=xmldsig.TRANSFORM_ENVELOPED) - - c14_transform = xmldsig.Transform(algorithm=xmldsig.ALG_EXC_C14N) - transforms.transform = [envelope_transform, c14_transform] - - digest_method = xmldsig.DigestMethod(algorithm=xmldsig.DIGEST_SHA1) - digest_value = xmldsig.DigestValue() - - reference = xmldsig.Reference() - reference.uri = '#' + self.assertion_id - reference.digest_method = digest_method - reference.digest_value = digest_value - reference.transforms = transforms - - signed_info = xmldsig.SignedInfo() - signed_info.canonicalization_method = canonicalization_method - signed_info.signature_method = signature_method - signed_info.reference = reference - - key_info = xmldsig.KeyInfo() - key_info.x509_data = xmldsig.X509Data() - - signature = xmldsig.Signature() - signature.signed_info = signed_info - signature.signature_value = xmldsig.SignatureValue() - signature.key_info = key_info - - return signature - - -def _sign_assertion(assertion): - """Sign a SAML assertion. - - This method utilizes ``xmlsec1`` binary and signs SAML assertions in a - separate process. ``xmlsec1`` cannot read input data from stdin so the - prepared assertion needs to be serialized and stored in a temporary - file. This file will be deleted immediately after ``xmlsec1`` returns. - The signed assertion is redirected to a standard output and read using - subprocess.PIPE redirection. A ``saml.Assertion`` class is created - from the signed string again and returned. - - Parameters that are required in the CONF:: - * xmlsec_binary - * private key file path - * public key file path - :return: XML <Assertion> object - - """ - xmlsec_binary = CONF.saml.xmlsec1_binary - idp_private_key = CONF.saml.keyfile - idp_public_key = CONF.saml.certfile - - # xmlsec1 --sign --privkey-pem privkey,cert --id-attr:ID <tag> <file> - certificates = '%(idp_private_key)s,%(idp_public_key)s' % { - 'idp_public_key': idp_public_key, - 'idp_private_key': idp_private_key - } - - command_list = [xmlsec_binary, '--sign', '--privkey-pem', certificates, - '--id-attr:ID', 'Assertion'] - - file_path = None - try: - # NOTE(gyee): need to make the namespace prefixes explicit so - # they won't get reassigned when we wrap the assertion into - # SAML2 response - file_path = fileutils.write_to_tempfile(assertion.to_string( - nspair={'saml': saml2.NAMESPACE, - 'xmldsig': xmldsig.NAMESPACE})) - command_list.append(file_path) - stdout = subprocess.check_output(command_list, - stderr=subprocess.STDOUT) - except Exception as e: - msg = _LE('Error when signing assertion, reason: %(reason)s%(output)s') - LOG.error(msg, - {'reason': e, - 'output': ' ' + e.output if hasattr(e, 'output') else ''}) - raise exception.SAMLSigningError(reason=e) - finally: - try: - if file_path: - os.remove(file_path) - except OSError: - pass - - return saml2.create_class_from_xml_string(saml.Assertion, stdout) - - -class MetadataGenerator(object): - """A class for generating SAML IdP Metadata.""" - - def generate_metadata(self): - """Generate Identity Provider Metadata. - - Generate and format metadata into XML that can be exposed and - consumed by a federated Service Provider. - - :return: XML <EntityDescriptor> object. - :raises: keystone.exception.ValidationError: Raises if the required - config options aren't set. - - """ - self._ensure_required_values_present() - entity_descriptor = self._create_entity_descriptor() - entity_descriptor.idpsso_descriptor = ( - self._create_idp_sso_descriptor()) - return entity_descriptor - - def _create_entity_descriptor(self): - ed = md.EntityDescriptor() - ed.entity_id = CONF.saml.idp_entity_id - return ed - - def _create_idp_sso_descriptor(self): - - def get_cert(): - try: - return sigver.read_cert_from_file(CONF.saml.certfile, 'pem') - except (IOError, sigver.CertificateError) as e: - msg = _('Cannot open certificate %(cert_file)s. ' - 'Reason: %(reason)s') - msg = msg % {'cert_file': CONF.saml.certfile, 'reason': e} - LOG.error(msg) - raise IOError(msg) - - def key_descriptor(): - cert = get_cert() - return md.KeyDescriptor( - key_info=xmldsig.KeyInfo( - x509_data=xmldsig.X509Data( - x509_certificate=xmldsig.X509Certificate(text=cert) - ) - ), use='signing' - ) - - def single_sign_on_service(): - idp_sso_endpoint = CONF.saml.idp_sso_endpoint - return md.SingleSignOnService( - binding=saml2.BINDING_URI, - location=idp_sso_endpoint) - - def organization(): - name = md.OrganizationName(lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_name) - display_name = md.OrganizationDisplayName( - lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_display_name) - url = md.OrganizationURL(lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_url) - - return md.Organization( - organization_display_name=display_name, - organization_url=url, organization_name=name) - - def contact_person(): - company = md.Company(text=CONF.saml.idp_contact_company) - given_name = md.GivenName(text=CONF.saml.idp_contact_name) - surname = md.SurName(text=CONF.saml.idp_contact_surname) - email = md.EmailAddress(text=CONF.saml.idp_contact_email) - telephone = md.TelephoneNumber( - text=CONF.saml.idp_contact_telephone) - contact_type = CONF.saml.idp_contact_type - - return md.ContactPerson( - company=company, given_name=given_name, sur_name=surname, - email_address=email, telephone_number=telephone, - contact_type=contact_type) - - def name_id_format(): - return md.NameIDFormat(text=saml.NAMEID_FORMAT_TRANSIENT) - - idpsso = md.IDPSSODescriptor() - idpsso.protocol_support_enumeration = samlp.NAMESPACE - idpsso.key_descriptor = key_descriptor() - idpsso.single_sign_on_service = single_sign_on_service() - idpsso.name_id_format = name_id_format() - if self._check_organization_values(): - idpsso.organization = organization() - if self._check_contact_person_values(): - idpsso.contact_person = contact_person() - return idpsso - - def _ensure_required_values_present(self): - """Ensure idp_sso_endpoint and idp_entity_id have values.""" - - if CONF.saml.idp_entity_id is None: - msg = _('Ensure configuration option idp_entity_id is set.') - raise exception.ValidationError(msg) - if CONF.saml.idp_sso_endpoint is None: - msg = _('Ensure configuration option idp_sso_endpoint is set.') - raise exception.ValidationError(msg) - - def _check_contact_person_values(self): - """Determine if contact information is included in metadata.""" - - # Check if we should include contact information - params = [CONF.saml.idp_contact_company, - CONF.saml.idp_contact_name, - CONF.saml.idp_contact_surname, - CONF.saml.idp_contact_email, - CONF.saml.idp_contact_telephone] - for value in params: - if value is None: - return False - - # Check if contact type is an invalid value - valid_type_values = ['technical', 'other', 'support', 'administrative', - 'billing'] - if CONF.saml.idp_contact_type not in valid_type_values: - msg = _('idp_contact_type must be one of: [technical, other, ' - 'support, administrative or billing.') - raise exception.ValidationError(msg) - return True - - def _check_organization_values(self): - """Determine if organization information is included in metadata.""" - - params = [CONF.saml.idp_organization_name, - CONF.saml.idp_organization_display_name, - CONF.saml.idp_organization_url] - for value in params: - if value is None: - return False - return True - - -class ECPGenerator(object): - """A class for generating an ECP assertion.""" - - @staticmethod - def generate_ecp(saml_assertion, relay_state_prefix): - ecp_generator = ECPGenerator() - header = ecp_generator._create_header(relay_state_prefix) - body = ecp_generator._create_body(saml_assertion) - envelope = soapenv.Envelope(header=header, body=body) - return envelope - - def _create_header(self, relay_state_prefix): - relay_state_text = relay_state_prefix + uuid.uuid4().hex - relay_state = ecp.RelayState(actor=client_base.ACTOR, - must_understand='1', - text=relay_state_text) - header = soapenv.Header() - header.extension_elements = ( - [saml2.element_to_extension_element(relay_state)]) - return header - - def _create_body(self, saml_assertion): - body = soapenv.Body() - body.extension_elements = ( - [saml2.element_to_extension_element(saml_assertion)]) - return body diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg deleted file mode 100644 index 464ab62b..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg +++ /dev/null @@ -1,25 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id=federation - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table=migrate_version - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs=[] - -# When creating new change scripts, Migrate will stamp the new script with -# a version number. By default this is latest_version + 1. You can set this -# to 'true' to tell Migrate to use the UTC timestamp instead. -use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py deleted file mode 100644 index 8ce8c6fa..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2014 Mirantis.inc -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from keystone import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py deleted file mode 100644 index d9b24a00..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 import exception - - -def upgrade(migrate_engine): - raise exception.MigrationMovedFailure(extension='federation') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/contrib/federation/routers.py b/keystone-moon/keystone/contrib/federation/routers.py deleted file mode 100644 index d5857ca6..00000000 --- a/keystone-moon/keystone/contrib/federation/routers.py +++ /dev/null @@ -1,31 +0,0 @@ -# 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_log import log -from oslo_log import versionutils - -from keystone.common import wsgi -from keystone.i18n import _ - - -LOG = log.getLogger(__name__) - - -class FederationExtension(wsgi.Middleware): - - def __init__(self, *args, **kwargs): - super(FederationExtension, self).__init__(*args, **kwargs) - msg = _("Remove federation_extension from the paste pipeline, the " - "federation extension is now always available. Update the " - "[pipeline:api_v3] section in keystone-paste.ini accordingly, " - "as it will be removed in the O release.") - versionutils.report_deprecated_feature(LOG, msg) diff --git a/keystone-moon/keystone/contrib/federation/schema.py b/keystone-moon/keystone/contrib/federation/schema.py deleted file mode 100644 index 17818a98..00000000 --- a/keystone-moon/keystone/contrib/federation/schema.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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 validation -from keystone.common.validation import parameter_types - - -basic_property_id = { - 'type': 'object', - 'properties': { - 'id': { - 'type': 'string' - } - }, - 'required': ['id'], - 'additionalProperties': False -} - -saml_create = { - 'type': 'object', - 'properties': { - 'identity': { - 'type': 'object', - 'properties': { - 'token': basic_property_id, - 'methods': { - 'type': 'array' - } - }, - 'required': ['token'], - 'additionalProperties': False - }, - 'scope': { - 'type': 'object', - 'properties': { - 'service_provider': basic_property_id - }, - 'required': ['service_provider'], - 'additionalProperties': False - }, - }, - 'required': ['identity', 'scope'], - 'additionalProperties': False -} - -_service_provider_properties = { - # NOTE(rodrigods): The database accepts URLs with 256 as max length, - # but parameter_types.url uses 225 as max length. - 'auth_url': parameter_types.url, - 'sp_url': parameter_types.url, - 'description': validation.nullable(parameter_types.description), - 'enabled': parameter_types.boolean, - 'relay_state_prefix': validation.nullable(parameter_types.description) -} - -service_provider_create = { - 'type': 'object', - 'properties': _service_provider_properties, - # NOTE(rodrigods): 'id' is not required since it is passed in the URL - 'required': ['auth_url', 'sp_url'], - 'additionalProperties': False -} - -service_provider_update = { - 'type': 'object', - 'properties': _service_provider_properties, - # Make sure at least one property is being updated - 'minProperties': 1, - 'additionalProperties': False -} diff --git a/keystone-moon/keystone/contrib/federation/utils.py b/keystone-moon/keystone/contrib/federation/utils.py deleted file mode 100644 index bde19cfd..00000000 --- a/keystone-moon/keystone/contrib/federation/utils.py +++ /dev/null @@ -1,776 +0,0 @@ -# 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. - -"""Utilities for Federation Extension.""" - -import ast -import re - -import jsonschema -from oslo_config import cfg -from oslo_log import log -from oslo_utils import timeutils -import six - -from keystone import exception -from keystone.i18n import _, _LW - - -CONF = cfg.CONF -LOG = log.getLogger(__name__) - - -MAPPING_SCHEMA = { - "type": "object", - "required": ['rules'], - "properties": { - "rules": { - "minItems": 1, - "type": "array", - "items": { - "type": "object", - "required": ['local', 'remote'], - "additionalProperties": False, - "properties": { - "local": { - "type": "array" - }, - "remote": { - "minItems": 1, - "type": "array", - "items": { - "type": "object", - "oneOf": [ - {"$ref": "#/definitions/empty"}, - {"$ref": "#/definitions/any_one_of"}, - {"$ref": "#/definitions/not_any_of"}, - {"$ref": "#/definitions/blacklist"}, - {"$ref": "#/definitions/whitelist"} - ], - } - } - } - } - } - }, - "definitions": { - "empty": { - "type": "object", - "required": ['type'], - "properties": { - "type": { - "type": "string" - }, - }, - "additionalProperties": False, - }, - "any_one_of": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'any_one_of'], - "properties": { - "type": { - "type": "string" - }, - "any_one_of": { - "type": "array" - }, - "regex": { - "type": "boolean" - } - } - }, - "not_any_of": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'not_any_of'], - "properties": { - "type": { - "type": "string" - }, - "not_any_of": { - "type": "array" - }, - "regex": { - "type": "boolean" - } - } - }, - "blacklist": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'blacklist'], - "properties": { - "type": { - "type": "string" - }, - "blacklist": { - "type": "array" - } - } - }, - "whitelist": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'whitelist'], - "properties": { - "type": { - "type": "string" - }, - "whitelist": { - "type": "array" - } - } - } - } -} - - -class DirectMaps(object): - """An abstraction around the remote matches. - - Each match is treated internally as a list. - """ - - def __init__(self): - self._matches = [] - - def add(self, values): - """Adds a matched value to the list of matches. - - :param list value: the match to save - - """ - self._matches.append(values) - - def __getitem__(self, idx): - """Used by Python when executing ``''.format(*DirectMaps())``.""" - value = self._matches[idx] - if isinstance(value, list) and len(value) == 1: - return value[0] - else: - return value - - -def validate_mapping_structure(ref): - v = jsonschema.Draft4Validator(MAPPING_SCHEMA) - - messages = '' - for error in sorted(v.iter_errors(ref), key=str): - messages = messages + error.message + "\n" - - if messages: - raise exception.ValidationError(messages) - - -def validate_expiration(token_ref): - if timeutils.utcnow() > token_ref.expires: - raise exception.Unauthorized(_('Federation token is expired')) - - -def validate_groups_cardinality(group_ids, mapping_id): - """Check if groups list is non-empty. - - :param group_ids: list of group ids - :type group_ids: list of str - - :raises exception.MissingGroups: if ``group_ids`` cardinality is 0 - - """ - if not group_ids: - raise exception.MissingGroups(mapping_id=mapping_id) - - -def get_remote_id_parameter(protocol): - # NOTE(marco-fargetta): Since we support any protocol ID, we attempt to - # retrieve the remote_id_attribute of the protocol ID. If it's not - # registered in the config, then register the option and try again. - # This allows the user to register protocols other than oidc and saml2. - remote_id_parameter = None - try: - remote_id_parameter = CONF[protocol]['remote_id_attribute'] - except AttributeError: - CONF.register_opt(cfg.StrOpt('remote_id_attribute'), - group=protocol) - try: - remote_id_parameter = CONF[protocol]['remote_id_attribute'] - except AttributeError: - pass - if not remote_id_parameter: - LOG.debug('Cannot find "remote_id_attribute" in configuration ' - 'group %s. Trying default location in ' - 'group federation.', protocol) - remote_id_parameter = CONF.federation.remote_id_attribute - - return remote_id_parameter - - -def validate_idp(idp, protocol, assertion): - """Validate the IdP providing the assertion is registered for the mapping. - """ - - remote_id_parameter = get_remote_id_parameter(protocol) - if not remote_id_parameter or not idp['remote_ids']: - LOG.debug('Impossible to identify the IdP %s ', idp['id']) - # If nothing is defined, the administrator may want to - # allow the mapping of every IdP - return - try: - idp_remote_identifier = assertion[remote_id_parameter] - except KeyError: - msg = _('Could not find Identity Provider identifier in ' - 'environment') - raise exception.ValidationError(msg) - if idp_remote_identifier not in idp['remote_ids']: - msg = _('Incoming identity provider identifier not included ' - 'among the accepted identifiers.') - raise exception.Forbidden(msg) - - -def validate_groups_in_backend(group_ids, mapping_id, identity_api): - """Iterate over group ids and make sure they are present in the backend/ - - This call is not transactional. - :param group_ids: IDs of the groups to be checked - :type group_ids: list of str - - :param mapping_id: id of the mapping used for this operation - :type mapping_id: str - - :param identity_api: Identity Manager object used for communication with - backend - :type identity_api: identity.Manager - - :raises: exception.MappedGroupNotFound - - """ - for group_id in group_ids: - try: - identity_api.get_group(group_id) - except exception.GroupNotFound: - raise exception.MappedGroupNotFound( - group_id=group_id, mapping_id=mapping_id) - - -def validate_groups(group_ids, mapping_id, identity_api): - """Check group ids cardinality and check their existence in the backend. - - This call is not transactional. - :param group_ids: IDs of the groups to be checked - :type group_ids: list of str - - :param mapping_id: id of the mapping used for this operation - :type mapping_id: str - - :param identity_api: Identity Manager object used for communication with - backend - :type identity_api: identity.Manager - - :raises: exception.MappedGroupNotFound - :raises: exception.MissingGroups - - """ - validate_groups_cardinality(group_ids, mapping_id) - validate_groups_in_backend(group_ids, mapping_id, identity_api) - - -# TODO(marek-denis): Optimize this function, so the number of calls to the -# backend are minimized. -def transform_to_group_ids(group_names, mapping_id, - identity_api, resource_api): - """Transform groups identitified by name/domain to their ids - - Function accepts list of groups identified by a name and domain giving - a list of group ids in return. - - Example of group_names parameter:: - - [ - { - "name": "group_name", - "domain": { - "id": "domain_id" - }, - }, - { - "name": "group_name_2", - "domain": { - "name": "domain_name" - } - } - ] - - :param group_names: list of group identified by name and its domain. - :type group_names: list - - :param mapping_id: id of the mapping used for mapping assertion into - local credentials - :type mapping_id: str - - :param identity_api: identity_api object - :param resource_api: resource manager object - - :returns: generator object with group ids - - :raises: excepton.MappedGroupNotFound: in case asked group doesn't - exist in the backend. - - """ - - def resolve_domain(domain): - """Return domain id. - - Input is a dictionary with a domain identified either by a ``id`` or a - ``name``. In the latter case system will attempt to fetch domain object - from the backend. - - :returns: domain's id - :rtype: str - - """ - domain_id = (domain.get('id') or - resource_api.get_domain_by_name( - domain.get('name')).get('id')) - return domain_id - - for group in group_names: - try: - group_dict = identity_api.get_group_by_name( - group['name'], resolve_domain(group['domain'])) - yield group_dict['id'] - except exception.GroupNotFound: - LOG.debug('Skip mapping group %s; has no entry in the backend', - group['name']) - - -def get_assertion_params_from_env(context): - LOG.debug('Environment variables: %s', context['environment']) - prefix = CONF.federation.assertion_prefix - for k, v in list(context['environment'].items()): - if k.startswith(prefix): - yield (k, v) - - -class UserType(object): - """User mapping type.""" - EPHEMERAL = 'ephemeral' - LOCAL = 'local' - - -class RuleProcessor(object): - """A class to process assertions and mapping rules.""" - - class _EvalType(object): - """Mapping rule evaluation types.""" - ANY_ONE_OF = 'any_one_of' - NOT_ANY_OF = 'not_any_of' - BLACKLIST = 'blacklist' - WHITELIST = 'whitelist' - - def __init__(self, rules): - """Initialize RuleProcessor. - - Example rules can be found at: - :class:`keystone.tests.mapping_fixtures` - - :param rules: rules from a mapping - :type rules: dict - - """ - - self.rules = rules - - def process(self, assertion_data): - """Transform assertion to a dictionary of user name and group ids - based on mapping rules. - - This function will iterate through the mapping rules to find - assertions that are valid. - - :param assertion_data: an assertion containing values from an IdP - :type assertion_data: dict - - Example assertion_data:: - - { - 'Email': 'testacct@example.com', - 'UserName': 'testacct', - 'FirstName': 'Test', - 'LastName': 'Account', - 'orgPersonType': 'Tester' - } - - :returns: dictionary with user and group_ids - - The expected return structure is:: - - { - 'name': 'foobar', - 'group_ids': ['abc123', 'def456'], - 'group_names': [ - { - 'name': 'group_name_1', - 'domain': { - 'name': 'domain1' - } - }, - { - 'name': 'group_name_1_1', - 'domain': { - 'name': 'domain1' - } - }, - { - 'name': 'group_name_2', - 'domain': { - 'id': 'xyz132' - } - } - ] - } - - """ - - # Assertions will come in as string key-value pairs, and will use a - # semi-colon to indicate multiple values, i.e. groups. - # This will create a new dictionary where the values are arrays, and - # any multiple values are stored in the arrays. - LOG.debug('assertion data: %s', assertion_data) - assertion = {n: v.split(';') for n, v in assertion_data.items() - if isinstance(v, six.string_types)} - LOG.debug('assertion: %s', assertion) - identity_values = [] - - LOG.debug('rules: %s', self.rules) - for rule in self.rules: - direct_maps = self._verify_all_requirements(rule['remote'], - assertion) - - # If the compare comes back as None, then the rule did not apply - # to the assertion data, go on to the next rule - if direct_maps is None: - continue - - # If there are no direct mappings, then add the local mapping - # directly to the array of saved values. However, if there is - # a direct mapping, then perform variable replacement. - if not direct_maps: - identity_values += rule['local'] - else: - for local in rule['local']: - new_local = self._update_local_mapping(local, direct_maps) - identity_values.append(new_local) - - LOG.debug('identity_values: %s', identity_values) - mapped_properties = self._transform(identity_values) - LOG.debug('mapped_properties: %s', mapped_properties) - return mapped_properties - - def _transform(self, identity_values): - """Transform local mappings, to an easier to understand format. - - Transform the incoming array to generate the return value for - the process function. Generating content for Keystone tokens will - be easier if some pre-processing is done at this level. - - :param identity_values: local mapping from valid evaluations - :type identity_values: array of dict - - Example identity_values:: - - [ - { - 'group': {'id': '0cd5e9'}, - 'user': { - 'email': 'bob@example.com' - }, - }, - { - 'groups': ['member', 'admin', tester'], - 'domain': { - 'name': 'default_domain' - } - } - ] - - :returns: dictionary with user name, group_ids and group_names. - :rtype: dict - - """ - - def extract_groups(groups_by_domain): - for groups in list(groups_by_domain.values()): - for group in list({g['name']: g for g in groups}.values()): - yield group - - def normalize_user(user): - """Parse and validate user mapping.""" - - user_type = user.get('type') - - if user_type and user_type not in (UserType.EPHEMERAL, - UserType.LOCAL): - msg = _("User type %s not supported") % user_type - raise exception.ValidationError(msg) - - if user_type is None: - user_type = user['type'] = UserType.EPHEMERAL - - if user_type == UserType.EPHEMERAL: - user['domain'] = { - 'id': CONF.federation.federated_domain_name - } - - # initialize the group_ids as a set to eliminate duplicates - user = {} - group_ids = set() - group_names = list() - groups_by_domain = dict() - - for identity_value in identity_values: - if 'user' in identity_value: - # if a mapping outputs more than one user name, log it - if user: - LOG.warning(_LW('Ignoring user name')) - else: - user = identity_value.get('user') - if 'group' in identity_value: - group = identity_value['group'] - if 'id' in group: - group_ids.add(group['id']) - elif 'name' in group: - domain = (group['domain'].get('name') or - group['domain'].get('id')) - groups_by_domain.setdefault(domain, list()).append(group) - group_names.extend(extract_groups(groups_by_domain)) - if 'groups' in identity_value: - if 'domain' not in identity_value: - msg = _("Invalid rule: %(identity_value)s. Both 'groups' " - "and 'domain' keywords must be specified.") - msg = msg % {'identity_value': identity_value} - raise exception.ValidationError(msg) - # In this case, identity_value['groups'] is a string - # representation of a list, and we want a real list. This is - # due to the way we do direct mapping substitutions today (see - # function _update_local_mapping() ) - try: - group_names_list = ast.literal_eval( - identity_value['groups']) - except ValueError: - group_names_list = [identity_value['groups']] - domain = identity_value['domain'] - group_dicts = [{'name': name, 'domain': domain} for name in - group_names_list] - - group_names.extend(group_dicts) - - normalize_user(user) - - return {'user': user, - 'group_ids': list(group_ids), - 'group_names': group_names} - - def _update_local_mapping(self, local, direct_maps): - """Replace any {0}, {1} ... values with data from the assertion. - - :param local: local mapping reference that needs to be updated - :type local: dict - :param direct_maps: identity values used to update local - :type direct_maps: keystone.contrib.federation.utils.DirectMaps - - Example local:: - - {'user': {'name': '{0} {1}', 'email': '{2}'}} - - Example direct_maps:: - - ['Bob', 'Thompson', 'bob@example.com'] - - :returns: new local mapping reference with replaced values. - - The expected return structure is:: - - {'user': {'name': 'Bob Thompson', 'email': 'bob@example.org'}} - - """ - - LOG.debug('direct_maps: %s', direct_maps) - LOG.debug('local: %s', local) - new = {} - for k, v in local.items(): - if isinstance(v, dict): - new_value = self._update_local_mapping(v, direct_maps) - else: - new_value = v.format(*direct_maps) - new[k] = new_value - return new - - def _verify_all_requirements(self, requirements, assertion): - """Go through the remote requirements of a rule, and compare against - the assertion. - - If a value of ``None`` is returned, the rule with this assertion - doesn't apply. - If an array of zero length is returned, then there are no direct - mappings to be performed, but the rule is valid. - Otherwise, then it will first attempt to filter the values according - to blacklist or whitelist rules and finally return the values in - order, to be directly mapped. - - :param requirements: list of remote requirements from rules - :type requirements: list - - Example requirements:: - - [ - { - "type": "UserName" - }, - { - "type": "orgPersonType", - "any_one_of": [ - "Customer" - ] - }, - { - "type": "ADFS_GROUPS", - "whitelist": [ - "g1", "g2", "g3", "g4" - ] - } - ] - - :param assertion: dict of attributes from an IdP - :type assertion: dict - - Example assertion:: - - { - 'UserName': ['testacct'], - 'LastName': ['Account'], - 'orgPersonType': ['Tester'], - 'Email': ['testacct@example.com'], - 'FirstName': ['Test'], - 'ADFS_GROUPS': ['g1', 'g2'] - } - - :returns: identity values used to update local - :rtype: keystone.contrib.federation.utils.DirectMaps or None - - """ - - direct_maps = DirectMaps() - - for requirement in requirements: - requirement_type = requirement['type'] - direct_map_values = assertion.get(requirement_type) - regex = requirement.get('regex', False) - - if not direct_map_values: - return None - - any_one_values = requirement.get(self._EvalType.ANY_ONE_OF) - if any_one_values is not None: - if self._evaluate_requirement(any_one_values, - direct_map_values, - self._EvalType.ANY_ONE_OF, - regex): - continue - else: - return None - - not_any_values = requirement.get(self._EvalType.NOT_ANY_OF) - if not_any_values is not None: - if self._evaluate_requirement(not_any_values, - direct_map_values, - self._EvalType.NOT_ANY_OF, - regex): - continue - else: - return None - - # If 'any_one_of' or 'not_any_of' are not found, then values are - # within 'type'. Attempt to find that 'type' within the assertion, - # and filter these values if 'whitelist' or 'blacklist' is set. - blacklisted_values = requirement.get(self._EvalType.BLACKLIST) - whitelisted_values = requirement.get(self._EvalType.WHITELIST) - - # If a blacklist or whitelist is used, we want to map to the - # whole list instead of just its values separately. - if blacklisted_values is not None: - direct_map_values = [v for v in direct_map_values - if v not in blacklisted_values] - elif whitelisted_values is not None: - direct_map_values = [v for v in direct_map_values - if v in whitelisted_values] - - direct_maps.add(direct_map_values) - - LOG.debug('updating a direct mapping: %s', direct_map_values) - - return direct_maps - - def _evaluate_values_by_regex(self, values, assertion_values): - for value in values: - for assertion_value in assertion_values: - if re.search(value, assertion_value): - return True - return False - - def _evaluate_requirement(self, values, assertion_values, - eval_type, regex): - """Evaluate the incoming requirement and assertion. - - If the requirement type does not exist in the assertion data, then - return False. If regex is specified, then compare the values and - assertion values. Otherwise, grab the intersection of the values - and use that to compare against the evaluation type. - - :param values: list of allowed values, defined in the requirement - :type values: list - :param assertion_values: The values from the assertion to evaluate - :type assertion_values: list/string - :param eval_type: determine how to evaluate requirements - :type eval_type: string - :param regex: perform evaluation with regex - :type regex: boolean - - :returns: boolean, whether requirement is valid or not. - - """ - if regex: - any_match = self._evaluate_values_by_regex(values, - assertion_values) - else: - any_match = bool(set(values).intersection(set(assertion_values))) - if any_match and eval_type == self._EvalType.ANY_ONE_OF: - return True - if not any_match and eval_type == self._EvalType.NOT_ANY_OF: - return True - - return False - - -def assert_enabled_identity_provider(federation_api, idp_id): - identity_provider = federation_api.get_idp(idp_id) - if identity_provider.get('enabled') is not True: - msg = _('Identity Provider %(idp)s is disabled') % {'idp': idp_id} - LOG.debug(msg) - raise exception.Forbidden(msg) - - -def assert_enabled_service_provider_object(service_provider): - if service_provider.get('enabled') is not True: - sp_id = service_provider['id'] - msg = _('Service Provider %(sp)s is disabled') % {'sp': sp_id} - LOG.debug(msg) - raise exception.Forbidden(msg) |