Convert OS-FEDERATION to flask native dispatching

Convert OS-FEDERATION to flask native dispatching.

NOTE: Two changes occured that impact testing in this patch.
      * The JSON Home test now uses assertDictEquals to make it
        easier to debug json_home document errors

      * It was by general good luck that the overloaded relation
        'identity_providers' worked as expected. The relation was
        used for both '/OS-FEDERATION/identity_providers' and
        the Identity-Provider-Specific WebSSO path. The change
        to the JSON Home document and the tests make the
        Identity-Provider-Specific WebSSO path now a relation
        of 'identity_providers_websso' to more closely align
        with 'websso' relation for
        '/auth/OS-FEDERATION/websso/{protocol_id}'. While
        this constitutes a minor break in our contract (the
        output of the json home document) it was required to
        ensure consistency and functionality. The alternative
        is to not represent '/OS-FEDERATION/identity_providers'
        (list endpoint) in the JSON Home document at all, instead
        represent only the WebSSO endpoint.

Change-Id: If746c14491322d4a5f88fa0cbb31105f6d38c240
Partial-Bug: #1776504
This commit is contained in:
Morgan Fainberg 2018-08-10 13:41:52 -07:00 committed by morgan fainberg
parent 294ca38554
commit 94f8f103ab
9 changed files with 696 additions and 514 deletions

View File

