aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/federation/controllers.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/federation/controllers.py')
-rw-r--r--keystone-moon/keystone/federation/controllers.py519
1 files changed, 519 insertions, 0 deletions
diff --git a/keystone-moon/keystone/federation/controllers.py b/keystone-moon/keystone/federation/controllers.py
new file mode 100644
index 00000000..b9e2d883
--- /dev/null
+++ b/keystone-moon/keystone/federation/controllers.py
@@ -0,0 +1,519 @@
+# 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 utils as k_utils
+from keystone.common import validation
+from keystone.common import wsgi
+from keystone import exception
+from keystone.federation import idp as keystone_idp
+from keystone.federation import schema
+from keystone.federation import utils
+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'
+
+ _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['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()
+ @validation.validated(schema.identity_provider_create, 'identity_provider')
+ def create_identity_provider(self, context, idp_id, identity_provider):
+ identity_provider = self._normalize_dict(identity_provider)
+ identity_provider.setdefault('enabled', False)
+ 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.filterprotected('id', 'enabled')
+ def list_identity_providers(self, context, filters):
+ hints = self.build_driver_hints(context, filters)
+ ref = self.federation_api.list_idps(hints=hints)
+ ref = [self.filter_params(x) for x in ref]
+ return IdentityProvider.wrap_collection(context, ref, hints=hints)
+
+ @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()
+ @validation.validated(schema.identity_provider_update, 'identity_provider')
+ def update_identity_provider(self, context, idp_id, identity_provider):
+ identity_provider = self._normalize_dict(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 keystone.common.controller.V3Controller docstring for explanation
+ on _public_parameters class attributes.
+
+ """
+
+ collection_name = 'protocols'
+ member_name = 'protocol'
+
+ _public_parameters = frozenset(['id', 'mapping_id', 'links'])
+
+ @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()
+ @validation.validated(schema.federation_protocol_schema, 'protocol')
+ def create_protocol(self, context, idp_id, protocol_id, protocol):
+ ref = self._normalize_dict(protocol)
+ 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()
+ @validation.validated(schema.federation_protocol_schema, 'protocol')
+ def update_protocol(self, context, idp_id, protocol_id, protocol):
+ ref = self._normalize_dict(protocol)
+ 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 keystone.exception.ValidationError: ``origin`` query parameter
+ was not specified. The URL is deemed invalid.
+ :raises keystone.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']['origin']
+ host = urllib.parse.unquote_plus(origin)
+ else:
+ msg = _('Request must have an origin query parameter')
+ LOG.error(msg)
+ raise exception.ValidationError(msg)
+
+ # change trusted_dashboard hostnames to lowercase before comparison
+ trusted_dashboards = [k_utils.lower_case_hostname(trusted)
+ for trusted in CONF.federation.trusted_dashboard]
+
+ if host not in trusted_dashboards:
+ 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, idp_id, protocol_id):
+ """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_id],
+ protocol_id: {
+ 'identity_provider': idp_id,
+ 'protocol': protocol_id
+ }
+ }
+ }
+
+ 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['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['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'
+
+ _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)
+ 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.filterprotected('id', 'enabled')
+ def list_service_providers(self, context, filters):
+ hints = self.build_driver_hints(context, filters)
+ ref = self.federation_api.list_sps(hints=hints)
+ ref = [self.filter_params(x) for x in ref]
+ return ServiceProvider.wrap_collection(context, ref, hints=hints)
+
+ @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)
+ 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')])