aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/contrib/federation
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/contrib/federation')
-rw-r--r--keystone-moon/keystone/contrib/federation/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/federation/backends/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/federation/backends/sql.py29
-rw-r--r--keystone-moon/keystone/contrib/federation/constants.py15
-rw-r--r--keystone-moon/keystone/contrib/federation/controllers.py520
-rw-r--r--keystone-moon/keystone/contrib/federation/core.py355
-rw-r--r--keystone-moon/keystone/contrib/federation/idp.py609
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg25
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py20
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py17
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py0
-rw-r--r--keystone-moon/keystone/contrib/federation/routers.py31
-rw-r--r--keystone-moon/keystone/contrib/federation/schema.py79
-rw-r--r--keystone-moon/keystone/contrib/federation/utils.py776
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)