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:
parent
294ca38554
commit
94f8f103ab
@ -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,
|
||||
|
@ -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')
|
||||
|
597
keystone/api/os_federation.py
Normal file
597
keystone/api/os_federation.py
Normal 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
|
||||
)
|
@ -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,
|
||||
|
@ -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')])
|
||||
|
@ -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'))
|
||||
|
@ -42,6 +42,7 @@ _MOVED_API_PREFIXES = frozenset(
|
||||
'endpoints',
|
||||
'OS-OAUTH1',
|
||||
'OS-EP-FILTER',
|
||||
'OS-FEDERATION',
|
||||
'OS-REVOKE',
|
||||
'OS-SIMPLE-CERT',
|
||||
'OS-TRUST',
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user