From cbefe7c7b83b5f940f817adf60a4be35bd324775 Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Sun, 11 Oct 2015 03:01:05 -0400 Subject: [PATCH] Move federation extension into keystone core Remove federation as an extension and move it to a core resource. For now we leave the database migrations in the extension directory until we have a general policy for merging these into core. Some instances of federation constants were removed because they were causing a circular dependency, these can be refactored in a later patch. DocImpact: You should no longer run the migrations for this extension Implements: bp move-extensions Co-Authored-By: Nithya Renganathan Change-Id: If5857a6ee4c7c527929069b25beab40f4c5d87e2 --- etc/keystone-paste.ini | 5 +- keystone/auth/controllers.py | 4 +- keystone/auth/plugins/mapped.py | 4 +- keystone/cmd/cli.py | 6 +- keystone/common/tokenless_auth.py | 4 +- keystone/contrib/federation/__init__.py | 15 - keystone/contrib/federation/backends/sql.py | 359 +---------------- keystone/contrib/federation/routers.py | 245 +----------- keystone/federation/__init__.py | 16 + keystone/federation/backends/__init__.py | 0 keystone/federation/backends/sql.py | 366 ++++++++++++++++++ .../{contrib => }/federation/constants.py | 0 .../{contrib => }/federation/controllers.py | 6 +- keystone/{contrib => }/federation/core.py | 2 +- keystone/{contrib => }/federation/idp.py | 0 keystone/federation/routers.py | 252 ++++++++++++ keystone/{contrib => }/federation/schema.py | 0 keystone/{contrib => }/federation/utils.py | 4 +- keystone/middleware/auth.py | 4 +- keystone/models/token_model.py | 17 +- keystone/server/backends.py | 2 +- .../tests/unit/common/test_authorization.py | 2 +- .../unit/contrib/federation/test_utils.py | 2 +- keystone/tests/unit/test_middleware.py | 2 +- keystone/tests/unit/test_v3_federation.py | 36 +- keystone/tests/unit/test_validation.py | 2 +- .../tests/unit/token/test_fernet_provider.py | 2 +- keystone/tests/unit/token/test_token_model.py | 2 +- keystone/token/providers/common.py | 2 +- keystone/token/providers/fernet/core.py | 2 +- keystone/version/service.py | 4 +- setup.cfg | 2 +- 32 files changed, 714 insertions(+), 655 deletions(-) create mode 100644 keystone/federation/__init__.py create mode 100644 keystone/federation/backends/__init__.py create mode 100644 keystone/federation/backends/sql.py rename keystone/{contrib => }/federation/constants.py (100%) rename keystone/{contrib => }/federation/controllers.py (99%) rename keystone/{contrib => }/federation/core.py (99%) rename keystone/{contrib => }/federation/idp.py (100%) create mode 100644 keystone/federation/routers.py rename keystone/{contrib => }/federation/schema.py (100%) rename keystone/{contrib => }/federation/utils.py (99%) diff --git a/etc/keystone-paste.ini b/etc/keystone-paste.ini index 70db3823dd..888e005101 100644 --- a/etc/keystone-paste.ini +++ b/etc/keystone-paste.ini @@ -30,9 +30,6 @@ use = egg:keystone#ec2_extension [filter:ec2_extension_v3] use = egg:keystone#ec2_extension_v3 -[filter:federation_extension] -use = egg:keystone#federation_extension - [filter:oauth1_extension] use = egg:keystone#oauth1_extension @@ -76,7 +73,7 @@ pipeline = sizelimit url_normalize request_id build_auth_context token_auth admi [pipeline:api_v3] # The last item in this pipeline must be service_v3 or an equivalent # application. It cannot be a filter. -pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension federation_extension oauth1_extension endpoint_filter_extension service_v3 +pipeline = sizelimit url_normalize request_id build_auth_context token_auth admin_token_auth json_body ec2_extension_v3 s3_extension simple_cert_extension revoke_extension oauth1_extension endpoint_filter_extension service_v3 [app:public_version_service] use = egg:keystone#public_version_service diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py index 998282bf08..bebf66f839 100644 --- a/keystone/auth/controllers.py +++ b/keystone/auth/controllers.py @@ -28,7 +28,6 @@ from keystone.common import dependency from keystone.common import utils from keystone.common import wsgi from keystone import config -from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _, _LI, _LW from keystone.resource import controllers as resource_controllers @@ -423,7 +422,8 @@ class Auth(controller.V3Controller): return # Skip scoping when unscoped federated token is being issued - if federation_constants.IDENTITY_PROVIDER in auth_context: + # FIXME(stevemar): Use constants from keystone.federation.constants + if 'OS-FEDERATION:identity_provider' in auth_context: return # Do not scope if request is for explicitly unscoped token diff --git a/keystone/auth/plugins/mapped.py b/keystone/auth/plugins/mapped.py index 3f031d20be..504ddaaabe 100644 --- a/keystone/auth/plugins/mapped.py +++ b/keystone/auth/plugins/mapped.py @@ -19,9 +19,9 @@ from six.moves.urllib import parse from keystone import auth from keystone.auth import plugins as auth_plugins from keystone.common import dependency -from keystone.contrib.federation import constants as federation_constants -from keystone.contrib.federation import utils from keystone import exception +from keystone.federation import constants as federation_constants +from keystone.federation import utils from keystone.i18n import _ from keystone.models import token_model from keystone import notifications diff --git a/keystone/cmd/cli.py b/keystone/cmd/cli.py index 665cbcd1e8..983f63475d 100644 --- a/keystone/cmd/cli.py +++ b/keystone/cmd/cli.py @@ -29,6 +29,8 @@ from keystone.common.sql import migration_helpers from keystone.common import utils from keystone import config from keystone import exception +from keystone.federation import idp +from keystone.federation import utils as mapping_engine from keystone.i18n import _, _LW from keystone.server import backends from keystone import token @@ -538,9 +540,6 @@ class SamlIdentityProviderMetadata(BaseApp): @staticmethod def main(): - # NOTE(marek-denis): Since federation is currently an extension import - # corresponding modules only when they are really going to be used. - from keystone.contrib.federation import idp metadata = idp.MetadataGenerator().generate_metadata() print(metadata.to_string()) @@ -598,7 +597,6 @@ class MappingEngineTester(BaseApp): @classmethod def main(cls): - from keystone.contrib.federation import utils as mapping_engine if not CONF.command.engine_debug: mapping_engine.LOG.logger.setLevel('WARN') diff --git a/keystone/common/tokenless_auth.py b/keystone/common/tokenless_auth.py index d1c334adf2..2962489d13 100644 --- a/keystone/common/tokenless_auth.py +++ b/keystone/common/tokenless_auth.py @@ -20,9 +20,9 @@ from oslo_log import log from keystone.auth import controllers from keystone.common import dependency -from keystone.contrib.federation import constants as federation_constants -from keystone.contrib.federation import utils from keystone import exception +from keystone.federation import constants as federation_constants +from keystone.federation import utils from keystone.i18n import _ diff --git a/keystone/contrib/federation/__init__.py b/keystone/contrib/federation/__init__.py index 57c9e42ce4..e69de29bb2 100644 --- a/keystone/contrib/federation/__init__.py +++ b/keystone/contrib/federation/__init__.py @@ -1,15 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# -# 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. - -from keystone.contrib.federation.core import * # noqa diff --git a/keystone/contrib/federation/backends/sql.py b/keystone/contrib/federation/backends/sql.py index dbd17025e1..3c24d9c08d 100644 --- a/keystone/contrib/federation/backends/sql.py +++ b/keystone/contrib/federation/backends/sql.py @@ -12,355 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_serialization import jsonutils +from oslo_log import versionutils -from keystone.common import sql -from keystone.contrib.federation import core -from keystone import exception -from sqlalchemy import orm +from keystone.federation.backends import sql + +_OLD = "keystone.contrib.federation.backends.sql.Federation" +_NEW = "sql" -class FederationProtocolModel(sql.ModelBase, sql.DictBase): - __tablename__ = 'federation_protocol' - attributes = ['id', 'idp_id', 'mapping_id'] - mutable_attributes = frozenset(['mapping_id']) +class Federation(sql.Federation): - id = sql.Column(sql.String(64), primary_key=True) - idp_id = sql.Column(sql.String(64), sql.ForeignKey('identity_provider.id', - ondelete='CASCADE'), primary_key=True) - mapping_id = sql.Column(sql.String(64), nullable=False) - - @classmethod - def from_dict(cls, dictionary): - new_dictionary = dictionary.copy() - return cls(**new_dictionary) - - def to_dict(self): - """Return a dictionary with model's attributes.""" - d = dict() - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - return d - - -class IdentityProviderModel(sql.ModelBase, sql.DictBase): - __tablename__ = 'identity_provider' - attributes = ['id', 'enabled', 'description', 'remote_ids'] - mutable_attributes = frozenset(['description', 'enabled', 'remote_ids']) - - id = sql.Column(sql.String(64), primary_key=True) - enabled = sql.Column(sql.Boolean, nullable=False) - description = sql.Column(sql.Text(), nullable=True) - remote_ids = orm.relationship('IdPRemoteIdsModel', - order_by='IdPRemoteIdsModel.remote_id', - cascade='all, delete-orphan') - - @classmethod - def from_dict(cls, dictionary): - new_dictionary = dictionary.copy() - remote_ids_list = new_dictionary.pop('remote_ids', None) - if not remote_ids_list: - remote_ids_list = [] - identity_provider = cls(**new_dictionary) - remote_ids = [] - # NOTE(fmarco76): the remote_ids_list contains only remote ids - # associated with the IdP because of the "relationship" established in - # sqlalchemy and corresponding to the FK in the idp_remote_ids table - for remote in remote_ids_list: - remote_ids.append(IdPRemoteIdsModel(remote_id=remote)) - identity_provider.remote_ids = remote_ids - return identity_provider - - def to_dict(self): - """Return a dictionary with model's attributes.""" - d = dict() - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - d['remote_ids'] = [] - for remote in self.remote_ids: - d['remote_ids'].append(remote.remote_id) - return d - - -class IdPRemoteIdsModel(sql.ModelBase, sql.DictBase): - __tablename__ = 'idp_remote_ids' - attributes = ['idp_id', 'remote_id'] - mutable_attributes = frozenset(['idp_id', 'remote_id']) - - idp_id = sql.Column(sql.String(64), - sql.ForeignKey('identity_provider.id', - ondelete='CASCADE')) - remote_id = sql.Column(sql.String(255), - primary_key=True) - - @classmethod - def from_dict(cls, dictionary): - new_dictionary = dictionary.copy() - return cls(**new_dictionary) - - def to_dict(self): - """Return a dictionary with model's attributes.""" - d = dict() - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - return d - - -class MappingModel(sql.ModelBase, sql.DictBase): - __tablename__ = 'mapping' - attributes = ['id', 'rules'] - - id = sql.Column(sql.String(64), primary_key=True) - rules = sql.Column(sql.JsonBlob(), nullable=False) - - @classmethod - def from_dict(cls, dictionary): - new_dictionary = dictionary.copy() - new_dictionary['rules'] = jsonutils.dumps(new_dictionary['rules']) - return cls(**new_dictionary) - - def to_dict(self): - """Return a dictionary with model's attributes.""" - d = dict() - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - d['rules'] = jsonutils.loads(d['rules']) - return d - - -class ServiceProviderModel(sql.ModelBase, sql.DictBase): - __tablename__ = 'service_provider' - attributes = ['auth_url', 'id', 'enabled', 'description', - 'relay_state_prefix', 'sp_url'] - mutable_attributes = frozenset(['auth_url', 'description', 'enabled', - 'relay_state_prefix', 'sp_url']) - - id = sql.Column(sql.String(64), primary_key=True) - enabled = sql.Column(sql.Boolean, nullable=False) - description = sql.Column(sql.Text(), nullable=True) - auth_url = sql.Column(sql.String(256), nullable=False) - sp_url = sql.Column(sql.String(256), nullable=False) - relay_state_prefix = sql.Column(sql.String(256), nullable=False) - - @classmethod - def from_dict(cls, dictionary): - new_dictionary = dictionary.copy() - return cls(**new_dictionary) - - def to_dict(self): - """Return a dictionary with model's attributes.""" - d = dict() - for attr in self.__class__.attributes: - d[attr] = getattr(self, attr) - return d - - -class Federation(core.FederationDriverV8): - - # Identity Provider CRUD - @sql.handle_conflicts(conflict_type='identity_provider') - def create_idp(self, idp_id, idp): - idp['id'] = idp_id - with sql.transaction() as session: - idp_ref = IdentityProviderModel.from_dict(idp) - session.add(idp_ref) - return idp_ref.to_dict() - - def delete_idp(self, idp_id): - with sql.transaction() as session: - self._delete_assigned_protocols(session, idp_id) - idp_ref = self._get_idp(session, idp_id) - session.delete(idp_ref) - - def _get_idp(self, session, idp_id): - idp_ref = session.query(IdentityProviderModel).get(idp_id) - if not idp_ref: - raise exception.IdentityProviderNotFound(idp_id=idp_id) - return idp_ref - - def _get_idp_from_remote_id(self, session, remote_id): - q = session.query(IdPRemoteIdsModel) - q = q.filter_by(remote_id=remote_id) - try: - return q.one() - except sql.NotFound: - raise exception.IdentityProviderNotFound(idp_id=remote_id) - - def list_idps(self): - with sql.transaction() as session: - idps = session.query(IdentityProviderModel) - idps_list = [idp.to_dict() for idp in idps] - return idps_list - - def get_idp(self, idp_id): - with sql.transaction() as session: - idp_ref = self._get_idp(session, idp_id) - return idp_ref.to_dict() - - def get_idp_from_remote_id(self, remote_id): - with sql.transaction() as session: - ref = self._get_idp_from_remote_id(session, remote_id) - return ref.to_dict() - - def update_idp(self, idp_id, idp): - with sql.transaction() as session: - idp_ref = self._get_idp(session, idp_id) - old_idp = idp_ref.to_dict() - old_idp.update(idp) - new_idp = IdentityProviderModel.from_dict(old_idp) - for attr in IdentityProviderModel.mutable_attributes: - setattr(idp_ref, attr, getattr(new_idp, attr)) - return idp_ref.to_dict() - - # Protocol CRUD - def _get_protocol(self, session, idp_id, protocol_id): - q = session.query(FederationProtocolModel) - q = q.filter_by(id=protocol_id, idp_id=idp_id) - try: - return q.one() - except sql.NotFound: - kwargs = {'protocol_id': protocol_id, - 'idp_id': idp_id} - raise exception.FederatedProtocolNotFound(**kwargs) - - @sql.handle_conflicts(conflict_type='federation_protocol') - def create_protocol(self, idp_id, protocol_id, protocol): - protocol['id'] = protocol_id - protocol['idp_id'] = idp_id - with sql.transaction() as session: - self._get_idp(session, idp_id) - protocol_ref = FederationProtocolModel.from_dict(protocol) - session.add(protocol_ref) - return protocol_ref.to_dict() - - def update_protocol(self, idp_id, protocol_id, protocol): - with sql.transaction() as session: - proto_ref = self._get_protocol(session, idp_id, protocol_id) - old_proto = proto_ref.to_dict() - old_proto.update(protocol) - new_proto = FederationProtocolModel.from_dict(old_proto) - for attr in FederationProtocolModel.mutable_attributes: - setattr(proto_ref, attr, getattr(new_proto, attr)) - return proto_ref.to_dict() - - def get_protocol(self, idp_id, protocol_id): - with sql.transaction() as session: - protocol_ref = self._get_protocol(session, idp_id, protocol_id) - return protocol_ref.to_dict() - - def list_protocols(self, idp_id): - with sql.transaction() as session: - q = session.query(FederationProtocolModel) - q = q.filter_by(idp_id=idp_id) - protocols = [protocol.to_dict() for protocol in q] - return protocols - - def delete_protocol(self, idp_id, protocol_id): - with sql.transaction() as session: - key_ref = self._get_protocol(session, idp_id, protocol_id) - session.delete(key_ref) - - def _delete_assigned_protocols(self, session, idp_id): - query = session.query(FederationProtocolModel) - query = query.filter_by(idp_id=idp_id) - query.delete() - - # Mapping CRUD - def _get_mapping(self, session, mapping_id): - mapping_ref = session.query(MappingModel).get(mapping_id) - if not mapping_ref: - raise exception.MappingNotFound(mapping_id=mapping_id) - return mapping_ref - - @sql.handle_conflicts(conflict_type='mapping') - def create_mapping(self, mapping_id, mapping): - ref = {} - ref['id'] = mapping_id - ref['rules'] = mapping.get('rules') - with sql.transaction() as session: - mapping_ref = MappingModel.from_dict(ref) - session.add(mapping_ref) - return mapping_ref.to_dict() - - def delete_mapping(self, mapping_id): - with sql.transaction() as session: - mapping_ref = self._get_mapping(session, mapping_id) - session.delete(mapping_ref) - - def list_mappings(self): - with sql.transaction() as session: - mappings = session.query(MappingModel) - return [x.to_dict() for x in mappings] - - def get_mapping(self, mapping_id): - with sql.transaction() as session: - mapping_ref = self._get_mapping(session, mapping_id) - return mapping_ref.to_dict() - - @sql.handle_conflicts(conflict_type='mapping') - def update_mapping(self, mapping_id, mapping): - ref = {} - ref['id'] = mapping_id - ref['rules'] = mapping.get('rules') - with sql.transaction() as session: - mapping_ref = self._get_mapping(session, mapping_id) - old_mapping = mapping_ref.to_dict() - old_mapping.update(ref) - new_mapping = MappingModel.from_dict(old_mapping) - for attr in MappingModel.attributes: - setattr(mapping_ref, attr, getattr(new_mapping, attr)) - return mapping_ref.to_dict() - - def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): - with sql.transaction() as session: - protocol_ref = self._get_protocol(session, idp_id, protocol_id) - mapping_id = protocol_ref.mapping_id - mapping_ref = self._get_mapping(session, mapping_id) - return mapping_ref.to_dict() - - # Service Provider CRUD - @sql.handle_conflicts(conflict_type='service_provider') - def create_sp(self, sp_id, sp): - sp['id'] = sp_id - with sql.transaction() as session: - sp_ref = ServiceProviderModel.from_dict(sp) - session.add(sp_ref) - return sp_ref.to_dict() - - def delete_sp(self, sp_id): - with sql.transaction() as session: - sp_ref = self._get_sp(session, sp_id) - session.delete(sp_ref) - - def _get_sp(self, session, sp_id): - sp_ref = session.query(ServiceProviderModel).get(sp_id) - if not sp_ref: - raise exception.ServiceProviderNotFound(sp_id=sp_id) - return sp_ref - - def list_sps(self): - with sql.transaction() as session: - sps = session.query(ServiceProviderModel) - sps_list = [sp.to_dict() for sp in sps] - return sps_list - - def get_sp(self, sp_id): - with sql.transaction() as session: - sp_ref = self._get_sp(session, sp_id) - return sp_ref.to_dict() - - def update_sp(self, sp_id, sp): - with sql.transaction() as session: - sp_ref = self._get_sp(session, sp_id) - old_sp = sp_ref.to_dict() - old_sp.update(sp) - new_sp = ServiceProviderModel.from_dict(old_sp) - for attr in ServiceProviderModel.mutable_attributes: - setattr(sp_ref, attr, getattr(new_sp, attr)) - return sp_ref.to_dict() - - def get_enabled_service_providers(self): - with sql.transaction() as session: - service_providers = session.query(ServiceProviderModel) - service_providers = service_providers.filter_by(enabled=True) - return service_providers + @versionutils.deprecated(versionutils.deprecated.MITAKA, + in_favor_of=_NEW, + what=_OLD) + def __init__(self, *args, **kwargs): + super(Federation, self).__init__(*args, **kwargs) diff --git a/keystone/contrib/federation/routers.py b/keystone/contrib/federation/routers.py index 686ddf41b2..d5857ca61b 100644 --- a/keystone/contrib/federation/routers.py +++ b/keystone/contrib/federation/routers.py @@ -10,243 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. -import functools +from oslo_log import log +from oslo_log import versionutils -from keystone.common import json_home from keystone.common import wsgi -from keystone.contrib.federation import controllers +from keystone.i18n import _ -build_resource_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-FEDERATION', extension_version='1.0') - -build_parameter_relation = functools.partial( - json_home.build_v3_extension_parameter_relation, - extension_name='OS-FEDERATION', extension_version='1.0') - -IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id') -PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation( - parameter_name='protocol_id') -SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id') +LOG = log.getLogger(__name__) -class FederationExtension(wsgi.V3ExtensionRouter): - """API Endpoints for the Federation extension. +class FederationExtension(wsgi.Middleware): - The API looks like:: - - PUT /OS-FEDERATION/identity_providers/{idp_id} - GET /OS-FEDERATION/identity_providers - GET /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 - GET /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 - PATCH /OS-FEDERATION/mappings/{mapping_id} - GET /OS-FEDERATION/mappings/{mapping_id} - DELETE /OS-FEDERATION/mappings/{mapping_id} - - GET /OS-FEDERATION/projects - GET /OS-FEDERATION/domains - - PUT /OS-FEDERATION/service_providers/{sp_id} - GET /OS-FEDERATION/service_providers - GET /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/{identity_provider}/ - protocols/{protocol}/auth - POST /OS-FEDERATION/identity_providers/{identity_provider}/ - protocols/{protocol}/auth - GET /auth/OS-FEDERATION/identity_providers/ - {idp_id}/protocols/{protocol_id}/websso - ?origin=https%3A//horizon.example.com - POST /auth/OS-FEDERATION/identity_providers/ - {idp_id}/protocols/{protocol_id}/websso - ?origin=https%3A//horizon.example.com - - - POST /auth/OS-FEDERATION/saml2 - POST /auth/OS-FEDERATION/saml2/ecp - GET /OS-FEDERATION/saml2/metadata - - GET /auth/OS-FEDERATION/websso/{protocol_id} - ?origin=https%3A//horizon.example.com - - POST /auth/OS-FEDERATION/websso/{protocol_id} - ?origin=https%3A//horizon.example.com - - """ - - def _construct_url(self, suffix): - return "/OS-FEDERATION/%s" % suffix - - def add_routes(self, mapper): - 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_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_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_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_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_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_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_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_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_action='list_domains_for_groups', - rel=build_resource_relation(resource_name='domains')) - self._add_resource( - mapper, project_controller, - path=self._construct_url('projects'), - new_path='/auth/projects', - get_action='list_projects_for_groups', - rel=build_resource_relation(resource_name='projects')) - - # Auth operations - self._add_resource( - mapper, auth_controller, - path=self._construct_url('identity_providers/{identity_provider}/' - 'protocols/{protocol}/auth'), - get_post_action='federated_authentication', - rel=build_resource_relation( - resource_name='identity_provider_protocol_auth'), - path_vars={ - 'identity_provider': IDP_ID_PARAMETER_RELATION, - 'protocol': PROTOCOL_ID_PARAMETER_RELATION, - }) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('saml2'), - post_action='create_saml_assertion', - rel=build_resource_relation(resource_name='saml2')) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('saml2/ecp'), - post_action='create_ecp_assertion', - rel=build_resource_relation(resource_name='ecp')) - self._add_resource( - mapper, auth_controller, - path='/auth' + self._construct_url('websso/{protocol_id}'), - get_post_action='federated_sso_auth', - rel=build_resource_relation(resource_name='websso'), - path_vars={ - 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, - }) - self._add_resource( - mapper, auth_controller, - 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'), - 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_action='get_metadata', - rel=build_resource_relation(resource_name='metadata')) + def __init__(self, *args, **kwargs): + super(FederationExtension, self).__init__(*args, **kwargs) + msg = _("Remove federation_extension from the paste pipeline, the " + "federation extension is now always available. Update the " + "[pipeline:api_v3] section in keystone-paste.ini accordingly, " + "as it will be removed in the O release.") + versionutils.report_deprecated_feature(LOG, msg) diff --git a/keystone/federation/__init__.py b/keystone/federation/__init__.py new file mode 100644 index 0000000000..be528a55b8 --- /dev/null +++ b/keystone/federation/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. + +from keystone.federation.core import * # noqa +from keystone.federation import routers # noqa diff --git a/keystone/federation/backends/__init__.py b/keystone/federation/backends/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/keystone/federation/backends/sql.py b/keystone/federation/backends/sql.py new file mode 100644 index 0000000000..851a91128d --- /dev/null +++ b/keystone/federation/backends/sql.py @@ -0,0 +1,366 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. + +from oslo_serialization import jsonutils +from sqlalchemy import orm + +from keystone.common import sql +from keystone import exception +from keystone.federation import core + + +class FederationProtocolModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'federation_protocol' + attributes = ['id', 'idp_id', 'mapping_id'] + mutable_attributes = frozenset(['mapping_id']) + + id = sql.Column(sql.String(64), primary_key=True) + idp_id = sql.Column(sql.String(64), sql.ForeignKey('identity_provider.id', + ondelete='CASCADE'), primary_key=True) + mapping_id = sql.Column(sql.String(64), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class IdentityProviderModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'identity_provider' + attributes = ['id', 'enabled', 'description', 'remote_ids'] + mutable_attributes = frozenset(['description', 'enabled', 'remote_ids']) + + id = sql.Column(sql.String(64), primary_key=True) + enabled = sql.Column(sql.Boolean, nullable=False) + description = sql.Column(sql.Text(), nullable=True) + remote_ids = orm.relationship('IdPRemoteIdsModel', + order_by='IdPRemoteIdsModel.remote_id', + cascade='all, delete-orphan') + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + remote_ids_list = new_dictionary.pop('remote_ids', None) + if not remote_ids_list: + remote_ids_list = [] + identity_provider = cls(**new_dictionary) + remote_ids = [] + # NOTE(fmarco76): the remote_ids_list contains only remote ids + # associated with the IdP because of the "relationship" established in + # sqlalchemy and corresponding to the FK in the idp_remote_ids table + for remote in remote_ids_list: + remote_ids.append(IdPRemoteIdsModel(remote_id=remote)) + identity_provider.remote_ids = remote_ids + return identity_provider + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + d['remote_ids'] = [] + for remote in self.remote_ids: + d['remote_ids'].append(remote.remote_id) + return d + + +class IdPRemoteIdsModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'idp_remote_ids' + attributes = ['idp_id', 'remote_id'] + mutable_attributes = frozenset(['idp_id', 'remote_id']) + + idp_id = sql.Column(sql.String(64), + sql.ForeignKey('identity_provider.id', + ondelete='CASCADE')) + remote_id = sql.Column(sql.String(255), + primary_key=True) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class MappingModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'mapping' + attributes = ['id', 'rules'] + + id = sql.Column(sql.String(64), primary_key=True) + rules = sql.Column(sql.JsonBlob(), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + new_dictionary['rules'] = jsonutils.dumps(new_dictionary['rules']) + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + d['rules'] = jsonutils.loads(d['rules']) + return d + + +class ServiceProviderModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'service_provider' + attributes = ['auth_url', 'id', 'enabled', 'description', + 'relay_state_prefix', 'sp_url'] + mutable_attributes = frozenset(['auth_url', 'description', 'enabled', + 'relay_state_prefix', 'sp_url']) + + id = sql.Column(sql.String(64), primary_key=True) + enabled = sql.Column(sql.Boolean, nullable=False) + description = sql.Column(sql.Text(), nullable=True) + auth_url = sql.Column(sql.String(256), nullable=False) + sp_url = sql.Column(sql.String(256), nullable=False) + relay_state_prefix = sql.Column(sql.String(256), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class Federation(core.FederationDriverV8): + + # Identity Provider CRUD + @sql.handle_conflicts(conflict_type='identity_provider') + def create_idp(self, idp_id, idp): + idp['id'] = idp_id + with sql.transaction() as session: + idp_ref = IdentityProviderModel.from_dict(idp) + session.add(idp_ref) + return idp_ref.to_dict() + + def delete_idp(self, idp_id): + with sql.transaction() as session: + self._delete_assigned_protocols(session, idp_id) + idp_ref = self._get_idp(session, idp_id) + session.delete(idp_ref) + + def _get_idp(self, session, idp_id): + idp_ref = session.query(IdentityProviderModel).get(idp_id) + if not idp_ref: + raise exception.IdentityProviderNotFound(idp_id=idp_id) + return idp_ref + + def _get_idp_from_remote_id(self, session, remote_id): + q = session.query(IdPRemoteIdsModel) + q = q.filter_by(remote_id=remote_id) + try: + return q.one() + except sql.NotFound: + raise exception.IdentityProviderNotFound(idp_id=remote_id) + + def list_idps(self): + with sql.transaction() as session: + idps = session.query(IdentityProviderModel) + idps_list = [idp.to_dict() for idp in idps] + return idps_list + + def get_idp(self, idp_id): + with sql.transaction() as session: + idp_ref = self._get_idp(session, idp_id) + return idp_ref.to_dict() + + def get_idp_from_remote_id(self, remote_id): + with sql.transaction() as session: + ref = self._get_idp_from_remote_id(session, remote_id) + return ref.to_dict() + + def update_idp(self, idp_id, idp): + with sql.transaction() as session: + idp_ref = self._get_idp(session, idp_id) + old_idp = idp_ref.to_dict() + old_idp.update(idp) + new_idp = IdentityProviderModel.from_dict(old_idp) + for attr in IdentityProviderModel.mutable_attributes: + setattr(idp_ref, attr, getattr(new_idp, attr)) + return idp_ref.to_dict() + + # Protocol CRUD + def _get_protocol(self, session, idp_id, protocol_id): + q = session.query(FederationProtocolModel) + q = q.filter_by(id=protocol_id, idp_id=idp_id) + try: + return q.one() + except sql.NotFound: + kwargs = {'protocol_id': protocol_id, + 'idp_id': idp_id} + raise exception.FederatedProtocolNotFound(**kwargs) + + @sql.handle_conflicts(conflict_type='federation_protocol') + def create_protocol(self, idp_id, protocol_id, protocol): + protocol['id'] = protocol_id + protocol['idp_id'] = idp_id + with sql.transaction() as session: + self._get_idp(session, idp_id) + protocol_ref = FederationProtocolModel.from_dict(protocol) + session.add(protocol_ref) + return protocol_ref.to_dict() + + def update_protocol(self, idp_id, protocol_id, protocol): + with sql.transaction() as session: + proto_ref = self._get_protocol(session, idp_id, protocol_id) + old_proto = proto_ref.to_dict() + old_proto.update(protocol) + new_proto = FederationProtocolModel.from_dict(old_proto) + for attr in FederationProtocolModel.mutable_attributes: + setattr(proto_ref, attr, getattr(new_proto, attr)) + return proto_ref.to_dict() + + def get_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + protocol_ref = self._get_protocol(session, idp_id, protocol_id) + return protocol_ref.to_dict() + + def list_protocols(self, idp_id): + with sql.transaction() as session: + q = session.query(FederationProtocolModel) + q = q.filter_by(idp_id=idp_id) + protocols = [protocol.to_dict() for protocol in q] + return protocols + + def delete_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + key_ref = self._get_protocol(session, idp_id, protocol_id) + session.delete(key_ref) + + def _delete_assigned_protocols(self, session, idp_id): + query = session.query(FederationProtocolModel) + query = query.filter_by(idp_id=idp_id) + query.delete() + + # Mapping CRUD + def _get_mapping(self, session, mapping_id): + mapping_ref = session.query(MappingModel).get(mapping_id) + if not mapping_ref: + raise exception.MappingNotFound(mapping_id=mapping_id) + return mapping_ref + + @sql.handle_conflicts(conflict_type='mapping') + def create_mapping(self, mapping_id, mapping): + ref = {} + ref['id'] = mapping_id + ref['rules'] = mapping.get('rules') + with sql.transaction() as session: + mapping_ref = MappingModel.from_dict(ref) + session.add(mapping_ref) + return mapping_ref.to_dict() + + def delete_mapping(self, mapping_id): + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + session.delete(mapping_ref) + + def list_mappings(self): + with sql.transaction() as session: + mappings = session.query(MappingModel) + return [x.to_dict() for x in mappings] + + def get_mapping(self, mapping_id): + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + return mapping_ref.to_dict() + + @sql.handle_conflicts(conflict_type='mapping') + def update_mapping(self, mapping_id, mapping): + ref = {} + ref['id'] = mapping_id + ref['rules'] = mapping.get('rules') + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + old_mapping = mapping_ref.to_dict() + old_mapping.update(ref) + new_mapping = MappingModel.from_dict(old_mapping) + for attr in MappingModel.attributes: + setattr(mapping_ref, attr, getattr(new_mapping, attr)) + return mapping_ref.to_dict() + + def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + protocol_ref = self._get_protocol(session, idp_id, protocol_id) + mapping_id = protocol_ref.mapping_id + mapping_ref = self._get_mapping(session, mapping_id) + return mapping_ref.to_dict() + + # Service Provider CRUD + @sql.handle_conflicts(conflict_type='service_provider') + def create_sp(self, sp_id, sp): + sp['id'] = sp_id + with sql.transaction() as session: + sp_ref = ServiceProviderModel.from_dict(sp) + session.add(sp_ref) + return sp_ref.to_dict() + + def delete_sp(self, sp_id): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + session.delete(sp_ref) + + def _get_sp(self, session, sp_id): + sp_ref = session.query(ServiceProviderModel).get(sp_id) + if not sp_ref: + raise exception.ServiceProviderNotFound(sp_id=sp_id) + return sp_ref + + def list_sps(self): + with sql.transaction() as session: + sps = session.query(ServiceProviderModel) + sps_list = [sp.to_dict() for sp in sps] + return sps_list + + def get_sp(self, sp_id): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + return sp_ref.to_dict() + + def update_sp(self, sp_id, sp): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + old_sp = sp_ref.to_dict() + old_sp.update(sp) + new_sp = ServiceProviderModel.from_dict(old_sp) + for attr in ServiceProviderModel.mutable_attributes: + setattr(sp_ref, attr, getattr(new_sp, attr)) + return sp_ref.to_dict() + + def get_enabled_service_providers(self): + with sql.transaction() as session: + service_providers = session.query(ServiceProviderModel) + service_providers = service_providers.filter_by(enabled=True) + return service_providers diff --git a/keystone/contrib/federation/constants.py b/keystone/federation/constants.py similarity index 100% rename from keystone/contrib/federation/constants.py rename to keystone/federation/constants.py diff --git a/keystone/contrib/federation/controllers.py b/keystone/federation/controllers.py similarity index 99% rename from keystone/contrib/federation/controllers.py rename to keystone/federation/controllers.py index 278a1f4d3c..7492cbfd25 100644 --- a/keystone/contrib/federation/controllers.py +++ b/keystone/federation/controllers.py @@ -26,10 +26,10 @@ from keystone.common import controller from keystone.common import dependency from keystone.common import validation from keystone.common import wsgi -from keystone.contrib.federation import idp as keystone_idp -from keystone.contrib.federation import schema -from keystone.contrib.federation import utils from keystone import exception +from keystone.federation import idp as keystone_idp +from keystone.federation import schema +from keystone.federation import utils from keystone.i18n import _ from keystone.models import token_model diff --git a/keystone/contrib/federation/core.py b/keystone/federation/core.py similarity index 99% rename from keystone/contrib/federation/core.py rename to keystone/federation/core.py index 1595be1d9c..e3d5a309b2 100644 --- a/keystone/contrib/federation/core.py +++ b/keystone/federation/core.py @@ -21,8 +21,8 @@ import six from keystone.common import dependency from keystone.common import extension from keystone.common import manager -from keystone.contrib.federation import utils from keystone import exception +from keystone.federation import utils CONF = cfg.CONF diff --git a/keystone/contrib/federation/idp.py b/keystone/federation/idp.py similarity index 100% rename from keystone/contrib/federation/idp.py rename to keystone/federation/idp.py diff --git a/keystone/federation/routers.py b/keystone/federation/routers.py new file mode 100644 index 0000000000..0fe1b41ad7 --- /dev/null +++ b/keystone/federation/routers.py @@ -0,0 +1,252 @@ +# 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. + +import functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.federation import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-FEDERATION', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-FEDERATION', extension_version='1.0') + +IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id') +PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation( + parameter_name='protocol_id') +SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id') + + +class Routers(wsgi.RoutersBase): + """API Endpoints for the Federation extension. + + The API looks like:: + + PUT /OS-FEDERATION/identity_providers/{idp_id} + GET /OS-FEDERATION/identity_providers + GET /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 + GET /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 + PATCH /OS-FEDERATION/mappings/{mapping_id} + GET /OS-FEDERATION/mappings/{mapping_id} + DELETE /OS-FEDERATION/mappings/{mapping_id} + + GET /OS-FEDERATION/projects + GET /OS-FEDERATION/domains + + PUT /OS-FEDERATION/service_providers/{sp_id} + GET /OS-FEDERATION/service_providers + GET /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/{identity_provider}/ + protocols/{protocol}/auth + POST /OS-FEDERATION/identity_providers/{identity_provider}/ + protocols/{protocol}/auth + GET /auth/OS-FEDERATION/identity_providers/ + {idp_id}/protocols/{protocol_id}/websso + ?origin=https%3A//horizon.example.com + POST /auth/OS-FEDERATION/identity_providers/ + {idp_id}/protocols/{protocol_id}/websso + ?origin=https%3A//horizon.example.com + + + POST /auth/OS-FEDERATION/saml2 + POST /auth/OS-FEDERATION/saml2/ecp + GET /OS-FEDERATION/saml2/metadata + + GET /auth/OS-FEDERATION/websso/{protocol_id} + ?origin=https%3A//horizon.example.com + + POST /auth/OS-FEDERATION/websso/{protocol_id} + ?origin=https%3A//horizon.example.com + + """ + + 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_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_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_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_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_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_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_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_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_action='list_domains_for_groups', + rel=build_resource_relation(resource_name='domains')) + self._add_resource( + mapper, project_controller, + path=self._construct_url('projects'), + new_path='/auth/projects', + get_action='list_projects_for_groups', + rel=build_resource_relation(resource_name='projects')) + + # Auth operations + self._add_resource( + mapper, auth_controller, + path=self._construct_url('identity_providers/{identity_provider}/' + 'protocols/{protocol}/auth'), + get_post_action='federated_authentication', + rel=build_resource_relation( + resource_name='identity_provider_protocol_auth'), + path_vars={ + 'identity_provider': IDP_ID_PARAMETER_RELATION, + 'protocol': PROTOCOL_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, auth_controller, + path='/auth' + self._construct_url('saml2'), + post_action='create_saml_assertion', + rel=build_resource_relation(resource_name='saml2')) + self._add_resource( + mapper, auth_controller, + path='/auth' + self._construct_url('saml2/ecp'), + post_action='create_ecp_assertion', + rel=build_resource_relation(resource_name='ecp')) + self._add_resource( + mapper, auth_controller, + path='/auth' + self._construct_url('websso/{protocol_id}'), + get_post_action='federated_sso_auth', + rel=build_resource_relation(resource_name='websso'), + path_vars={ + 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, auth_controller, + 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'), + 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_action='get_metadata', + rel=build_resource_relation(resource_name='metadata')) diff --git a/keystone/contrib/federation/schema.py b/keystone/federation/schema.py similarity index 100% rename from keystone/contrib/federation/schema.py rename to keystone/federation/schema.py diff --git a/keystone/contrib/federation/utils.py b/keystone/federation/utils.py similarity index 99% rename from keystone/contrib/federation/utils.py rename to keystone/federation/utils.py index a71bd2c461..b9b89e6c51 100644 --- a/keystone/contrib/federation/utils.py +++ b/keystone/federation/utils.py @@ -581,7 +581,7 @@ class RuleProcessor(object): :param local: local mapping reference that needs to be updated :type local: dict :param direct_maps: identity values used to update local - :type direct_maps: keystone.contrib.federation.utils.DirectMaps + :type direct_maps: keystone.federation.utils.DirectMaps Example local:: @@ -659,7 +659,7 @@ class RuleProcessor(object): } :returns: identity values used to update local - :rtype: keystone.contrib.federation.utils.DirectMaps or None + :rtype: keystone.federation.utils.DirectMaps or None """ direct_maps = DirectMaps() diff --git a/keystone/middleware/auth.py b/keystone/middleware/auth.py index 5a61158f8f..d3fc50f82a 100644 --- a/keystone/middleware/auth.py +++ b/keystone/middleware/auth.py @@ -17,9 +17,9 @@ from oslo_log import log from keystone.common import authorization from keystone.common import tokenless_auth from keystone.common import wsgi -from keystone.contrib.federation import constants as federation_constants -from keystone.contrib.federation import utils from keystone import exception +from keystone.federation import constants as federation_constants +from keystone.federation import utils from keystone.i18n import _, _LI, _LW from keystone.middleware import core from keystone.models import token_model diff --git a/keystone/models/token_model.py b/keystone/models/token_model.py index 45e998ded6..aa273c646b 100644 --- a/keystone/models/token_model.py +++ b/keystone/models/token_model.py @@ -17,11 +17,11 @@ from oslo_config import cfg from oslo_utils import timeutils import six -from keystone.contrib.federation import constants as federation_constants from keystone import exception from keystone.i18n import _ - +# FIXME(stevemar): Use constants from keystone.federation.constants +OS_FEDERATION = 'OS-FEDERATION' CONF = cfg.CONF # supported token versions V2 = 'v2.0' @@ -297,8 +297,7 @@ class KeystoneToken(dict): @property def is_federated_user(self): try: - return (self.version is V3 and - federation_constants.FEDERATION in self['user']) + return (self.version is V3 and OS_FEDERATION in self['user']) except KeyError: raise exception.UnexpectedError() @@ -307,8 +306,7 @@ class KeystoneToken(dict): if self.is_federated_user: if self.version is V3: try: - groups = self['user'][federation_constants.FEDERATION].get( - 'groups', []) + groups = self['user'][OS_FEDERATION].get('groups', []) return [g['id'] for g in groups] except KeyError: raise exception.UnexpectedError() @@ -318,15 +316,12 @@ class KeystoneToken(dict): def federation_idp_id(self): if self.version is not V3 or not self.is_federated_user: return None - return ( - self['user'][federation_constants.FEDERATION] - ['identity_provider']['id']) + return self['user'][OS_FEDERATION]['identity_provider']['id'] @property def federation_protocol_id(self): if self.version is V3 and self.is_federated_user: - return (self['user'][federation_constants.FEDERATION]['protocol'] - ['id']) + return self['user'][OS_FEDERATION]['protocol']['id'] return None @property diff --git a/keystone/server/backends.py b/keystone/server/backends.py index 511b805739..a2dc347c0f 100644 --- a/keystone/server/backends.py +++ b/keystone/server/backends.py @@ -15,11 +15,11 @@ from keystone import auth from keystone import catalog from keystone.common import cache from keystone.contrib import endpoint_filter -from keystone.contrib import federation from keystone.contrib import oauth1 from keystone.contrib import revoke from keystone import credential from keystone import endpoint_policy +from keystone import federation from keystone import identity from keystone import policy from keystone import resource diff --git a/keystone/tests/unit/common/test_authorization.py b/keystone/tests/unit/common/test_authorization.py index 190ffc2ed9..349fa53b19 100644 --- a/keystone/tests/unit/common/test_authorization.py +++ b/keystone/tests/unit/common/test_authorization.py @@ -17,8 +17,8 @@ import copy import uuid from keystone.common import authorization -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.models import token_model from keystone.tests import unit from keystone.tests.unit import test_token_provider diff --git a/keystone/tests/unit/contrib/federation/test_utils.py b/keystone/tests/unit/contrib/federation/test_utils.py index 20970f542a..dbd09f2a83 100644 --- a/keystone/tests/unit/contrib/federation/test_utils.py +++ b/keystone/tests/unit/contrib/federation/test_utils.py @@ -13,8 +13,8 @@ import uuid from keystone.auth.plugins import mapped -from keystone.contrib.federation import utils as mapping_utils from keystone import exception +from keystone.federation import utils as mapping_utils from keystone.tests import unit from keystone.tests.unit import mapping_fixtures diff --git a/keystone/tests/unit/test_middleware.py b/keystone/tests/unit/test_middleware.py index b28744fff6..97ff305e46 100644 --- a/keystone/tests/unit/test_middleware.py +++ b/keystone/tests/unit/test_middleware.py @@ -22,8 +22,8 @@ import webob from keystone.common import authorization from keystone.common import tokenless_auth -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone import middleware from keystone.tests import unit from keystone.tests.unit import mapping_fixtures diff --git a/keystone/tests/unit/test_v3_federation.py b/keystone/tests/unit/test_v3_federation.py index 598744a38d..25932ddd1f 100644 --- a/keystone/tests/unit/test_v3_federation.py +++ b/keystone/tests/unit/test_v3_federation.py @@ -20,6 +20,7 @@ from lxml import etree import mock from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_utils import importutils from oslotest import mockpatch import saml2 @@ -33,9 +34,10 @@ if not xmldsig: from keystone.auth import controllers as auth_controllers from keystone.common import environment -from keystone.contrib.federation import controllers as federation_controllers -from keystone.contrib.federation import idp as keystone_idp +from keystone.contrib.federation import routers from keystone import exception +from keystone.federation import controllers as federation_controllers +from keystone.federation import idp as keystone_idp from keystone import notifications from keystone.tests import unit from keystone.tests.unit import core @@ -60,8 +62,12 @@ def dummy_validator(*args, **kwargs): class FederationTests(test_v3.RestfulTestCase): - EXTENSION_NAME = 'federation' - EXTENSION_TO_ADD = 'federation_extension' + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.FederationExtension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove federation_extension from", args[1]) class FederatedSetupMixin(object): @@ -770,7 +776,7 @@ class FederatedSetupMixin(object): self.domainC['id']) -class FederatedIdentityProviderTests(FederationTests): +class FederatedIdentityProviderTests(test_v3.RestfulTestCase): """A test class for Identity Providers.""" idp_keys = ['description', 'enabled'] @@ -1298,7 +1304,7 @@ class FederatedIdentityProviderTests(FederationTests): self.get(url, expected_status=http_client.NOT_FOUND) -class MappingCRUDTests(FederationTests): +class MappingCRUDTests(test_v3.RestfulTestCase): """A class for testing CRUD operations for Mappings.""" MAPPING_URL = '/OS-FEDERATION/mappings/' @@ -1465,7 +1471,7 @@ class MappingCRUDTests(FederationTests): body={'mapping': mapping}) -class FederatedTokenTests(FederationTests, FederatedSetupMixin): +class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def auth_plugin_config_override(self): methods = ['saml2'] @@ -1502,7 +1508,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.assertTrue(note['send_notification_called']) def load_fixtures(self, fixtures): - super(FederationTests, self).load_fixtures(fixtures) + super(FederatedTokenTests, self).load_fixtures(fixtures) self.load_federation_sample_data() def test_issue_unscoped_token_notify(self): @@ -2367,7 +2373,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): assertion='ANOTHER_LOCAL_USER_ASSERTION') -class FernetFederatedTokenTests(FederationTests, FederatedSetupMixin): +class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): AUTH_METHOD = 'token' def load_fixtures(self, fixtures): @@ -2441,7 +2447,7 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests): self).auth_plugin_config_override(methods) -class JsonHomeTests(FederationTests, test_v3.JsonHomeTestMixin): +class JsonHomeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): JSON_HOME_DATA = { 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-FEDERATION/' '1.0/rel/identity_provider': { @@ -2469,7 +2475,7 @@ def _load_xml(filename): return xml.read() -class SAMLGenerationTests(FederationTests): +class SAMLGenerationTests(test_v3.RestfulTestCase): SP_AUTH_URL = ('http://beta.com:5000/v3/OS-FEDERATION/identity_providers' '/BETA/protocols/saml2/auth') @@ -2936,7 +2942,7 @@ class SAMLGenerationTests(FederationTests): self.assertEqual(expected_log, logger_fixture.output) -class IdPMetadataGenerationTests(FederationTests): +class IdPMetadataGenerationTests(test_v3.RestfulTestCase): """A class for testing Identity Provider Metadata generation.""" METADATA_URL = '/OS-FEDERATION/saml2/metadata' @@ -3066,7 +3072,7 @@ class IdPMetadataGenerationTests(FederationTests): self.assertEqual(reference_file, r.result) -class ServiceProviderTests(FederationTests): +class ServiceProviderTests(test_v3.RestfulTestCase): """A test class for Service Providers.""" MEMBER_NAME = 'service_provider' @@ -3076,7 +3082,7 @@ class ServiceProviderTests(FederationTests): 'relay_state_prefix', 'sp_url'] def setUp(self): - super(FederationTests, self).setUp() + super(ServiceProviderTests, self).setUp() # Add a Service Provider url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) self.SP_REF = self.sp_ref() @@ -3359,7 +3365,7 @@ class WebSSOTests(FederatedTokenTests): self.assertIn(self.TRUSTED_DASHBOARD, resp.body) -class K2KServiceCatalogTests(FederationTests): +class K2KServiceCatalogTests(test_v3.RestfulTestCase): SP1 = 'SP1' SP2 = 'SP2' SP3 = 'SP3' diff --git a/keystone/tests/unit/test_validation.py b/keystone/tests/unit/test_validation.py index 93d20fd007..c478ca1f91 100644 --- a/keystone/tests/unit/test_validation.py +++ b/keystone/tests/unit/test_validation.py @@ -22,9 +22,9 @@ from keystone.common import validation from keystone.common.validation import parameter_types from keystone.common.validation import validators from keystone.contrib.endpoint_filter import schema as endpoint_filter_schema -from keystone.contrib.federation import schema as federation_schema from keystone.credential import schema as credential_schema from keystone import exception +from keystone.federation import schema as federation_schema from keystone.identity import schema as identity_schema from keystone.policy import schema as policy_schema from keystone.resource import schema as resource_schema diff --git a/keystone/tests/unit/token/test_fernet_provider.py b/keystone/tests/unit/token/test_fernet_provider.py index e05608cea4..0b3ec65880 100644 --- a/keystone/tests/unit/token/test_fernet_provider.py +++ b/keystone/tests/unit/token/test_fernet_provider.py @@ -22,8 +22,8 @@ from six.moves import urllib from keystone.common import config from keystone.common import utils -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.tests import unit from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import database diff --git a/keystone/tests/unit/token/test_token_model.py b/keystone/tests/unit/token/test_token_model.py index f139849156..1cb0ef553c 100644 --- a/keystone/tests/unit/token/test_token_model.py +++ b/keystone/tests/unit/token/test_token_model.py @@ -17,8 +17,8 @@ from oslo_config import cfg from oslo_utils import timeutils from six.moves import range -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.models import token_model from keystone.tests.unit import core from keystone.tests.unit import test_token_provider diff --git a/keystone/token/providers/common.py b/keystone/token/providers/common.py index a41f7ec5af..fb064d0627 100644 --- a/keystone/token/providers/common.py +++ b/keystone/token/providers/common.py @@ -22,8 +22,8 @@ from six.moves.urllib import parse from keystone.common import controller as common_controller from keystone.common import dependency from keystone.common import utils -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.i18n import _, _LE from keystone import token from keystone.token import provider diff --git a/keystone/token/providers/fernet/core.py b/keystone/token/providers/fernet/core.py index 31e71a4516..1009966d41 100644 --- a/keystone/token/providers/fernet/core.py +++ b/keystone/token/providers/fernet/core.py @@ -15,8 +15,8 @@ from oslo_log import log from keystone.common import dependency from keystone.common import utils as ks_utils -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.i18n import _ from keystone.token import provider from keystone.token.providers import common diff --git a/keystone/version/service.py b/keystone/version/service.py index 2ae900a4b6..c485af2c81 100644 --- a/keystone/version/service.py +++ b/keystone/version/service.py @@ -26,6 +26,7 @@ from keystone import catalog from keystone.common import wsgi from keystone import credential from keystone import endpoint_policy +from keystone import federation from keystone.i18n import _LW from keystone import identity from keystone import policy @@ -129,7 +130,8 @@ def v3_app_factory(global_conf, **local_conf): credential, identity, policy, - resource] + resource, + federation] if CONF.trust.enabled: router_modules.append(trust) diff --git a/setup.cfg b/setup.cfg index dde0ea49dc..b2b1b6ac82 100644 --- a/setup.cfg +++ b/setup.cfg @@ -162,7 +162,7 @@ keystone.endpoint_policy = sql = keystone.endpoint_policy.backends.sql:EndpointPolicy keystone.federation = - sql = keystone.contrib.federation.backends.sql:Federation + sql = keystone.federation.backends.sql:Federation keystone.oauth1 = sql = keystone.contrib.oauth1.backends.sql:OAuth1