diff options
author | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
---|---|---|
committer | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
commit | b8c756ecdd7cced1db4300935484e8c83701c82e (patch) | |
tree | 87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/contrib/federation/controllers.py | |
parent | c304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff) |
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604
Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/contrib/federation/controllers.py')
-rw-r--r-- | keystone-moon/keystone/contrib/federation/controllers.py | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/keystone-moon/keystone/contrib/federation/controllers.py b/keystone-moon/keystone/contrib/federation/controllers.py new file mode 100644 index 00000000..6066a33f --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/controllers.py @@ -0,0 +1,457 @@ +# 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. + +"""Extensions supporting Federation.""" + +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_id']) + _public_parameters = frozenset(['id', 'enabled', 'description', + 'remote_id', '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 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 = CONF.federation.remote_id_attribute + remote_id = context['environment'][remote_id_name] + except KeyError: + msg = _('Missing entity ID from environment') + LOG.error(msg) + raise exception.Unauthorized(msg) + + 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 in CONF.federation.trusted_dashboard: + ref = self.federation_api.get_idp_from_remote_id(remote_id) + identity_provider = ref['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) + else: + msg = _('%(host)s is not a trusted dashboard host') + msg = msg % {'host': host} + LOG.error(msg) + raise exception.Unauthorized(msg) + + 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) + + @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 + """ + + 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') + auth_url = service_provider.get('auth_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) + subject = token_ref.user_name + roles = token_ref.role_names + + if not token_ref.project_scoped: + action = _('Use a project scoped token when attempting to create ' + 'a SAML assertion') + raise exception.ForbiddenAction(action=action) + + project = token_ref.project_name + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(issuer, sp_url, subject, roles, + project) + + return wsgi.render_response(body=response.to_string(), + status=('200', 'OK'), + headers=[('Content-Type', 'text/xml'), + ('X-sp-url', + six.binary_type(sp_url)), + ('X-auth-url', + six.binary_type(auth_url))]) + + +@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', + 'sp_url']) + _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', + 'links', '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) + 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')]) |