@ -15,6 +15,7 @@ from keystone.api import discovery
from keystone.api import endpoints
from keystone.api import limits
from keystone.api import os_ep_filter
from keystone.api import os_federation
from keystone.api import os_oauth1
from keystone.api import os_revoke
from keystone.api import os_simple_cert
@ -34,6 +35,7 @@ __all__ = (
'endpoints',
'limits',
'os_ep_filter',
'os_federation',
'os_oauth1',
'os_revoke',
'os_simple_cert',
@ -54,6 +56,7 @@ __apis__ = (
endpoints,
limits,
os_ep_filter,
os_federation,
os_oauth1,
os_revoke,
os_simple_cert,

View File

@ -57,3 +57,11 @@ os_trust_parameter_rel_func = functools.partial(
os_endpoint_policy_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-ENDPOINT-POLICY', extension_version='1.0')
# OS-FEDERATION "extension"
os_federation_resource_rel_func = functools.partial(
json_home.build_v3_extension_resource_relation,
extension_name='OS-FEDERATION', extension_version='1.0')
os_federation_parameter_rel_func = functools.partial(
json_home.build_v3_extension_parameter_relation,
extension_name='OS-FEDERATION', extension_version='1.0')

View File

@ -0,0 +1,597 @@
# 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
from oslo_log import versionutils
from six.moves import http_client
from keystone.api._shared import json_home_relations
from keystone.common import authorization
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import request
from keystone.common import validation
import keystone.conf
from keystone import exception
import keystone.federation.controllers
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: ref}
@staticmethod
def _add_related_links(ref):
# Do Nothing, This is in support of child class mechanisms.
pass
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'
])
_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])
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
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
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)
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 IdentityProvidersProtocolsResource(_ResourceBase):
collection_key = 'protocols'
member_key = 'protocol'
_public_parameters = frozenset(['id', 'mapping_id', 'links'])
api_prefix = '/OS-FEDERATION/identity_providers/<string:idp_id>'
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'])
def get(self, idp_id, protocol_id=None):
if protocol_id is not None:
return self._get_protocol(idp_id, protocol_id)
return self._list_protocols(idp_id)
def _get_protocol(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)
def _list_protocols(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
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
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)
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
class MappingResource(_ResourceBase):
collection_key = 'mappings'
member_key = 'mapping'
api_prefix = '/OS-FEDERATION'
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())
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
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)
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
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'
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)
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
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)
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
class OSFederationProjectResource(flask_restful.Resource):
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
what='GET /v3/OS-FEDERATION/projects',
in_favor_of='GET /v3/auth/projects')
def get(self):
"""Get projects for user.
GET/HEAD /OS-FEDERATION/projects
"""
ENFORCER.enforce_call(action='identity:get_auth_projects')
# TODO(morgan): Make this method simply call the endpoint for
# /v3/auth/projects once auth is ported to flask.
auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV)
user_id = auth_context.get('user_id')
group_ids = auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_projects_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
group_refs = []
if group_ids:
group_refs = PROVIDERS.assignment_api.list_projects_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_refs, group_refs)
return ks_flask.ResourceBase.wrap_collection(
refs, collection_name='projects')
class OSFederationDomainResource(flask_restful.Resource):
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
what='GET /v3/OS-FEDERATION/domains',
in_favor_of='GET /v3/auth/domains')
def get(self):
"""Get domains for user.
GET/HEAD /OS-FEDERATION/domains
"""
ENFORCER.enforce_call(action='identity:get_auth_domains')
# TODO(morgan): Make this method simply call the endpoint for
# /v3/auth/domains once auth is ported to flask.
auth_context = flask.request.environ.get(
authorization.AUTH_CONTEXT_ENV)
user_id = auth_context.get('user_id')
group_ids = auth_context.get('group_ids')
user_refs = []
if user_id:
try:
user_refs = PROVIDERS.assignment_api.list_domains_for_user(
user_id)
except exception.UserNotFound: # nosec
# federated users have an ide bu they don't link to anything
pass
group_refs = []
if group_ids:
group_refs = PROVIDERS.assignment_api.list_domains_for_groups(
group_ids)
refs = _combine_lists_uniquely(user_refs, group_refs)
return ks_flask.ResourceBase.wrap_collection(
refs, collection_name='domains')
class SAML2MetadataResource(flask_restful.Resource):
@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
class OSFederationAuthResource(flask_restful.Resource):
def _construct_webob_request(self):
# Build a fake(ish) webob request object from the flask request state
# to pass to the Auth Controller's authenticate_for_token. This is
# purely transitional code.
return request.Request(flask.request.environ)
@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)
@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 auth controller.
Build HTTP request body for federated authentication and inject
it into the ``authenticate_for_token`` function.
"""
compat_controller = keystone.federation.controllers.Auth()
auth = {
'identity': {
'methods': [protocol_id],
protocol_id: {
'identity_provider': idp_id,
'protocol': protocol_id
},
}
}
# NOTE(morgan): for compatibility, make sure we use a webob request
# until /auth is ported to flask. Since this is a webob response,
# deconstruct it and turn it into a flask response.
webob_resp = compat_controller.authenticate_for_token(
self._construct_webob_request(), auth)
flask_resp = flask.make_response(
webob_resp.body, webob_resp.status_code)
flask_resp.headers.extend(webob_resp.headers.dict_of_lists())
return flask_resp
class OSFederationAPI(ks_flask.APIBase):
_name = 'OS-FEDERATION'
_import_name = __name__
_api_url_prefix = '/OS-FEDERATION'
resources = []
resource_mapping = [
ks_flask.construct_resource_map(
# NOTE(morgan): No resource relation here, the resource relation is
# to /v3/auth/domains and /v3/auth/projects
resource=OSFederationDomainResource,
url='/domains',
resource_kwargs={}),
ks_flask.construct_resource_map(
# NOTE(morgan): No resource relation here, the resource relation is
# to /v3/auth/domains and /v3/auth/projects
resource=OSFederationProjectResource,
url='/projects',
resource_kwargs={}),
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}),
]
class OSFederationIdentityProvidersAPI(ks_flask.APIBase):
_name = 'identity_providers'
_import_name = __name__
_api_url_prefix = '/OS-FEDERATION'
resources = [IdentityProvidersResource]
resource_mapping = []
class OSFederationIdentityProvidersProtocolsAPI(ks_flask.APIBase):
_name = 'protocols'
_import_name = __name__
_api_url_prefix = '/OS-FEDERATION/identity_providers/<string:idp_id>'
resources = [IdentityProvidersProtocolsResource]
resource_mapping = []
class OSFederationMappingsAPI(ks_flask.APIBase):
_name = 'mappings'
_import_name = __name__
_api_url_prefix = '/OS-FEDERATION'
resources = [MappingResource]
resource_mapping = []
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
)

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from keystone.api._shared import json_home_relations
from keystone.auth import controllers
from keystone.common import json_home
from keystone.common import wsgi
@ -57,6 +58,17 @@ class Routers(wsgi.RoutersBase):
path='/auth/domains',
get_head_action='get_auth_domains',
rel=json_home.build_v3_resource_relation('auth_domains'))
# NOTE(morgan): explicitly add json_home data for auth_projects and
# auth_domains for OS-FEDERATION here, as auth will always own it
# based upon how the flask scaffolding works. This bit is transitional
# for the move to flask.
for element in ['projects', 'domains']:
resource_data = {'href': '/auth/%s' % element}
json_home.Status.update_resource_data(
resource_data, status=json_home.Status.STABLE)
json_home.JsonHomeResources.append_resource(
json_home_relations.os_federation_resource_rel_func(
resource_name=element), resource_data)
self._add_resource(
mapper, auth_controller,

View File

@ -15,7 +15,6 @@
import string
from oslo_log import log
from oslo_log import versionutils
from six.moves import http_client
from six.moves import urllib
import webob
@ -49,217 +48,6 @@ class _ControllerBase(controller.V3Controller):
return super(_ControllerBase, cls).base_url(context, path=path)
class IdentityProvider(_ControllerBase):
"""Identity Provider representation."""
collection_name = 'identity_providers'
member_name = 'identity_provider'
_public_parameters = frozenset(['id', 'enabled', 'description',
'remote_ids', 'links', 'domain_id'
])
@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()
def create_identity_provider(self, request, idp_id, identity_provider):
validation.lazy_validate(schema.identity_provider_create,
identity_provider)
identity_provider = self._normalize_dict(identity_provider)
identity_provider.setdefault('enabled', False)
idp_ref = PROVIDERS.federation_api.create_idp(
idp_id, identity_provider
)
response = IdentityProvider.wrap_member(request.context_dict, idp_ref)
return wsgi.render_response(
body=response, status=(http_client.CREATED,
http_client.responses[http_client.CREATED]))
@controller.filterprotected('id', 'enabled')
def list_identity_providers(self, request, filters):
hints = self.build_driver_hints(request, filters)
ref = PROVIDERS.federation_api.list_idps(hints=hints)
ref = [self.filter_params(x) for x in ref]
return IdentityProvider.wrap_collection(request.context_dict,
ref, hints=hints)
@controller.protected()
def get_identity_provider(self, request, idp_id):
ref = PROVIDERS.federation_api.get_idp(idp_id)
return IdentityProvider.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_identity_provider(self, request, idp_id):
PROVIDERS.federation_api.delete_idp(idp_id)
@controller.protected()
def update_identity_provider(self, request, idp_id, identity_provider):
validation.lazy_validate(schema.identity_provider_update,
identity_provider)
identity_provider = self._normalize_dict(identity_provider)
idp_ref = PROVIDERS.federation_api.update_idp(
idp_id, identity_provider
)
return IdentityProvider.wrap_member(request.context_dict, idp_ref)
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()
def create_protocol(self, request, idp_id, protocol_id, protocol):
validation.lazy_validate(schema.protocol_create, protocol)
protocol = self._normalize_dict(protocol)
ref = PROVIDERS.federation_api.create_protocol(
idp_id, protocol_id, protocol)
response = FederationProtocol.wrap_member(request.context_dict, ref)
return wsgi.render_response(
body=response, status=(http_client.CREATED,
http_client.responses[http_client.CREATED]))
@controller.protected()
def update_protocol(self, request, idp_id, protocol_id, protocol):
validation.lazy_validate(schema.protocol_update, protocol)
protocol = self._normalize_dict(protocol)
ref = PROVIDERS.federation_api.update_protocol(idp_id, protocol_id,
protocol)
return FederationProtocol.wrap_member(request.context_dict, ref)
@controller.protected()
def get_protocol(self, request, idp_id, protocol_id):
ref = PROVIDERS.federation_api.get_protocol(idp_id, protocol_id)
return FederationProtocol.wrap_member(request.context_dict, ref)
@controller.protected()
def list_protocols(self, request, idp_id):
protocols_ref = PROVIDERS.federation_api.list_protocols(idp_id)
protocols = list(protocols_ref)
return FederationProtocol.wrap_collection(request.context_dict,
protocols)
@controller.protected()
def delete_protocol(self, request, idp_id, protocol_id):
PROVIDERS.federation_api.delete_protocol(idp_id, protocol_id)
class MappingController(_ControllerBase):
collection_name = 'mappings'
member_name = 'mapping'
@controller.protected()
def create_mapping(self, request, mapping_id, mapping):
ref = self._normalize_dict(mapping)
utils.validate_mapping_structure(ref)
mapping_ref = PROVIDERS.federation_api.create_mapping(mapping_id, ref)
response = MappingController.wrap_member(request.context_dict,
mapping_ref)
return wsgi.render_response(
body=response, status=(http_client.CREATED,
http_client.responses[http_client.CREATED]))
@controller.protected()
def list_mappings(self, request):
ref = PROVIDERS.federation_api.list_mappings()
return MappingController.wrap_collection(request.context_dict, ref)
@controller.protected()
def get_mapping(self, request, mapping_id):
ref = PROVIDERS.federation_api.get_mapping(mapping_id)
return MappingController.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_mapping(self, request, mapping_id):
PROVIDERS.federation_api.delete_mapping(mapping_id)
@controller.protected()
def update_mapping(self, request, mapping_id, mapping):
mapping = self._normalize_dict(mapping)
utils.validate_mapping_structure(mapping)
mapping_ref = PROVIDERS.federation_api.update_mapping(
mapping_id, mapping
)
return MappingController.wrap_member(request.context_dict, mapping_ref)
class Auth(auth_controllers.Auth):
def _get_sso_origin_host(self, request):
@ -436,117 +224,3 @@ class Auth(auth_controllers.Auth):
body=ecp_assertion.to_string(),
status=(http_client.OK, http_client.responses[http_client.OK]),
headers=headers)
class DomainV3(controller.V3Controller):
collection_name = 'domains'
member_name = 'domain'
def __init__(self):
super(DomainV3, self).__init__()
self.get_member_from_driver = PROVIDERS.resource_api.get_domain
@versionutils.deprecated(
as_of=versionutils.deprecated.JUNO,
in_favor_of='GET /v3/auth/domains/',
)
@controller.protected()
def list_domains_for_user(self, request):
"""List all domains available to an authenticated user.
:param context: request context
:returns: list of accessible domains
"""
controller = auth_controllers.Auth()
return controller.get_auth_domains(request)
class ProjectAssignmentV3(controller.V3Controller):
collection_name = 'projects'
member_name = 'project'
def __init__(self):
super(ProjectAssignmentV3, self).__init__()
self.get_member_from_driver = PROVIDERS.resource_api.get_project
@versionutils.deprecated(
as_of=versionutils.deprecated.JUNO,
in_favor_of='GET /v3/auth/projects/',
)
@controller.protected()
def list_projects_for_user(self, request):
"""List all projects available to an authenticated user.
:param context: request context
:returns: list of accessible projects
"""
controller = auth_controllers.Auth()
return controller.get_auth_projects(request)
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()
def create_service_provider(self, request, sp_id, service_provider):
validation.lazy_validate(schema.service_provider_create,
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 = PROVIDERS.federation_api.create_sp(sp_id, service_provider)
response = ServiceProvider.wrap_member(request.context_dict, sp_ref)
return wsgi.render_response(
body=response, status=(http_client.CREATED,
http_client.responses[http_client.CREATED]))
@controller.filterprotected('id', 'enabled')
def list_service_providers(self, request, filters):
hints = self.build_driver_hints(request, filters)
ref = PROVIDERS.federation_api.list_sps(hints=hints)
ref = [self.filter_params(x) for x in ref]
return ServiceProvider.wrap_collection(request.context_dict,
ref, hints=hints)
@controller.protected()
def get_service_provider(self, request, sp_id):
ref = PROVIDERS.federation_api.get_sp(sp_id)
return ServiceProvider.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_service_provider(self, request, sp_id):
PROVIDERS.federation_api.delete_sp(sp_id)
@controller.protected()
def update_service_provider(self, request, sp_id, service_provider):
validation.lazy_validate(schema.service_provider_update,
service_provider)
service_provider = self._normalize_dict(service_provider)
sp_ref = PROVIDERS.federation_api.update_sp(sp_id, service_provider)
return ServiceProvider.wrap_member(request.context_dict, 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=(http_client.OK,
http_client.responses[http_client.OK]),
headers=[('Content-Type', 'text/xml')])

View File

@ -36,54 +36,6 @@ class Routers(wsgi.RoutersBase):
The API looks like::
PUT /OS-FEDERATION/identity_providers/{idp_id}
GET /OS-FEDERATION/identity_providers
HEAD /OS-FEDERATION/identity_providers
GET /OS-FEDERATION/identity_providers/{idp_id}
HEAD /OS-FEDERATION/identity_providers/{idp_id}
DELETE /OS-FEDERATION/identity_providers/{idp_id}
PATCH /OS-FEDERATION/identity_providers/{idp_id}
PUT /OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}
GET /OS-FEDERATION/identity_providers/
{idp_id}/protocols
HEAD /OS-FEDERATION/identity_providers/
{idp_id}/protocols
GET /OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}
HEAD /OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}
PATCH /OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}
DELETE /OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}
PUT /OS-FEDERATION/mappings
GET /OS-FEDERATION/mappings
HEAD /OS-FEDERATION/mappings
PATCH /OS-FEDERATION/mappings/{mapping_id}
GET /OS-FEDERATION/mappings/{mapping_id}
HEAD /OS-FEDERATION/mappings/{mapping_id}
DELETE /OS-FEDERATION/mappings/{mapping_id}
GET /OS-FEDERATION/projects
HEAD /OS-FEDERATION/projects
GET /OS-FEDERATION/domains
HEAD /OS-FEDERATION/domains
PUT /OS-FEDERATION/service_providers/{sp_id}
GET /OS-FEDERATION/service_providers
HEAD /OS-FEDERATION/service_providers
GET /OS-FEDERATION/service_providers/{sp_id}
HEAD /OS-FEDERATION/service_providers/{sp_id}
DELETE /OS-FEDERATION/service_providers/{sp_id}
PATCH /OS-FEDERATION/service_providers/{sp_id}
GET /OS-FEDERATION/identity_providers/{idp_id}/
protocols/{protocol_id}/auth
POST /OS-FEDERATION/identity_providers/{idp_id}/
protocols/{protocol_id}/auth
GET /auth/OS-FEDERATION/identity_providers/
{idp_id}/protocols/{protocol_id}/websso
?origin=https%3A//horizon.example.com
@ -94,8 +46,6 @@ class Routers(wsgi.RoutersBase):
POST /auth/OS-FEDERATION/saml2
POST /auth/OS-FEDERATION/saml2/ecp
GET /OS-FEDERATION/saml2/metadata
HEAD /OS-FEDERATION/saml2/metadata
GET /auth/OS-FEDERATION/websso/{protocol_id}
?origin=https%3A//horizon.example.com
@ -105,131 +55,15 @@ class Routers(wsgi.RoutersBase):
"""
_path_prefixes = ('auth', 'OS-FEDERATION')
_path_prefixes = ('auth',)
def _construct_url(self, suffix):
return "/OS-FEDERATION/%s" % suffix
def append_v3_routers(self, mapper, routers):
auth_controller = controllers.Auth()
idp_controller = controllers.IdentityProvider()
protocol_controller = controllers.FederationProtocol()
mapping_controller = controllers.MappingController()
project_controller = controllers.ProjectAssignmentV3()
domain_controller = controllers.DomainV3()
saml_metadata_controller = controllers.SAMLMetadataV3()
sp_controller = controllers.ServiceProvider()
# Identity Provider CRUD operations
self._add_resource(
mapper, idp_controller,
path=self._construct_url('identity_providers/{idp_id}'),
get_head_action='get_identity_provider',
put_action='create_identity_provider',
patch_action='update_identity_provider',
delete_action='delete_identity_provider',
rel=build_resource_relation(resource_name='identity_provider'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
})
self._add_resource(
mapper, idp_controller,
path=self._construct_url('identity_providers'),
get_head_action='list_identity_providers',
rel=build_resource_relation(resource_name='identity_providers'))
# Protocol CRUD operations
self._add_resource(
mapper, protocol_controller,
path=self._construct_url('identity_providers/{idp_id}/protocols/'
'{protocol_id}'),
get_head_action='get_protocol',
put_action='create_protocol',
patch_action='update_protocol',
delete_action='delete_protocol',
rel=build_resource_relation(
resource_name='identity_provider_protocol'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
})
self._add_resource(
mapper, protocol_controller,
path=self._construct_url('identity_providers/{idp_id}/protocols'),
get_head_action='list_protocols',
rel=build_resource_relation(
resource_name='identity_provider_protocols'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
})
# Mapping CRUD operations
self._add_resource(
mapper, mapping_controller,
path=self._construct_url('mappings/{mapping_id}'),
get_head_action='get_mapping',
put_action='create_mapping',
patch_action='update_mapping',
delete_action='delete_mapping',
rel=build_resource_relation(resource_name='mapping'),
path_vars={
'mapping_id': build_parameter_relation(
parameter_name='mapping_id'),
})
self._add_resource(
mapper, mapping_controller,
path=self._construct_url('mappings'),
get_head_action='list_mappings',
rel=build_resource_relation(resource_name='mappings'))
# Service Providers CRUD operations
self._add_resource(
mapper, sp_controller,
path=self._construct_url('service_providers/{sp_id}'),
get_head_action='get_service_provider',
put_action='create_service_provider',
patch_action='update_service_provider',
delete_action='delete_service_provider',
rel=build_resource_relation(resource_name='service_provider'),
path_vars={
'sp_id': SP_ID_PARAMETER_RELATION,
})
self._add_resource(
mapper, sp_controller,
path=self._construct_url('service_providers'),
get_head_action='list_service_providers',
rel=build_resource_relation(resource_name='service_providers'))
self._add_resource(
mapper, domain_controller,
path=self._construct_url('domains'),
new_path='/auth/domains',
get_head_action='list_domains_for_user',
rel=build_resource_relation(resource_name='domains'))
self._add_resource(
mapper, project_controller,
path=self._construct_url('projects'),
new_path='/auth/projects',
get_head_action='list_projects_for_user',
rel=build_resource_relation(resource_name='projects'))
# Auth operations
self._add_resource(
mapper, auth_controller,
path=self._construct_url('identity_providers/{idp_id}/'
'protocols/{protocol_id}/auth'),
get_post_action='federated_authentication',
rel=build_resource_relation(
resource_name='identity_provider_protocol_auth'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
})
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url('saml2'),
@ -253,15 +87,9 @@ class Routers(wsgi.RoutersBase):
path='/auth' + self._construct_url(
'identity_providers/{idp_id}/protocols/{protocol_id}/websso'),
get_post_action='federated_idp_specific_sso_auth',
rel=build_resource_relation(resource_name='identity_providers'),
rel=build_resource_relation(
resource_name='identity_providers_websso'),
path_vars={
'idp_id': IDP_ID_PARAMETER_RELATION,
'protocol_id': PROTOCOL_ID_PARAMETER_RELATION,
})
# Keystone-Identity-Provider metadata endpoint
self._add_resource(
mapper, saml_metadata_controller,
path=self._construct_url('saml2/metadata'),
get_head_action='get_metadata',
rel=build_resource_relation(resource_name='metadata'))

