Source code for keystone.api.os_federation

#    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.

# This file handles all flask-restful resources for /v3/OS-FEDERATION

import flask
import flask_restful
import http.client
from oslo_serialization import jsonutils

from keystone.api._shared import authentication
from keystone.api._shared import json_home_relations
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import render_token
from keystone.common import validation
import keystone.conf
from keystone import exception
from keystone.federation import schema
from keystone.federation import utils
from keystone.server import flask as ks_flask


CONF = keystone.conf.CONF
ENFORCER = rbac_enforcer.RBACEnforcer
PROVIDERS = provider_api.ProviderAPIs


_build_param_relation = json_home_relations.os_federation_parameter_rel_func
_build_resource_relation = json_home_relations.os_federation_resource_rel_func

IDP_ID_PARAMETER_RELATION = _build_param_relation(parameter_name='idp_id')
PROTOCOL_ID_PARAMETER_RELATION = _build_param_relation(
    parameter_name='protocol_id')
SP_ID_PARAMETER_RELATION = _build_param_relation(parameter_name='sp_id')


def _combine_lists_uniquely(a, b):
    # it's most likely that only one of these will be filled so avoid
    # the combination if possible.
    if a and b:
        return {x['id']: x for x in a + b}.values()
    else:
        return a or b


class _ResourceBase(ks_flask.ResourceBase):
    json_home_resource_rel_func = _build_resource_relation
    json_home_parameter_rel_func = _build_param_relation

    @classmethod
    def wrap_member(cls, ref, collection_name=None, member_name=None):
        cls._add_self_referential_link(ref, collection_name)
        cls._add_related_links(ref)
        return {member_name or cls.member_key: cls.filter_params(ref)}

    @staticmethod
    def _add_related_links(ref):
        # Do Nothing, This is in support of child class mechanisms.
        pass