View File

@ -42,6 +42,7 @@ _MOVED_API_PREFIXES = frozenset(
'endpoints',
'OS-OAUTH1',
'OS-EP-FILTER',
'OS-FEDERATION',
'OS-REVOKE',
'OS-SIMPLE-CERT',
'OS-TRUST',

View File

@ -317,6 +317,7 @@ class APIBase(object):
c_key = getattr(r, 'collection_key', None)
m_key = getattr(r, 'member_key', None)
r_pfx = getattr(r, 'api_prefix', None)
if not c_key or not m_key:
LOG.debug('Unable to add resource %(resource)s to API '
'%(name)s, both `member_key` and `collection_key` '
@ -336,13 +337,20 @@ class APIBase(object):
# NOTE(morgan): The Prefix is automatically added by the API, so
# we do not add it to the paths here.
collection_path = '/%s' % c_key
entity_path = '/%(collection)s/<string:%(member)s_id>' % {
'collection': c_key, 'member': m_key}
if getattr(r, '_id_path_param_name_override', None):
# The member_key doesn't match the "id" key in the url, make
# sure to use the correct path-key for ID.
member_id_key = getattr(r, '_id_path_param_name_override')
else:
member_id_key = '%(member_key)s_id' % {'member_key': m_key}
entity_path = '/%(collection)s/<string:%(member)s>' % {
'collection': c_key, 'member': member_id_key}
# NOTE(morgan): The json-home form of the entity path is different
# from the flask-url routing form. Must also include the prefix
jh_e_path = '%(pfx)s/%(e_path)s' % {
jh_e_path = _URL_SUBST.sub('{\\1}', '%(pfx)s/%(e_path)s' % {
'pfx': self._api_url_prefix,
'e_path': _URL_SUBST.sub('{\\1}', entity_path).lstrip('/')}
'e_path': entity_path.lstrip('/')})
LOG.debug(
'Adding standard routes to API %(name)s for `%(resource)s` '
@ -360,16 +368,37 @@ class APIBase(object):
json_home.build_v3_resource_relation)
resource_rel_status = getattr(
r, 'json_home_resource_status', None)
collection_rel = resource_rel_func(resource_name=c_key)
collection_rel_resource_name = getattr(
r, 'json_home_collection_resource_name_override', c_key)
collection_rel = resource_rel_func(
resource_name=collection_rel_resource_name)
# NOTE(morgan): Add the prefix explicitly for JSON Home documents
# to the collection path.
rel_data = {'href': '%(pfx)s%(collection_path)s' % {
href_val = '%(pfx)s%(collection_path)s' % {
'pfx': self._api_url_prefix,
'collection_path': collection_path}
}
entity_rel = resource_rel_func(resource_name=m_key)
id_str = '%s_id' % m_key
# If additional parameters exist in the URL, add them to the
# href-vars dict.
additional_params = getattr(
r, 'json_home_additional_parameters', {})
if additional_params:
# NOTE(morgan): Special case, we have 'additional params' which
# means we know the params are in the "prefix". This guarantees
# the correct data in the json_home document with href-template
# and href-vars even on the "collection" entry
rel_data = dict()
rel_data['href-template'] = _URL_SUBST.sub('{\\1}', href_val)
rel_data['href-vars'] = additional_params
else:
rel_data = {'href': href_val}
member_rel_resource_name = getattr(
r, 'json_home_member_resource_name_override', m_key)
entity_rel = resource_rel_func(
resource_name=member_rel_resource_name)
id_str = member_id_key
parameter_rel_func = getattr(
r, 'json_home_parameter_rel_func',
@ -378,6 +407,10 @@ class APIBase(object):
entity_rel_data = {'href-template': jh_e_path,
'href-vars': {id_str: id_param_rel}}
if additional_params:
entity_rel_data.setdefault('href-vars', {}).update(
additional_params)
if resource_rel_status is not None:
json_home.Status.update_resource_data(
rel_data, resource_rel_status)
@ -535,9 +568,11 @@ class ResourceBase(flask_restful.Resource):
collection_key = None
member_key = None
_public_parameters = frozenset([])
# NOTE(morgan): This must match the string on the API the resource is
# registered to.
api_prefix = ''
_id_path_param_name_override = None
method_decorators = []
@ -565,6 +600,25 @@ class ResourceBase(flask_restful.Resource):
if ref.get('id') is not None and id_arg != ref['id']:
raise exception.ValidationError('Cannot change ID')
@classmethod
def filter_params(cls, ref):
"""Remove unspecified parameters from the dictionary.
This function removes unspecified parameters from the dictionary.
This method checks only root-level keys from a ref dictionary.
:param ref: a dictionary representing deserialized response to be
serialized
"""
# NOTE(morgan): if _public_parameters is empty, do nothing. We do not
# filter if we do not have an explicit white-list to work from.
if cls._public_parameters:
ref_keys = set(ref.keys())
blocked_keys = ref_keys - cls._public_parameters
for blocked_param in blocked_keys:
del ref[blocked_param]
return ref
@classmethod
def wrap_collection(cls, refs, hints=None, collection_name=None):
"""Wrap a collection, checking for filtering and pagination.

View File

@ -391,7 +391,7 @@ V3_JSON_HOME_RESOURCES = {
{
'href-template': '/OS-FEDERATION/identity_providers/{idp_id}',
'href-vars': {'idp_id': IDP_ID_PARAMETER_RELATION, }},
_build_federation_rel(resource_name='identity_providers'): {
_build_federation_rel(resource_name='identity_providers_websso'): {
'href-template': FEDERATED_IDP_SPECIFIC_WEBSSO,
'href-vars': {
'idp_id': IDP_ID_PARAMETER_RELATION,
@ -799,9 +799,14 @@ class VersionTestCase(unit.TestCase):
self.assertThat(resp.status, tt_matchers.Equals('200 OK'))
self.assertThat(resp.headers['Content-Type'],
tt_matchers.Equals('application/json-home'))
self.assertThat(jsonutils.loads(resp.body),
tt_matchers.Equals(exp_json_home_data))
maxDiff = self.maxDiff
self.maxDiff = None
# NOTE(morgan): Changed from tt_matchers.Equals to make it easier to
# determine issues. Reset maxDiff to the original value at the end
# of the assert.
self.assertDictEqual(exp_json_home_data,
jsonutils.loads(resp.body))
self.maxDiff = maxDiff
def test_json_home_v3(self):
# If the request is /v3 and the Accept header is application/json-home