[docs]class IdentityProvidersResource(_ResourceBase): collection_key = 'identity_providers' member_key = 'identity_provider' api_prefix = '/OS-FEDERATION' _public_parameters = frozenset(['id', 'enabled', 'description', 'remote_ids', 'links', 'domain_id', 'authorization_ttl' ]) _id_path_param_name_override = 'idp_id' @staticmethod def _add_related_links(ref): """Add URLs for entities related with Identity Provider. Add URLs pointing to: - protocols tied to the Identity Provider """ base_path = ref['links'].get('self') if base_path is None: base_path = '/'.join(ks_flask.base_url(path='/%s' % ref['id'])) for name in ['protocols']: ref['links'][name] = '/'.join([base_path, name])
[docs] def get(self, idp_id=None): if idp_id is not None: return self._get_idp(idp_id) return self._list_idps()
def _get_idp(self, idp_id): """Get an IDP resource. GET/HEAD /OS-FEDERATION/identity_providers/{idp_id} """ ENFORCER.enforce_call(action='identity:get_identity_provider') ref = PROVIDERS.federation_api.get_idp(idp_id) return self.wrap_member(ref) def _list_idps(self): """List all identity providers. GET/HEAD /OS-FEDERATION/identity_providers """ filters = ['id', 'enabled'] ENFORCER.enforce_call(action='identity:list_identity_providers', filters=filters) hints = self.build_driver_hints(filters) refs = PROVIDERS.federation_api.list_idps(hints=hints) refs = [self.filter_params(r) for r in refs] collection = self.wrap_collection(refs, hints=hints) for r in collection[self.collection_key]: # Add the related links explicitly self._add_related_links(r) return collection
[docs] def put(self, idp_id): """Create an idp resource for federated authentication. PUT /OS-FEDERATION/identity_providers/{idp_id} """ ENFORCER.enforce_call(action='identity:create_identity_provider') idp = self.request_body_json.get('identity_provider', {}) validation.lazy_validate(schema.identity_provider_create, idp) idp = self._normalize_dict(idp) idp.setdefault('enabled', False) idp_ref = PROVIDERS.federation_api.create_idp( idp_id, idp) return self.wrap_member(idp_ref), http.client.CREATED
[docs] def patch(self, idp_id): ENFORCER.enforce_call(action='identity:update_identity_provider') idp = self.request_body_json.get('identity_provider', {}) validation.lazy_validate(schema.identity_provider_update, idp) idp = self._normalize_dict(idp) idp_ref = PROVIDERS.federation_api.update_idp( idp_id, idp) return self.wrap_member(idp_ref)
[docs] def delete(self, idp_id): ENFORCER.enforce_call(action='identity:delete_identity_provider') PROVIDERS.federation_api.delete_idp(idp_id) return None, http.client.NO_CONTENT
class _IdentityProvidersProtocolsResourceBase(_ResourceBase): collection_key = 'protocols' member_key = 'protocol' _public_parameters = frozenset(['id', 'mapping_id', 'links']) json_home_additional_parameters = { 'idp_id': IDP_ID_PARAMETER_RELATION} json_home_collection_resource_name_override = 'identity_provider_protocols' json_home_member_resource_name_override = 'identity_provider_protocol' @staticmethod def _add_related_links(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', {}) ref['links']['identity_provider'] = ks_flask.base_url( path=ref['idp_id'])
[docs]class IDPProtocolsListResource(_IdentityProvidersProtocolsResourceBase):
[docs] def get(self, idp_id): """List protocols for an IDP. HEAD/GET /OS-FEDERATION/identity_providers/{idp_id}/protocols """ ENFORCER.enforce_call(action='identity:list_protocols') protocol_refs = PROVIDERS.federation_api.list_protocols(idp_id) protocols = list(protocol_refs) collection = self.wrap_collection(protocols) for r in collection[self.collection_key]: # explicitly add related links self._add_related_links(r) return collection
[docs]class IDPProtocolsCRUDResource(_IdentityProvidersProtocolsResourceBase):
[docs] def get(self, idp_id, protocol_id): """Get protocols for an IDP. HEAD/GET /OS-FEDERATION/identity_providers/ {idp_id}/protocols/{protocol_id} """ ENFORCER.enforce_call(action='identity:get_protocol') ref = PROVIDERS.federation_api.get_protocol(idp_id, protocol_id) return self.wrap_member(ref)
[docs] def put(self, idp_id, protocol_id): """Create protocol for an IDP. PUT /OS-Federation/identity_providers/{idp_id}/protocols/{protocol_id} """ ENFORCER.enforce_call(action='identity:create_protocol') protocol = self.request_body_json.get('protocol', {}) validation.lazy_validate(schema.protocol_create, protocol) protocol = self._normalize_dict(protocol) ref = PROVIDERS.federation_api.create_protocol(idp_id, protocol_id, protocol) return self.wrap_member(ref), http.client.CREATED
[docs] def patch(self, idp_id, protocol_id): """Update protocol for an IDP. PATCH /OS-FEDERATION/identity_providers/ {idp_id}/protocols/{protocol_id} """ ENFORCER.enforce_call(action='identity:update_protocol') protocol = self.request_body_json.get('protocol', {}) validation.lazy_validate(schema.protocol_update, protocol) ref = PROVIDERS.federation_api.update_protocol(idp_id, protocol_id, protocol) return self.wrap_member(ref)
[docs] def delete(self, idp_id, protocol_id): """Delete a protocol from an IDP. DELETE /OS-FEDERATION/identity_providers/ {idp_id}/protocols/{protocol_id} """ ENFORCER.enforce_call(action='identity:delete_protocol') PROVIDERS.federation_api.delete_protocol(idp_id, protocol_id) return None, http.client.NO_CONTENT
[docs]class MappingResource(_ResourceBase): collection_key = 'mappings' member_key = 'mapping' api_prefix = '/OS-FEDERATION'
[docs] def get(self, mapping_id=None): if mapping_id is not None: return self._get_mapping(mapping_id) return self._list_mappings()
def _get_mapping(self, mapping_id): """Get a mapping. HEAD/GET /OS-FEDERATION/mappings/{mapping_id} """ ENFORCER.enforce_call(action='identity:get_mapping') return self.wrap_member(PROVIDERS.federation_api.get_mapping( mapping_id)) def _list_mappings(self): """List mappings. HEAD/GET /OS-FEDERATION/mappings """ ENFORCER.enforce_call(action='identity:list_mappings') return self.wrap_collection(PROVIDERS.federation_api.list_mappings())
[docs] def put(self, mapping_id): """Create a mapping. PUT /OS-FEDERATION/mappings/{mapping_id} """ ENFORCER.enforce_call(action='identity:create_mapping') mapping = self.request_body_json.get('mapping', {}) mapping = self._normalize_dict(mapping) utils.validate_mapping_structure(mapping) mapping_ref = PROVIDERS.federation_api.create_mapping( mapping_id, mapping) return self.wrap_member(mapping_ref), http.client.CREATED
[docs] def patch(self, mapping_id): """Update a mapping. PATCH /OS-FEDERATION/mappings/{mapping_id} """ ENFORCER.enforce_call(action='identity:update_mapping') mapping = self.request_body_json.get('mapping', {}) mapping = self._normalize_dict(mapping) utils.validate_mapping_structure(mapping) mapping_ref = PROVIDERS.federation_api.update_mapping( mapping_id, mapping) return self.wrap_member(mapping_ref)
[docs] def delete(self, mapping_id): """Delete a mapping. DELETE /OS-FEDERATION/mappings/{mapping_id} """ ENFORCER.enforce_call(action='identity:delete_mapping') PROVIDERS.federation_api.delete_mapping(mapping_id) return None, http.client.NO_CONTENT
[docs]class ServiceProvidersResource(_ResourceBase): collection_key = 'service_providers' member_key = 'service_provider' _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', 'links', 'relay_state_prefix', 'sp_url']) _id_path_param_name_override = 'sp_id' api_prefix = '/OS-FEDERATION'
[docs] def get(self, sp_id=None): if sp_id is not None: return self._get_service_provider(sp_id) return self._list_service_providers()
def _get_service_provider(self, sp_id): """Get a service provider. GET/HEAD /OS-FEDERATION/service_providers/{sp_id} """ ENFORCER.enforce_call(action='identity:get_service_provider') return self.wrap_member(PROVIDERS.federation_api.get_sp(sp_id)) def _list_service_providers(self): """List service providers. GET/HEAD /OS-FEDERATION/service_providers """ filters = ['id', 'enabled'] ENFORCER.enforce_call(action='identity:list_service_providers', filters=filters) hints = self.build_driver_hints(filters) refs = [self.filter_params(r) for r in PROVIDERS.federation_api.list_sps(hints=hints)] return self.wrap_collection(refs, hints=hints)
[docs] def put(self, sp_id): """Create a service provider. PUT /OS-FEDERATION/service_providers/{sp_id} """ ENFORCER.enforce_call(action='identity:create_service_provider') sp = self.request_body_json.get('service_provider', {}) validation.lazy_validate(schema.service_provider_create, sp) sp = self._normalize_dict(sp) sp.setdefault('enabled', False) sp.setdefault('relay_state_prefix', CONF.saml.relay_state_prefix) sp_ref = PROVIDERS.federation_api.create_sp(sp_id, sp) return self.wrap_member(sp_ref), http.client.CREATED
[docs] def patch(self, sp_id): """Update a service provider. PATCH /OS-FEDERATION/service_providers/{sp_id} """ ENFORCER.enforce_call(action='identity:update_service_provider') sp = self.request_body_json.get('service_provider', {}) validation.lazy_validate(schema.service_provider_update, sp) sp = self._normalize_dict(sp) sp_ref = PROVIDERS.federation_api.update_sp(sp_id, sp) return self.wrap_member(sp_ref)
[docs] def delete(self, sp_id): """Delete a service provider. DELETE /OS-FEDERATION/service_providers/{sp_id} """ ENFORCER.enforce_call(action='identity:delete_service_provider') PROVIDERS.federation_api.delete_sp(sp_id) return None, http.client.NO_CONTENT
[docs]class SAML2MetadataResource(flask_restful.Resource):
[docs] @ks_flask.unenforced_api def get(self): """Get SAML2 metadata. GET/HEAD /OS-FEDERATION/saml2/metadata """ 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) resp = flask.make_response(metadata, http.client.OK) resp.headers['Content-Type'] = 'text/xml' return resp
[docs]class OSFederationAuthResource(flask_restful.Resource):
[docs] @ks_flask.unenforced_api def get(self, idp_id, protocol_id): """Authenticate from dedicated uri endpoint. GET/HEAD /OS-FEDERATION/identity_providers/ {idp_id}/protocols/{protocol_id}/auth """ return self._auth(idp_id, protocol_id)
[docs] @ks_flask.unenforced_api def post(self, idp_id, protocol_id): """Authenticate from dedicated uri endpoint. POST /OS-FEDERATION/identity_providers/ {idp_id}/protocols/{protocol_id}/auth """ return self._auth(idp_id, protocol_id)
def _auth(self, idp_id, protocol_id): """Build and pass auth data to authentication code. 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 }, } } token = authentication.authenticate_for_token(auth) token_data = render_token.render_token_response_from_model(token) resp_data = jsonutils.dumps(token_data) flask_resp = flask.make_response(resp_data, http.client.CREATED) flask_resp.headers['X-Subject-Token'] = token.id flask_resp.headers['Content-Type'] = 'application/json' return flask_resp
[docs]class OSFederationAPI(ks_flask.APIBase): _name = 'OS-FEDERATION' _import_name = __name__ _api_url_prefix = '/OS-FEDERATION' resources = [] resource_mapping = [ ks_flask.construct_resource_map( resource=SAML2MetadataResource, url='/saml2/metadata', resource_kwargs={}, rel='metadata', resource_relation_func=_build_resource_relation), ks_flask.construct_resource_map( resource=OSFederationAuthResource, url=('/identity_providers/<string:idp_id>/protocols/' '<string:protocol_id>/auth'), resource_kwargs={}, rel='identity_provider_protocol_auth', resource_relation_func=_build_resource_relation, path_vars={ 'idp_id': IDP_ID_PARAMETER_RELATION, 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION}), ]
[docs]class OSFederationIdentityProvidersAPI(ks_flask.APIBase): _name = 'identity_providers' _import_name = __name__ _api_url_prefix = '/OS-FEDERATION' resources = [IdentityProvidersResource] resource_mapping = []
[docs]class OSFederationIdentityProvidersProtocolsAPI(ks_flask.APIBase): _name = 'protocols' _import_name = __name__ resources = [] resource_mapping = [ ks_flask.construct_resource_map( resource=IDPProtocolsCRUDResource, url=('/OS-FEDERATION/identity_providers/<string:idp_id>/protocols/' '<string:protocol_id>'), resource_kwargs={}, rel='identity_provider_protocol', resource_relation_func=_build_resource_relation, path_vars={ 'idp_id': IDP_ID_PARAMETER_RELATION, 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION } ), ks_flask.construct_resource_map( resource=IDPProtocolsListResource, url='/OS-FEDERATION/identity_providers/<string:idp_id>/protocols', resource_kwargs={}, rel='identity_provider_protocols', resource_relation_func=_build_resource_relation, path_vars={ 'idp_id': IDP_ID_PARAMETER_RELATION } ), ]
[docs]class OSFederationMappingsAPI(ks_flask.APIBase): _name = 'mappings' _import_name = __name__ _api_url_prefix = '/OS-FEDERATION' resources = [MappingResource] resource_mapping = []
[docs]class OSFederationServiceProvidersAPI(ks_flask.APIBase): _name = 'service_providers' _import_name = __name__ _api_url_prefix = '/OS-FEDERATION' resources = [ServiceProvidersResource] resource_mapping = []
APIs = ( OSFederationAPI, OSFederationIdentityProvidersAPI, OSFederationIdentityProvidersProtocolsAPI, OSFederationMappingsAPI, OSFederationServiceProvidersAPI )