diff options
author | RHE <rebirthmonkey@gmail.com> | 2017-11-24 13:54:26 +0100 |
---|---|---|
committer | RHE <rebirthmonkey@gmail.com> | 2017-11-24 13:54:26 +0100 |
commit | 920a49cfa055733d575282973e23558c33087a4a (patch) | |
tree | d371dab34efa5028600dad2e7ca58063626e7ba4 /keystone-moon/keystone/federation | |
parent | ef3eefca70d8abb4a00dafb9419ad32738e934b2 (diff) |
remove keystone-moon
Change-Id: I80d7c9b669f19d5f6607e162de8e0e55c2f80fdd
Signed-off-by: RHE <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/federation')
-rw-r--r-- | keystone-moon/keystone/federation/V8_backends/__init__.py | 0 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/V8_backends/sql.py | 389 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/__init__.py | 15 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/backends/__init__.py | 0 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/backends/sql.py | 393 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/constants.py | 15 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/controllers.py | 519 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/core.py | 611 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/idp.py | 615 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/routers.py | 252 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/schema.py | 115 | ||||
-rw-r--r-- | keystone-moon/keystone/federation/utils.py | 872 |
12 files changed, 0 insertions, 3796 deletions
diff --git a/keystone-moon/keystone/federation/V8_backends/__init__.py b/keystone-moon/keystone/federation/V8_backends/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/federation/V8_backends/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/federation/V8_backends/sql.py b/keystone-moon/keystone/federation/V8_backends/sql.py deleted file mode 100644 index d6b42aa0..00000000 --- a/keystone-moon/keystone/federation/V8_backends/sql.py +++ /dev/null @@ -1,389 +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 oslo_log import log -from oslo_serialization import jsonutils -import six -from sqlalchemy import orm - -from keystone.common import sql -from keystone import exception -from keystone.federation import core -from keystone.i18n import _ - - -LOG = log.getLogger(__name__) - - -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): - - _CONFLICT_LOG_MSG = 'Conflict %(conflict_type)s: %(details)s' - - def _handle_idp_conflict(self, e): - conflict_type = 'identity_provider' - details = six.text_type(e) - LOG.debug(self._CONFLICT_LOG_MSG, {'conflict_type': conflict_type, - 'details': details}) - if 'remote_id' in details: - msg = _('Duplicate remote ID: %s') - else: - msg = _('Duplicate entry: %s') - msg = msg % e.value - raise exception.Conflict(type=conflict_type, details=msg) - - # Identity Provider CRUD - @sql.handle_conflicts(conflict_type='identity_provider') - def create_idp(self, idp_id, idp): - idp['id'] = idp_id - with sql.session_for_write() 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.session_for_write() 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.session_for_read() 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.session_for_read() 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.session_for_read() as session: - ref = self._get_idp_from_remote_id(session, remote_id) - return ref.to_dict() - - def update_idp(self, idp_id, idp): - try: - with sql.session_for_write() 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() - except sql.DBDuplicateEntry as e: - self._handle_idp_conflict(e) - - # 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.session_for_write() 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.session_for_write() 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.session_for_read() 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.session_for_read() 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.session_for_write() 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.session_for_write() 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.session_for_write() as session: - mapping_ref = self._get_mapping(session, mapping_id) - session.delete(mapping_ref) - - def list_mappings(self): - with sql.session_for_read() as session: - mappings = session.query(MappingModel) - return [x.to_dict() for x in mappings] - - def get_mapping(self, mapping_id): - with sql.session_for_read() 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.session_for_write() 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.session_for_read() 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.session_for_write() 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.session_for_write() 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.session_for_read() 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.session_for_read() as session: - sp_ref = self._get_sp(session, sp_id) - return sp_ref.to_dict() - - def update_sp(self, sp_id, sp): - with sql.session_for_write() 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.session_for_read() as session: - service_providers = session.query(ServiceProviderModel) - service_providers = service_providers.filter_by(enabled=True) - return service_providers diff --git a/keystone-moon/keystone/federation/__init__.py b/keystone-moon/keystone/federation/__init__.py deleted file mode 100644 index b62cfb6f..00000000 --- a/keystone-moon/keystone/federation/__init__.py +++ /dev/null @@ -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.federation.core import * # noqa diff --git a/keystone-moon/keystone/federation/backends/__init__.py b/keystone-moon/keystone/federation/backends/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/keystone-moon/keystone/federation/backends/__init__.py +++ /dev/null diff --git a/keystone-moon/keystone/federation/backends/sql.py b/keystone-moon/keystone/federation/backends/sql.py deleted file mode 100644 index add409e6..00000000 --- a/keystone-moon/keystone/federation/backends/sql.py +++ /dev/null @@ -1,393 +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 oslo_log import log -from oslo_serialization import jsonutils -import six -from sqlalchemy import orm - -from keystone.common import sql -from keystone import exception -from keystone.federation import core -from keystone.i18n import _ - - -LOG = log.getLogger(__name__) - - -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.FederationDriverV9): - - _CONFLICT_LOG_MSG = 'Conflict %(conflict_type)s: %(details)s' - - def _handle_idp_conflict(self, e): - conflict_type = 'identity_provider' - details = six.text_type(e) - LOG.debug(self._CONFLICT_LOG_MSG, {'conflict_type': conflict_type, - 'details': details}) - if 'remote_id' in details: - msg = _('Duplicate remote ID: %s') - else: - msg = _('Duplicate entry: %s') - msg = msg % e.value - raise exception.Conflict(type=conflict_type, details=msg) - - # Identity Provider CRUD - def create_idp(self, idp_id, idp): - idp['id'] = idp_id - try: - with sql.session_for_write() as session: - idp_ref = IdentityProviderModel.from_dict(idp) - session.add(idp_ref) - return idp_ref.to_dict() - except sql.DBDuplicateEntry as e: - self._handle_idp_conflict(e) - - def delete_idp(self, idp_id): - with sql.session_for_write() 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, hints=None): - with sql.session_for_read() as session: - query = session.query(IdentityProviderModel) - idps = sql.filter_limit_query(IdentityProviderModel, query, hints) - idps_list = [idp.to_dict() for idp in idps] - return idps_list - - def get_idp(self, idp_id): - with sql.session_for_read() 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.session_for_read() as session: - ref = self._get_idp_from_remote_id(session, remote_id) - return ref.to_dict() - - def update_idp(self, idp_id, idp): - try: - with sql.session_for_write() 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() - except sql.DBDuplicateEntry as e: - self._handle_idp_conflict(e) - - # 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.session_for_write() 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.session_for_write() 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.session_for_read() 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.session_for_read() 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.session_for_write() 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.session_for_write() 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.session_for_write() as session: - mapping_ref = self._get_mapping(session, mapping_id) - session.delete(mapping_ref) - - def list_mappings(self): - with sql.session_for_read() as session: - mappings = session.query(MappingModel) - return [x.to_dict() for x in mappings] - - def get_mapping(self, mapping_id): - with sql.session_for_read() 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.session_for_write() 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.session_for_read() 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.session_for_write() 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.session_for_write() 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, hints=None): - with sql.session_for_read() as session: - query = session.query(ServiceProviderModel) - sps = sql.filter_limit_query(ServiceProviderModel, query, hints) - sps_list = [sp.to_dict() for sp in sps] - return sps_list - - def get_sp(self, sp_id): - with sql.session_for_read() as session: - sp_ref = self._get_sp(session, sp_id) - return sp_ref.to_dict() - - def update_sp(self, sp_id, sp): - with sql.session_for_write() 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.session_for_read() as session: - service_providers = session.query(ServiceProviderModel) - service_providers = service_providers.filter_by(enabled=True) - return service_providers diff --git a/keystone-moon/keystone/federation/constants.py b/keystone-moon/keystone/federation/constants.py deleted file mode 100644 index afb38494..00000000 --- a/keystone-moon/keystone/federation/constants.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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. - -FEDERATION = 'OS-FEDERATION' -IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider' -PROTOCOL = 'OS-FEDERATION:protocol' diff --git a/keystone-moon/keystone/federation/controllers.py b/keystone-moon/keystone/federation/controllers.py deleted file mode 100644 index b9e2d883..00000000 --- a/keystone-moon/keystone/federation/controllers.py +++ /dev/null @@ -1,519 +0,0 @@ -# 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. - -"""Workflow logic for the Federation service.""" - -import string - -from oslo_config import cfg -from oslo_log import log -import six -from six.moves import urllib -import webob - -from keystone.auth import controllers as auth_controllers -from keystone.common import authorization -from keystone.common import controller -from keystone.common import dependency -from keystone.common import utils as k_utils -from keystone.common import validation -from keystone.common import wsgi -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 - - -CONF = cfg.CONF -LOG = log.getLogger(__name__) - - -class _ControllerBase(controller.V3Controller): - """Base behaviors for federation controllers.""" - - @classmethod - def base_url(cls, context, path=None): - """Construct a path and pass it to V3Controller.base_url method.""" - path = '/OS-FEDERATION/' + cls.collection_name - return super(_ControllerBase, cls).base_url(context, path=path) - - -@dependency.requires('federation_api') -class IdentityProvider(_ControllerBase): - """Identity Provider representation.""" - - collection_name = 'identity_providers' - member_name = 'identity_provider' - - _public_parameters = frozenset(['id', 'enabled', 'description', - 'remote_ids', 'links' - ]) - - @classmethod - def _add_related_links(cls, context, ref): - """Add URLs for entities related with Identity Provider. - - Add URLs pointing to: - - protocols tied to the Identity Provider - - """ - ref.setdefault('links', {}) - base_path = ref['links'].get('self') - if base_path is None: - base_path = '/'.join([IdentityProvider.base_url(context), - ref['id']]) - for name in ['protocols']: - ref['links'][name] = '/'.join([base_path, name]) - - @classmethod - def _add_self_referential_link(cls, context, ref): - id = ref['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() - @validation.validated(schema.identity_provider_create, 'identity_provider') - def create_identity_provider(self, context, idp_id, identity_provider): - identity_provider = self._normalize_dict(identity_provider) - identity_provider.setdefault('enabled', False) - idp_ref = self.federation_api.create_idp(idp_id, identity_provider) - response = IdentityProvider.wrap_member(context, idp_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.filterprotected('id', 'enabled') - def list_identity_providers(self, context, filters): - hints = self.build_driver_hints(context, filters) - ref = self.federation_api.list_idps(hints=hints) - ref = [self.filter_params(x) for x in ref] - return IdentityProvider.wrap_collection(context, ref, hints=hints) - - @controller.protected() - def get_identity_provider(self, context, idp_id): - ref = self.federation_api.get_idp(idp_id) - return IdentityProvider.wrap_member(context, ref) - - @controller.protected() - def delete_identity_provider(self, context, idp_id): - self.federation_api.delete_idp(idp_id) - - @controller.protected() - @validation.validated(schema.identity_provider_update, 'identity_provider') - def update_identity_provider(self, context, idp_id, identity_provider): - identity_provider = self._normalize_dict(identity_provider) - idp_ref = self.federation_api.update_idp(idp_id, identity_provider) - return IdentityProvider.wrap_member(context, idp_ref) - - -@dependency.requires('federation_api') -class FederationProtocol(_ControllerBase): - """A federation protocol representation. - - See 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() - @validation.validated(schema.federation_protocol_schema, 'protocol') - def create_protocol(self, context, idp_id, protocol_id, protocol): - ref = self._normalize_dict(protocol) - ref = self.federation_api.create_protocol(idp_id, protocol_id, ref) - response = FederationProtocol.wrap_member(context, ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - @validation.validated(schema.federation_protocol_schema, 'protocol') - def update_protocol(self, context, idp_id, protocol_id, protocol): - ref = self._normalize_dict(protocol) - ref = self.federation_api.update_protocol(idp_id, protocol_id, - protocol) - return FederationProtocol.wrap_member(context, ref) - - @controller.protected() - def get_protocol(self, context, idp_id, protocol_id): - ref = self.federation_api.get_protocol(idp_id, protocol_id) - return FederationProtocol.wrap_member(context, ref) - - @controller.protected() - def list_protocols(self, context, idp_id): - protocols_ref = self.federation_api.list_protocols(idp_id) - protocols = list(protocols_ref) - return FederationProtocol.wrap_collection(context, protocols) - - @controller.protected() - def delete_protocol(self, context, idp_id, protocol_id): - self.federation_api.delete_protocol(idp_id, protocol_id) - - -@dependency.requires('federation_api') -class MappingController(_ControllerBase): - collection_name = 'mappings' - member_name = 'mapping' - - @controller.protected() - def create_mapping(self, context, mapping_id, mapping): - ref = self._normalize_dict(mapping) - utils.validate_mapping_structure(ref) - mapping_ref = self.federation_api.create_mapping(mapping_id, ref) - response = MappingController.wrap_member(context, mapping_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.protected() - def list_mappings(self, context): - ref = self.federation_api.list_mappings() - return MappingController.wrap_collection(context, ref) - - @controller.protected() - def get_mapping(self, context, mapping_id): - ref = self.federation_api.get_mapping(mapping_id) - return MappingController.wrap_member(context, ref) - - @controller.protected() - def delete_mapping(self, context, mapping_id): - self.federation_api.delete_mapping(mapping_id) - - @controller.protected() - def update_mapping(self, context, mapping_id, mapping): - mapping = self._normalize_dict(mapping) - utils.validate_mapping_structure(mapping) - mapping_ref = self.federation_api.update_mapping(mapping_id, mapping) - return MappingController.wrap_member(context, mapping_ref) - - -@dependency.requires('federation_api') -class Auth(auth_controllers.Auth): - - def _get_sso_origin_host(self, context): - """Validate and return originating dashboard URL. - - Make sure the parameter is specified in the request's URL as well its - value belongs to a list of trusted dashboards. - - :param context: request's context - :raises keystone.exception.ValidationError: ``origin`` query parameter - was not specified. The URL is deemed invalid. - :raises keystone.exception.Unauthorized: URL specified in origin query - parameter does not exist in list of websso trusted dashboards. - :returns: URL with the originating dashboard - - """ - if 'origin' in context['query_string']: - origin = context['query_string']['origin'] - host = urllib.parse.unquote_plus(origin) - else: - msg = _('Request must have an origin query parameter') - LOG.error(msg) - raise exception.ValidationError(msg) - - # change trusted_dashboard hostnames to lowercase before comparison - trusted_dashboards = [k_utils.lower_case_hostname(trusted) - for trusted in CONF.federation.trusted_dashboard] - - if host not in trusted_dashboards: - msg = _('%(host)s is not a trusted dashboard host') - msg = msg % {'host': host} - LOG.error(msg) - raise exception.Unauthorized(msg) - - return host - - def federated_authentication(self, context, idp_id, protocol_id): - """Authenticate from dedicated url endpoint. - - Build HTTP request body for federated authentication and inject - it into the ``authenticate_for_token`` function. - - """ - auth = { - 'identity': { - 'methods': [protocol_id], - protocol_id: { - 'identity_provider': idp_id, - 'protocol': protocol_id - } - } - } - - return self.authenticate_for_token(context, auth=auth) - - def federated_sso_auth(self, context, protocol_id): - try: - remote_id_name = utils.get_remote_id_parameter(protocol_id) - remote_id = context['environment'][remote_id_name] - except KeyError: - msg = _('Missing entity ID from environment') - LOG.error(msg) - raise exception.Unauthorized(msg) - - host = self._get_sso_origin_host(context) - - ref = self.federation_api.get_idp_from_remote_id(remote_id) - # NOTE(stevemar): the returned object is a simple dict that - # contains the idp_id and remote_id. - identity_provider = ref['idp_id'] - res = self.federated_authentication(context, identity_provider, - protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def federated_idp_specific_sso_auth(self, context, idp_id, protocol_id): - host = self._get_sso_origin_host(context) - - # NOTE(lbragstad): We validate that the Identity Provider actually - # exists in the Mapped authentication plugin. - res = self.federated_authentication(context, idp_id, protocol_id) - token_id = res.headers['X-Subject-Token'] - return self.render_html_response(host, token_id) - - def render_html_response(self, host, token_id): - """Forms an HTML Form from a template with autosubmit.""" - headers = [('Content-Type', 'text/html')] - - with open(CONF.federation.sso_callback_template) as template: - src = string.Template(template.read()) - - subs = {'host': host, 'token': token_id} - body = src.substitute(subs) - return webob.Response(body=body, status='200', - headerlist=headers) - - def _create_base_saml_assertion(self, context, auth): - issuer = CONF.saml.idp_entity_id - sp_id = auth['scope']['service_provider']['id'] - service_provider = self.federation_api.get_sp(sp_id) - utils.assert_enabled_service_provider_object(service_provider) - sp_url = service_provider['sp_url'] - - token_id = auth['identity']['token']['id'] - token_data = self.token_provider_api.validate_token(token_id) - token_ref = token_model.KeystoneToken(token_id, token_data) - - if not token_ref.project_scoped: - action = _('Use a project scoped token when attempting to create ' - 'a SAML assertion') - raise exception.ForbiddenAction(action=action) - - subject = token_ref.user_name - roles = token_ref.role_names - project = token_ref.project_name - # NOTE(rodrigods): the domain name is necessary in order to distinguish - # between projects and users with the same name in different domains. - project_domain_name = token_ref.project_domain_name - subject_domain_name = token_ref.user_domain_name - - generator = keystone_idp.SAMLGenerator() - response = generator.samlize_token( - issuer, sp_url, subject, subject_domain_name, - roles, project, project_domain_name) - return (response, service_provider) - - def _build_response_headers(self, service_provider): - return [('Content-Type', 'text/xml'), - ('X-sp-url', six.binary_type(service_provider['sp_url'])), - ('X-auth-url', six.binary_type(service_provider['auth_url']))] - - @validation.validated(schema.saml_create, 'auth') - def create_saml_assertion(self, context, auth): - """Exchange a scoped token for a SAML assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: SAML Assertion based on properties from the token - """ - t = self._create_base_saml_assertion(context, auth) - (response, service_provider) = t - - headers = self._build_response_headers(service_provider) - return wsgi.render_response(body=response.to_string(), - status=('200', 'OK'), - headers=headers) - - @validation.validated(schema.saml_create, 'auth') - def create_ecp_assertion(self, context, auth): - """Exchange a scoped token for an ECP assertion. - - :param auth: Dictionary that contains a token and service provider ID - :returns: ECP Assertion based on properties from the token - """ - t = self._create_base_saml_assertion(context, auth) - (saml_assertion, service_provider) = t - relay_state_prefix = service_provider['relay_state_prefix'] - - generator = keystone_idp.ECPGenerator() - ecp_assertion = generator.generate_ecp(saml_assertion, - relay_state_prefix) - - headers = self._build_response_headers(service_provider) - return wsgi.render_response(body=ecp_assertion.to_string(), - status=('200', 'OK'), - headers=headers) - - -@dependency.requires('assignment_api', 'resource_api') -class DomainV3(controller.V3Controller): - collection_name = 'domains' - member_name = 'domain' - - def __init__(self): - super(DomainV3, self).__init__() - self.get_member_from_driver = self.resource_api.get_domain - - @controller.protected() - def list_domains_for_groups(self, context): - """List all domains available to an authenticated user's groups. - - :param context: request context - :returns: list of accessible domains - - """ - auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] - domains = self.assignment_api.list_domains_for_groups( - auth_context['group_ids']) - return DomainV3.wrap_collection(context, domains) - - -@dependency.requires('assignment_api', 'resource_api') -class ProjectAssignmentV3(controller.V3Controller): - collection_name = 'projects' - member_name = 'project' - - def __init__(self): - super(ProjectAssignmentV3, self).__init__() - self.get_member_from_driver = self.resource_api.get_project - - @controller.protected() - def list_projects_for_groups(self, context): - """List all projects available to an authenticated user's groups. - - :param context: request context - :returns: list of accessible projects - - """ - auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] - projects = self.assignment_api.list_projects_for_groups( - auth_context['group_ids']) - return ProjectAssignmentV3.wrap_collection(context, projects) - - -@dependency.requires('federation_api') -class ServiceProvider(_ControllerBase): - """Service Provider representation.""" - - collection_name = 'service_providers' - member_name = 'service_provider' - - _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', - 'links', 'relay_state_prefix', 'sp_url']) - - @controller.protected() - @validation.validated(schema.service_provider_create, 'service_provider') - def create_service_provider(self, context, sp_id, service_provider): - service_provider = self._normalize_dict(service_provider) - service_provider.setdefault('enabled', False) - service_provider.setdefault('relay_state_prefix', - CONF.saml.relay_state_prefix) - sp_ref = self.federation_api.create_sp(sp_id, service_provider) - response = ServiceProvider.wrap_member(context, sp_ref) - return wsgi.render_response(body=response, status=('201', 'Created')) - - @controller.filterprotected('id', 'enabled') - def list_service_providers(self, context, filters): - hints = self.build_driver_hints(context, filters) - ref = self.federation_api.list_sps(hints=hints) - ref = [self.filter_params(x) for x in ref] - return ServiceProvider.wrap_collection(context, ref, hints=hints) - - @controller.protected() - def get_service_provider(self, context, sp_id): - ref = self.federation_api.get_sp(sp_id) - return ServiceProvider.wrap_member(context, ref) - - @controller.protected() - def delete_service_provider(self, context, sp_id): - self.federation_api.delete_sp(sp_id) - - @controller.protected() - @validation.validated(schema.service_provider_update, 'service_provider') - def update_service_provider(self, context, sp_id, service_provider): - service_provider = self._normalize_dict(service_provider) - sp_ref = self.federation_api.update_sp(sp_id, service_provider) - return ServiceProvider.wrap_member(context, sp_ref) - - -class SAMLMetadataV3(_ControllerBase): - member_name = 'metadata' - - def get_metadata(self, context): - metadata_path = CONF.saml.idp_metadata_path - try: - with open(metadata_path, 'r') as metadata_handler: - metadata = metadata_handler.read() - except IOError as e: - # Raise HTTP 500 in case Metadata file cannot be read. - raise exception.MetadataFileError(reason=e) - return wsgi.render_response(body=metadata, status=('200', 'OK'), - headers=[('Content-Type', 'text/xml')]) diff --git a/keystone-moon/keystone/federation/core.py b/keystone-moon/keystone/federation/core.py deleted file mode 100644 index 23028dfd..00000000 --- a/keystone-moon/keystone/federation/core.py +++ /dev/null @@ -1,611 +0,0 @@ -# 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. - -"""Main entry point into the Federation service.""" - -import abc - -from oslo_config import cfg -from oslo_log import versionutils -import six - -from keystone.common import dependency -from keystone.common import extension -from keystone.common import manager -from keystone import exception -from keystone.federation import utils - - -CONF = cfg.CONF -EXTENSION_DATA = { - 'name': 'OpenStack Federation APIs', - 'namespace': 'http://docs.openstack.org/identity/api/ext/' - 'OS-FEDERATION/v1.0', - 'alias': 'OS-FEDERATION', - 'updated': '2013-12-17T12:00:0-00:00', - 'description': 'OpenStack Identity Providers Mechanism.', - 'links': [{ - 'rel': 'describedby', - 'type': 'text/html', - 'href': 'http://specs.openstack.org/openstack/keystone-specs/api/v3/' - 'identity-api-v3-os-federation-ext.html', - }]} -extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) -extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) - - -@dependency.provider('federation_api') -class Manager(manager.Manager): - """Default pivot point for the Federation backend. - - See :mod:`keystone.common.manager.Manager` for more details on how this - dynamically calls the backend. - - """ - - driver_namespace = 'keystone.federation' - - def __init__(self): - super(Manager, self).__init__(CONF.federation.driver) - - # Make sure it is a driver version we support, and if it is a legacy - # driver, then wrap it. - if isinstance(self.driver, FederationDriverV8): - self.driver = V9FederationWrapperForV8Driver(self.driver) - elif not isinstance(self.driver, FederationDriverV9): - raise exception.UnsupportedDriverVersion( - driver=CONF.federation.driver) - - def get_enabled_service_providers(self): - """List enabled service providers for Service Catalog - - Service Provider in a catalog contains three attributes: ``id``, - ``auth_url``, ``sp_url``, where: - - - id is a unique, user defined identifier for service provider object - - auth_url is an authentication URL of remote Keystone - - sp_url a URL accessible at the remote service provider where SAML - assertion is transmitted. - - :returns: list of dictionaries with enabled service providers - :rtype: list of dicts - - """ - def normalize(sp): - ref = { - 'auth_url': sp.auth_url, - 'id': sp.id, - 'sp_url': sp.sp_url - } - return ref - - service_providers = self.driver.get_enabled_service_providers() - return [normalize(sp) for sp in service_providers] - - def evaluate(self, idp_id, protocol_id, assertion_data): - mapping = self.get_mapping_from_idp_and_protocol(idp_id, protocol_id) - rules = mapping['rules'] - rule_processor = utils.RuleProcessor(mapping['id'], rules) - mapped_properties = rule_processor.process(assertion_data) - return mapped_properties, mapping['id'] - - -# The FederationDriverBase class is the set of driver methods from earlier -# drivers that we still support, that have not been removed or modified. This -# class is then used to created the augmented V8 and V9 version abstract driver -# classes, without having to duplicate a lot of abstract method signatures. -# If you remove a method from V9, then move the abstract methods from this Base -# class to the V8 class. Do not modify any of the method signatures in the Base -# class - changes should only be made in the V8 and subsequent classes. - -@six.add_metaclass(abc.ABCMeta) -class FederationDriverBase(object): - - @abc.abstractmethod - def create_idp(self, idp_id, idp): - """Create an identity provider. - - :param idp_id: ID of IdP object - :type idp_id: string - :param idp: idp object - :type idp: dict - :returns: idp ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_idp(self, idp_id): - """Delete an identity provider. - - :param idp_id: ID of IdP object - :type idp_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_idp(self, idp_id): - """Get an identity provider by ID. - - :param idp_id: ID of IdP object - :type idp_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :returns: idp ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_idp_from_remote_id(self, remote_id): - """Get an identity provider by remote ID. - - :param remote_id: ID of remote IdP - :type idp_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :returns: idp ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_idp(self, idp_id, idp): - """Update an identity provider by ID. - - :param idp_id: ID of IdP object - :type idp_id: string - :param idp: idp object - :type idp: dict - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :returns: idp ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_protocol(self, idp_id, protocol_id, protocol): - """Add an IdP-Protocol configuration. - - :param idp_id: ID of IdP object - :type idp_id: string - :param protocol_id: ID of protocol object - :type protocol_id: string - :param protocol: protocol object - :type protocol: dict - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :returns: protocol ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_protocol(self, idp_id, protocol_id, protocol): - """Change an IdP-Protocol configuration. - - :param idp_id: ID of IdP object - :type idp_id: string - :param protocol_id: ID of protocol object - :type protocol_id: string - :param protocol: protocol object - :type protocol: dict - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :raises keystone.exception.FederatedProtocolNotFound: If the federated - protocol cannot be found. - :returns: protocol ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_protocol(self, idp_id, protocol_id): - """Get an IdP-Protocol configuration. - - :param idp_id: ID of IdP object - :type idp_id: string - :param protocol_id: ID of protocol object - :type protocol_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :raises keystone.exception.FederatedProtocolNotFound: If the federated - protocol cannot be found. - :returns: protocol ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_protocols(self, idp_id): - """List an IdP's supported protocols. - - :param idp_id: ID of IdP object - :type idp_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :returns: list of protocol ref - :rtype: list of dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_protocol(self, idp_id, protocol_id): - """Delete an IdP-Protocol configuration. - - :param idp_id: ID of IdP object - :type idp_id: string - :param protocol_id: ID of protocol object - :type protocol_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :raises keystone.exception.FederatedProtocolNotFound: If the federated - protocol cannot be found. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_mapping(self, mapping_id, mapping): - """Create a mapping. - - :param mapping_id: ID of mapping object - :type mapping_id: string - :param mapping: mapping ref with mapping name - :type mapping: dict - :returns: mapping ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_mapping(self, mapping_id): - """Delete a mapping. - - :param mapping_id: id of mapping to delete - :type mapping_ref: string - :returns: None - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_mapping(self, mapping_id, mapping_ref): - """Update a mapping. - - :param mapping_id: id of mapping to update - :type mapping_id: string - :param mapping_ref: new mapping ref - :type mapping_ref: dict - :returns: mapping ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_mappings(self): - """List all mappings. - - :returns: list of mapping refs - :rtype: list of dicts - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_mapping(self, mapping_id): - """Get a mapping, returns the mapping based on mapping_id. - - :param mapping_id: id of mapping to get - :type mapping_ref: string - :raises keystone.exception.MappingNotFound: If the mapping cannot - be found. - :returns: mapping ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): - """Get mapping based on idp_id and protocol_id. - - :param idp_id: id of the identity provider - :type idp_id: string - :param protocol_id: id of the protocol - :type protocol_id: string - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - :raises keystone.exception.FederatedProtocolNotFound: If the federated - protocol cannot be found. - :returns: mapping ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def create_sp(self, sp_id, sp): - """Create a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - :param sp: service prvider object - :type sp: dict - - :returns: service provider ref - :rtype: dict - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def delete_sp(self, sp_id): - """Delete a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - - :raises keystone.exception.ServiceProviderNotFound: If the service - provider doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def get_sp(self, sp_id): - """Get a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - :returns: service provider ref - :rtype: dict - - :raises keystone.exception.ServiceProviderNotFound: If the service - provider doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def update_sp(self, sp_id, sp): - """Update a service provider. - - :param sp_id: id of the service provider - :type sp_id: string - :param sp: service prvider object - :type sp: dict - - :returns: service provider ref - :rtype: dict - - :raises keystone.exception.ServiceProviderNotFound: If the service - provider doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - def get_enabled_service_providers(self): - """List enabled service providers for Service Catalog - - Service Provider in a catalog contains three attributes: ``id``, - ``auth_url``, ``sp_url``, where: - - - id is a unique, user defined identifier for service provider object - - auth_url is an authentication URL of remote Keystone - - sp_url a URL accessible at the remote service provider where SAML - assertion is transmitted. - - :returns: list of dictionaries with enabled service providers - :rtype: list of dicts - - """ - raise exception.NotImplemented() # pragma: no cover - - -class FederationDriverV8(FederationDriverBase): - """Removed or redefined methods from V8. - - Move the abstract methods of any methods removed or modified in later - versions of the driver from FederationDriverBase to here. We maintain this - so that legacy drivers, which will be a subclass of FederationDriverV8, can - still reference them. - - """ - - @abc.abstractmethod - def list_idps(self): - """List all identity providers. - - :returns: list of idp refs - :rtype: list of dicts - - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_sps(self): - """List all service providers. - - :returns: List of service provider ref objects - :rtype: list of dicts - - """ - raise exception.NotImplemented() # pragma: no cover - - -class FederationDriverV9(FederationDriverBase): - """New or redefined methods from V8. - - Add any new V9 abstract methods (or those with modified signatures) to - this class. - - """ - - @abc.abstractmethod - def list_idps(self, hints): - """List all identity providers. - - :param hints: filter hints which the driver should - implement if at all possible. - :returns: list of idp refs - :rtype: list of dicts - - :raises keystone.exception.IdentityProviderNotFound: If the IdP - doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - @abc.abstractmethod - def list_sps(self, hints): - """List all service providers. - - :param hints: filter hints which the driver should - implement if at all possible. - :returns: List of service provider ref objects - :rtype: list of dicts - - :raises keystone.exception.ServiceProviderNotFound: If the SP - doesn't exist. - - """ - raise exception.NotImplemented() # pragma: no cover - - -class V9FederationWrapperForV8Driver(FederationDriverV9): - """Wrapper class to supported a V8 legacy driver. - - In order to support legacy drivers without having to make the manager code - driver-version aware, we wrap legacy drivers so that they look like the - latest version. For the various changes made in a new driver, here are the - actions needed in this wrapper: - - Method removed from new driver - remove the call-through method from this - class, since the manager will no longer be - calling it. - Method signature (or meaning) changed - wrap the old method in a new - signature here, and munge the input - and output parameters accordingly. - New method added to new driver - add a method to implement the new - functionality here if possible. If that is - not possible, then return NotImplemented, - since we do not guarantee to support new - functionality with legacy drivers. - - """ - - @versionutils.deprecated( - as_of=versionutils.deprecated.MITAKA, - what='keystone.federation.FederationDriverV8', - in_favor_of='keystone.federation.FederationDriverV9', - remove_in=+2) - def __init__(self, wrapped_driver): - self.driver = wrapped_driver - - def create_idp(self, idp_id, idp): - return self.driver.create_idp(idp_id, idp) - - def delete_idp(self, idp_id): - self.driver.delete_idp(idp_id) - - # NOTE(davechen): The hints is ignored here to support legacy drivers, - # but the filters in hints will be remain unsatisfied and V3Controller - # wrapper will apply these filters at the end. So that the result get - # returned for list IdP will still be filtered with the legacy drivers. - def list_idps(self, hints): - return self.driver.list_idps() - - def get_idp(self, idp_id): - return self.driver.get_idp(idp_id) - - def get_idp_from_remote_id(self, remote_id): - return self.driver.get_idp_from_remote_id(remote_id) - - def update_idp(self, idp_id, idp): - return self.driver.update_idp(idp_id, idp) - - def create_protocol(self, idp_id, protocol_id, protocol): - return self.driver.create_protocol(idp_id, protocol_id, protocol) - - def update_protocol(self, idp_id, protocol_id, protocol): - return self.driver.update_protocol(idp_id, protocol_id, protocol) - - def get_protocol(self, idp_id, protocol_id): - return self.driver.get_protocol(idp_id, protocol_id) - - def list_protocols(self, idp_id): - return self.driver.list_protocols(idp_id) - - def delete_protocol(self, idp_id, protocol_id): - self.driver.delete_protocol(idp_id, protocol_id) - - def create_mapping(self, mapping_id, mapping): - return self.driver.create_mapping(mapping_id, mapping) - - def delete_mapping(self, mapping_id): - self.driver.delete_mapping(mapping_id) - - def update_mapping(self, mapping_id, mapping_ref): - return self.driver.update_mapping(mapping_id, mapping_ref) - - def list_mappings(self): - return self.driver.list_mappings() - - def get_mapping(self, mapping_id): - return self.driver.get_mapping(mapping_id) - - def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): - return self.driver.get_mapping_from_idp_and_protocol( - idp_id, protocol_id) - - def create_sp(self, sp_id, sp): - return self.driver.create_sp(sp_id, sp) - - def delete_sp(self, sp_id): - self.driver.delete_sp(sp_id) - - # NOTE(davechen): The hints is ignored here to support legacy drivers, - # but the filters in hints will be remain unsatisfied and V3Controller - # wrapper will apply these filters at the end. So that the result get - # returned for list SPs will still be filtered with the legacy drivers. - def list_sps(self, hints): - return self.driver.list_sps() - - def get_sp(self, sp_id): - return self.driver.get_sp(sp_id) - - def update_sp(self, sp_id, sp): - return self.driver.update_sp(sp_id, sp) - - def get_enabled_service_providers(self): - return self.driver.get_enabled_service_providers() - - -Driver = manager.create_legacy_driver(FederationDriverV8) diff --git a/keystone-moon/keystone/federation/idp.py b/keystone-moon/keystone/federation/idp.py deleted file mode 100644 index 494d58b9..00000000 --- a/keystone-moon/keystone/federation/idp.py +++ /dev/null @@ -1,615 +0,0 @@ -# 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 datetime -import os -import uuid - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import fileutils -from oslo_utils import importutils -from oslo_utils import timeutils -import saml2 -from saml2 import client_base -from saml2 import md -from saml2.profile import ecp -from saml2 import saml -from saml2 import samlp -from saml2.schema import soapenv -from saml2 import sigver -xmldsig = importutils.try_import("saml2.xmldsig") -if not xmldsig: - xmldsig = importutils.try_import("xmldsig") - -from keystone.common import environment -from keystone.common import utils -from keystone import exception -from keystone.i18n import _, _LE - - -LOG = log.getLogger(__name__) -CONF = cfg.CONF - - -class SAMLGenerator(object): - """A class to generate SAML assertions.""" - - def __init__(self): - self.assertion_id = uuid.uuid4().hex - - def samlize_token(self, issuer, recipient, user, user_domain_name, roles, - project, project_domain_name, expires_in=None): - """Convert Keystone attributes to a SAML assertion. - - :param issuer: URL of the issuing party - :type issuer: string - :param recipient: URL of the recipient - :type recipient: string - :param user: User name - :type user: string - :param user_domain_name: User Domain name - :type user_domain_name: string - :param roles: List of role names - :type roles: list - :param project: Project name - :type project: string - :param project_domain_name: Project Domain name - :type project_domain_name: string - :param expires_in: Sets how long the assertion is valid for, in seconds - :type expires_in: int - - :returns: XML <Response> object - - """ - expiration_time = self._determine_expiration_time(expires_in) - status = self._create_status() - saml_issuer = self._create_issuer(issuer) - subject = self._create_subject(user, expiration_time, recipient) - attribute_statement = self._create_attribute_statement( - user, user_domain_name, roles, project, project_domain_name) - authn_statement = self._create_authn_statement(issuer, expiration_time) - signature = self._create_signature() - - assertion = self._create_assertion(saml_issuer, signature, - subject, authn_statement, - attribute_statement) - - assertion = _sign_assertion(assertion) - - response = self._create_response(saml_issuer, status, assertion, - recipient) - return response - - def _determine_expiration_time(self, expires_in): - if expires_in is None: - expires_in = CONF.saml.assertion_expiration_time - now = timeutils.utcnow() - future = now + datetime.timedelta(seconds=expires_in) - return utils.isotime(future, subsecond=True) - - def _create_status(self): - """Create an object that represents a SAML Status. - - <ns0:Status xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol"> - <ns0:StatusCode - Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> - </ns0:Status> - - :returns: XML <Status> object - - """ - status = samlp.Status() - status_code = samlp.StatusCode() - status_code.value = samlp.STATUS_SUCCESS - status_code.set_text('') - status.status_code = status_code - return status - - def _create_issuer(self, issuer_url): - """Create an object that represents a SAML Issuer. - - <ns0:Issuer - xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"> - https://acme.com/FIM/sps/openstack/saml20</ns0:Issuer> - - :returns: XML <Issuer> object - - """ - issuer = saml.Issuer() - issuer.format = saml.NAMEID_FORMAT_ENTITY - issuer.set_text(issuer_url) - return issuer - - def _create_subject(self, user, expiration_time, recipient): - """Create an object that represents a SAML Subject. - - <ns0:Subject> - <ns0:NameID> - john@smith.com</ns0:NameID> - <ns0:SubjectConfirmation - Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> - <ns0:SubjectConfirmationData - NotOnOrAfter="2014-08-19T11:53:57.243106Z" - Recipient="http://beta.com/Shibboleth.sso/SAML2/POST" /> - </ns0:SubjectConfirmation> - </ns0:Subject> - - :returns: XML <Subject> object - - """ - name_id = saml.NameID() - name_id.set_text(user) - subject_conf_data = saml.SubjectConfirmationData() - subject_conf_data.recipient = recipient - subject_conf_data.not_on_or_after = expiration_time - subject_conf = saml.SubjectConfirmation() - subject_conf.method = saml.SCM_BEARER - subject_conf.subject_confirmation_data = subject_conf_data - subject = saml.Subject() - subject.subject_confirmation = subject_conf - subject.name_id = name_id - return subject - - def _create_attribute_statement(self, user, user_domain_name, roles, - project, project_domain_name): - """Create an object that represents a SAML AttributeStatement. - - <ns0:AttributeStatement> - <ns0:Attribute Name="openstack_user"> - <ns0:AttributeValue - xsi:type="xs:string">test_user</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_user_domain"> - <ns0:AttributeValue - xsi:type="xs:string">Default</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_roles"> - <ns0:AttributeValue - xsi:type="xs:string">admin</ns0:AttributeValue> - <ns0:AttributeValue - xsi:type="xs:string">member</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_project"> - <ns0:AttributeValue - xsi:type="xs:string">development</ns0:AttributeValue> - </ns0:Attribute> - <ns0:Attribute Name="openstack_project_domain"> - <ns0:AttributeValue - xsi:type="xs:string">Default</ns0:AttributeValue> - </ns0:Attribute> - </ns0:AttributeStatement> - - :returns: XML <AttributeStatement> object - - """ - def _build_attribute(attribute_name, attribute_values): - attribute = saml.Attribute() - attribute.name = attribute_name - - for value in attribute_values: - attribute_value = saml.AttributeValue() - attribute_value.set_text(value) - attribute.attribute_value.append(attribute_value) - - return attribute - - user_attribute = _build_attribute('openstack_user', [user]) - roles_attribute = _build_attribute('openstack_roles', roles) - project_attribute = _build_attribute('openstack_project', [project]) - project_domain_attribute = _build_attribute( - 'openstack_project_domain', [project_domain_name]) - user_domain_attribute = _build_attribute( - 'openstack_user_domain', [user_domain_name]) - - attribute_statement = saml.AttributeStatement() - attribute_statement.attribute.append(user_attribute) - attribute_statement.attribute.append(roles_attribute) - attribute_statement.attribute.append(project_attribute) - attribute_statement.attribute.append(project_domain_attribute) - attribute_statement.attribute.append(user_domain_attribute) - return attribute_statement - - def _create_authn_statement(self, issuer, expiration_time): - """Create an object that represents a SAML AuthnStatement. - - <ns0:AuthnStatement xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion" - AuthnInstant="2014-07-30T03:04:25Z" SessionIndex="47335964efb" - SessionNotOnOrAfter="2014-07-30T03:04:26Z"> - <ns0:AuthnContext> - <ns0:AuthnContextClassRef> - urn:oasis:names:tc:SAML:2.0:ac:classes:Password - </ns0:AuthnContextClassRef> - <ns0:AuthenticatingAuthority> - https://acme.com/FIM/sps/openstack/saml20 - </ns0:AuthenticatingAuthority> - </ns0:AuthnContext> - </ns0:AuthnStatement> - - :returns: XML <AuthnStatement> object - - """ - authn_statement = saml.AuthnStatement() - authn_statement.authn_instant = utils.isotime() - authn_statement.session_index = uuid.uuid4().hex - authn_statement.session_not_on_or_after = expiration_time - - authn_context = saml.AuthnContext() - authn_context_class = saml.AuthnContextClassRef() - authn_context_class.set_text(saml.AUTHN_PASSWORD) - - authn_authority = saml.AuthenticatingAuthority() - authn_authority.set_text(issuer) - authn_context.authn_context_class_ref = authn_context_class - authn_context.authenticating_authority = authn_authority - - authn_statement.authn_context = authn_context - - return authn_statement - - def _create_assertion(self, issuer, signature, subject, authn_statement, - attribute_statement): - """Create an object that represents a SAML Assertion. - - <ns0:Assertion - ID="35daed258ba647ba8962e9baff4d6a46" - IssueInstant="2014-06-11T15:45:58Z" - Version="2.0"> - <ns0:Issuer> ... </ns0:Issuer> - <ns1:Signature> ... </ns1:Signature> - <ns0:Subject> ... </ns0:Subject> - <ns0:AuthnStatement> ... </ns0:AuthnStatement> - <ns0:AttributeStatement> ... </ns0:AttributeStatement> - </ns0:Assertion> - - :returns: XML <Assertion> object - - """ - assertion = saml.Assertion() - assertion.id = self.assertion_id - assertion.issue_instant = utils.isotime() - assertion.version = '2.0' - assertion.issuer = issuer - assertion.signature = signature - assertion.subject = subject - assertion.authn_statement = authn_statement - assertion.attribute_statement = attribute_statement - return assertion - - def _create_response(self, issuer, status, assertion, recipient): - """Create an object that represents a SAML Response. - - <ns0:Response - Destination="http://beta.com/Shibboleth.sso/SAML2/POST" - ID="c5954543230e4e778bc5b92923a0512d" - IssueInstant="2014-07-30T03:19:45Z" - Version="2.0" /> - <ns0:Issuer> ... </ns0:Issuer> - <ns0:Assertion> ... </ns0:Assertion> - <ns0:Status> ... </ns0:Status> - </ns0:Response> - - :returns: XML <Response> object - - """ - response = samlp.Response() - response.id = uuid.uuid4().hex - response.destination = recipient - response.issue_instant = utils.isotime() - response.version = '2.0' - response.issuer = issuer - response.status = status - response.assertion = assertion - return response - - def _create_signature(self): - """Create an object that represents a SAML <Signature>. - - This must be filled with algorithms that the signing binary will apply - in order to sign the whole message. - Currently we enforce X509 signing. - Example of the template:: - - <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> - <SignedInfo> - <CanonicalizationMethod - Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> - <SignatureMethod - Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> - <Reference URI="#<Assertion ID>"> - <Transforms> - <Transform - Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> - <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> - </Transforms> - <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> - <DigestValue /> - </Reference> - </SignedInfo> - <SignatureValue /> - <KeyInfo> - <X509Data /> - </KeyInfo> - </Signature> - - :returns: XML <Signature> object - - """ - canonicalization_method = xmldsig.CanonicalizationMethod() - canonicalization_method.algorithm = xmldsig.ALG_EXC_C14N - signature_method = xmldsig.SignatureMethod( - algorithm=xmldsig.SIG_RSA_SHA1) - - transforms = xmldsig.Transforms() - envelope_transform = xmldsig.Transform( - algorithm=xmldsig.TRANSFORM_ENVELOPED) - - c14_transform = xmldsig.Transform(algorithm=xmldsig.ALG_EXC_C14N) - transforms.transform = [envelope_transform, c14_transform] - - digest_method = xmldsig.DigestMethod(algorithm=xmldsig.DIGEST_SHA1) - digest_value = xmldsig.DigestValue() - - reference = xmldsig.Reference() - reference.uri = '#' + self.assertion_id - reference.digest_method = digest_method - reference.digest_value = digest_value - reference.transforms = transforms - - signed_info = xmldsig.SignedInfo() - signed_info.canonicalization_method = canonicalization_method - signed_info.signature_method = signature_method - signed_info.reference = reference - - key_info = xmldsig.KeyInfo() - key_info.x509_data = xmldsig.X509Data() - - signature = xmldsig.Signature() - signature.signed_info = signed_info - signature.signature_value = xmldsig.SignatureValue() - signature.key_info = key_info - - return signature - - -def _sign_assertion(assertion): - """Sign a SAML assertion. - - This method utilizes ``xmlsec1`` binary and signs SAML assertions in a - separate process. ``xmlsec1`` cannot read input data from stdin so the - prepared assertion needs to be serialized and stored in a temporary - file. This file will be deleted immediately after ``xmlsec1`` returns. - The signed assertion is redirected to a standard output and read using - subprocess.PIPE redirection. A ``saml.Assertion`` class is created - from the signed string again and returned. - - Parameters that are required in the CONF:: - * xmlsec_binary - * private key file path - * public key file path - :returns: XML <Assertion> object - - """ - xmlsec_binary = CONF.saml.xmlsec1_binary - idp_private_key = CONF.saml.keyfile - idp_public_key = CONF.saml.certfile - - # xmlsec1 --sign --privkey-pem privkey,cert --id-attr:ID <tag> <file> - certificates = '%(idp_private_key)s,%(idp_public_key)s' % { - 'idp_public_key': idp_public_key, - 'idp_private_key': idp_private_key - } - - command_list = [xmlsec_binary, '--sign', '--privkey-pem', certificates, - '--id-attr:ID', 'Assertion'] - - file_path = None - try: - # NOTE(gyee): need to make the namespace prefixes explicit so - # they won't get reassigned when we wrap the assertion into - # SAML2 response - file_path = fileutils.write_to_tempfile(assertion.to_string( - nspair={'saml': saml2.NAMESPACE, - 'xmldsig': xmldsig.NAMESPACE})) - command_list.append(file_path) - subprocess = environment.subprocess - stdout = subprocess.check_output(command_list, # nosec : The contents - # of the command list are coming from - # a trusted source because the - # executable and arguments all either - # come from the config file or are - # hardcoded. The command list is - # initialized earlier in this function - # to a list and it's still a list at - # this point in the function. There is - # no opportunity for an attacker to - # attempt command injection via string - # parsing. - stderr=subprocess.STDOUT) - except Exception as e: - msg = _LE('Error when signing assertion, reason: %(reason)s%(output)s') - LOG.error(msg, - {'reason': e, - 'output': ' ' + e.output if hasattr(e, 'output') else ''}) - raise exception.SAMLSigningError(reason=e) - finally: - try: - if file_path: - os.remove(file_path) - except OSError: # nosec - # The file is already gone, good. - pass - - return saml2.create_class_from_xml_string(saml.Assertion, stdout) - - -class MetadataGenerator(object): - """A class for generating SAML IdP Metadata.""" - - def generate_metadata(self): - """Generate Identity Provider Metadata. - - Generate and format metadata into XML that can be exposed and - consumed by a federated Service Provider. - - :returns: XML <EntityDescriptor> object. - :raises keystone.exception.ValidationError: If the required - config options aren't set. - """ - self._ensure_required_values_present() - entity_descriptor = self._create_entity_descriptor() - entity_descriptor.idpsso_descriptor = ( - self._create_idp_sso_descriptor()) - return entity_descriptor - - def _create_entity_descriptor(self): - ed = md.EntityDescriptor() - ed.entity_id = CONF.saml.idp_entity_id - return ed - - def _create_idp_sso_descriptor(self): - - def get_cert(): - try: - return sigver.read_cert_from_file(CONF.saml.certfile, 'pem') - except (IOError, sigver.CertificateError) as e: - msg = _('Cannot open certificate %(cert_file)s. ' - 'Reason: %(reason)s') - msg = msg % {'cert_file': CONF.saml.certfile, 'reason': e} - LOG.error(msg) - raise IOError(msg) - - def key_descriptor(): - cert = get_cert() - return md.KeyDescriptor( - key_info=xmldsig.KeyInfo( - x509_data=xmldsig.X509Data( - x509_certificate=xmldsig.X509Certificate(text=cert) - ) - ), use='signing' - ) - - def single_sign_on_service(): - idp_sso_endpoint = CONF.saml.idp_sso_endpoint - return md.SingleSignOnService( - binding=saml2.BINDING_URI, - location=idp_sso_endpoint) - - def organization(): - name = md.OrganizationName(lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_name) - display_name = md.OrganizationDisplayName( - lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_display_name) - url = md.OrganizationURL(lang=CONF.saml.idp_lang, - text=CONF.saml.idp_organization_url) - - return md.Organization( - organization_display_name=display_name, - organization_url=url, organization_name=name) - - def contact_person(): - company = md.Company(text=CONF.saml.idp_contact_company) - given_name = md.GivenName(text=CONF.saml.idp_contact_name) - surname = md.SurName(text=CONF.saml.idp_contact_surname) - email = md.EmailAddress(text=CONF.saml.idp_contact_email) - telephone = md.TelephoneNumber( - text=CONF.saml.idp_contact_telephone) - contact_type = CONF.saml.idp_contact_type - - return md.ContactPerson( - company=company, given_name=given_name, sur_name=surname, - email_address=email, telephone_number=telephone, - contact_type=contact_type) - - def name_id_format(): - return md.NameIDFormat(text=saml.NAMEID_FORMAT_TRANSIENT) - - idpsso = md.IDPSSODescriptor() - idpsso.protocol_support_enumeration = samlp.NAMESPACE - idpsso.key_descriptor = key_descriptor() - idpsso.single_sign_on_service = single_sign_on_service() - idpsso.name_id_format = name_id_format() - if self._check_organization_values(): - idpsso.organization = organization() - if self._check_contact_person_values(): - idpsso.contact_person = contact_person() - return idpsso - - def _ensure_required_values_present(self): - """Ensure idp_sso_endpoint and idp_entity_id have values.""" - if CONF.saml.idp_entity_id is None: - msg = _('Ensure configuration option idp_entity_id is set.') - raise exception.ValidationError(msg) - if CONF.saml.idp_sso_endpoint is None: - msg = _('Ensure configuration option idp_sso_endpoint is set.') - raise exception.ValidationError(msg) - - def _check_contact_person_values(self): - """Determine if contact information is included in metadata.""" - # Check if we should include contact information - params = [CONF.saml.idp_contact_company, - CONF.saml.idp_contact_name, - CONF.saml.idp_contact_surname, - CONF.saml.idp_contact_email, - CONF.saml.idp_contact_telephone] - for value in params: - if value is None: - return False - - # Check if contact type is an invalid value - valid_type_values = ['technical', 'other', 'support', 'administrative', - 'billing'] - if CONF.saml.idp_contact_type not in valid_type_values: - msg = _('idp_contact_type must be one of: [technical, other, ' - 'support, administrative or billing.') - raise exception.ValidationError(msg) - return True - - def _check_organization_values(self): - """Determine if organization information is included in metadata.""" - params = [CONF.saml.idp_organization_name, - CONF.saml.idp_organization_display_name, - CONF.saml.idp_organization_url] - for value in params: - if value is None: - return False - return True - - -class ECPGenerator(object): - """A class for generating an ECP assertion.""" - - @staticmethod - def generate_ecp(saml_assertion, relay_state_prefix): - ecp_generator = ECPGenerator() - header = ecp_generator._create_header(relay_state_prefix) - body = ecp_generator._create_body(saml_assertion) - envelope = soapenv.Envelope(header=header, body=body) - return envelope - - def _create_header(self, relay_state_prefix): - relay_state_text = relay_state_prefix + uuid.uuid4().hex - relay_state = ecp.RelayState(actor=client_base.ACTOR, - must_understand='1', - text=relay_state_text) - header = soapenv.Header() - header.extension_elements = ( - [saml2.element_to_extension_element(relay_state)]) - return header - - def _create_body(self, saml_assertion): - body = soapenv.Body() - body.extension_elements = ( - [saml2.element_to_extension_element(saml_assertion)]) - return body diff --git a/keystone-moon/keystone/federation/routers.py b/keystone-moon/keystone/federation/routers.py deleted file mode 100644 index a463ca63..00000000 --- a/keystone-moon/keystone/federation/routers.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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/{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 - 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/{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'), - 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-moon/keystone/federation/schema.py b/keystone-moon/keystone/federation/schema.py deleted file mode 100644 index 6cdfd1f5..00000000 --- a/keystone-moon/keystone/federation/schema.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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.common import validation -from keystone.common.validation import parameter_types - - -basic_property_id = { - 'type': 'object', - 'properties': { - 'id': { - 'type': 'string' - } - }, - 'required': ['id'], - 'additionalProperties': False -} - -saml_create = { - 'type': 'object', - 'properties': { - 'identity': { - 'type': 'object', - 'properties': { - 'token': basic_property_id, - 'methods': { - 'type': 'array' - } - }, - 'required': ['token'], - 'additionalProperties': False - }, - 'scope': { - 'type': 'object', - 'properties': { - 'service_provider': basic_property_id - }, - 'required': ['service_provider'], - 'additionalProperties': False - }, - }, - 'required': ['identity', 'scope'], - 'additionalProperties': False -} - -_service_provider_properties = { - # NOTE(rodrigods): The database accepts URLs with 256 as max length, - # but parameter_types.url uses 225 as max length. - 'auth_url': parameter_types.url, - 'sp_url': parameter_types.url, - 'description': validation.nullable(parameter_types.description), - 'enabled': parameter_types.boolean, - 'relay_state_prefix': validation.nullable(parameter_types.description) -} - -service_provider_create = { - 'type': 'object', - 'properties': _service_provider_properties, - # NOTE(rodrigods): 'id' is not required since it is passed in the URL - 'required': ['auth_url', 'sp_url'], - 'additionalProperties': False -} - -service_provider_update = { - 'type': 'object', - 'properties': _service_provider_properties, - # Make sure at least one property is being updated - 'minProperties': 1, - 'additionalProperties': False -} - -_identity_provider_properties = { - 'enabled': parameter_types.boolean, - 'description': validation.nullable(parameter_types.description), - 'remote_ids': { - 'type': ['array', 'null'], - 'items': { - 'type': 'string' - }, - 'uniqueItems': True - } -} - -identity_provider_create = { - 'type': 'object', - 'properties': _identity_provider_properties, - 'additionalProperties': False -} - -identity_provider_update = { - 'type': 'object', - 'properties': _identity_provider_properties, - # Make sure at least one property is being updated - 'minProperties': 1, - 'additionalProperties': False -} - -federation_protocol_schema = { - 'type': 'object', - 'properties': { - 'mapping_id': parameter_types.mapping_id_string - }, - # `mapping_id` is the property that cannot be ignored - 'minProperties': 1, - 'additionalProperties': False -} diff --git a/keystone-moon/keystone/federation/utils.py b/keystone-moon/keystone/federation/utils.py deleted file mode 100644 index 1d215a68..00000000 --- a/keystone-moon/keystone/federation/utils.py +++ /dev/null @@ -1,872 +0,0 @@ -# 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. - -"""Utilities for Federation Extension.""" - -import ast -import re - -import jsonschema -from oslo_config import cfg -from oslo_log import log -from oslo_utils import timeutils -import six - -from keystone import exception -from keystone.i18n import _, _LW - - -CONF = cfg.CONF -LOG = log.getLogger(__name__) - - -class UserType(object): - """User mapping type.""" - - EPHEMERAL = 'ephemeral' - LOCAL = 'local' - - -MAPPING_SCHEMA = { - "type": "object", - "required": ['rules'], - "properties": { - "rules": { - "minItems": 1, - "type": "array", - "items": { - "type": "object", - "required": ['local', 'remote'], - "additionalProperties": False, - "properties": { - "local": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": False, - "properties": { - "user": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "email": {"type": "string"}, - "domain": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"} - }, - "additionalProperties": False, - }, - "type": { - "type": "string", - "enum": [UserType.EPHEMERAL, - UserType.LOCAL] - } - }, - "additionalProperties": False - }, - "group": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "domain": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"} - }, - "additionalProperties": False, - }, - }, - "additionalProperties": False, - }, - "groups": { - "type": "string" - }, - "group_ids": { - "type": "string" - }, - "domain": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"} - }, - "additionalProperties": False - } - } - } - }, - "remote": { - "minItems": 1, - "type": "array", - "items": { - "type": "object", - "oneOf": [ - {"$ref": "#/definitions/empty"}, - {"$ref": "#/definitions/any_one_of"}, - {"$ref": "#/definitions/not_any_of"}, - {"$ref": "#/definitions/blacklist"}, - {"$ref": "#/definitions/whitelist"} - ], - } - } - } - } - } - }, - "definitions": { - "empty": { - "type": "object", - "required": ['type'], - "properties": { - "type": { - "type": "string" - }, - }, - "additionalProperties": False, - }, - "any_one_of": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'any_one_of'], - "properties": { - "type": { - "type": "string" - }, - "any_one_of": { - "type": "array" - }, - "regex": { - "type": "boolean" - } - } - }, - "not_any_of": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'not_any_of'], - "properties": { - "type": { - "type": "string" - }, - "not_any_of": { - "type": "array" - }, - "regex": { - "type": "boolean" - } - } - }, - "blacklist": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'blacklist'], - "properties": { - "type": { - "type": "string" - }, - "blacklist": { - "type": "array" - } - } - }, - "whitelist": { - "type": "object", - "additionalProperties": False, - "required": ['type', 'whitelist'], - "properties": { - "type": { - "type": "string" - }, - "whitelist": { - "type": "array" - } - } - } - } -} - - -class DirectMaps(object): - """An abstraction around the remote matches. - - Each match is treated internally as a list. - """ - - def __init__(self): - self._matches = [] - - def add(self, values): - """Adds a matched value to the list of matches. - - :param list value: the match to save - - """ - self._matches.append(values) - - def __getitem__(self, idx): - """Used by Python when executing ``''.format(*DirectMaps())``.""" - value = self._matches[idx] - if isinstance(value, list) and len(value) == 1: - return value[0] - else: - return value - - -def validate_mapping_structure(ref): - v = jsonschema.Draft4Validator(MAPPING_SCHEMA) - - messages = '' - for error in sorted(v.iter_errors(ref), key=str): - messages = messages + error.message + "\n" - - if messages: - raise exception.ValidationError(messages) - - -def validate_expiration(token_ref): - if timeutils.utcnow() > token_ref.expires: - raise exception.Unauthorized(_('Federation token is expired')) - - -def validate_groups_cardinality(group_ids, mapping_id): - """Check if groups list is non-empty. - - :param group_ids: list of group ids - :type group_ids: list of str - - :raises keystone.exception.MissingGroups: if ``group_ids`` cardinality is 0 - - """ - if not group_ids: - raise exception.MissingGroups(mapping_id=mapping_id) - - -def get_remote_id_parameter(protocol): - # NOTE(marco-fargetta): Since we support any protocol ID, we attempt to - # retrieve the remote_id_attribute of the protocol ID. If it's not - # registered in the config, then register the option and try again. - # This allows the user to register protocols other than oidc and saml2. - remote_id_parameter = None - try: - remote_id_parameter = CONF[protocol]['remote_id_attribute'] - except AttributeError: - CONF.register_opt(cfg.StrOpt('remote_id_attribute'), - group=protocol) - try: - remote_id_parameter = CONF[protocol]['remote_id_attribute'] - except AttributeError: # nosec - # No remote ID attr, will be logged and use the default instead. - pass - if not remote_id_parameter: - LOG.debug('Cannot find "remote_id_attribute" in configuration ' - 'group %s. Trying default location in ' - 'group federation.', protocol) - remote_id_parameter = CONF.federation.remote_id_attribute - - return remote_id_parameter - - -def validate_idp(idp, protocol, assertion): - """The IdP providing the assertion should be registered for the mapping.""" - remote_id_parameter = get_remote_id_parameter(protocol) - if not remote_id_parameter or not idp['remote_ids']: - LOG.debug('Impossible to identify the IdP %s ', idp['id']) - # If nothing is defined, the administrator may want to - # allow the mapping of every IdP - return - try: - idp_remote_identifier = assertion[remote_id_parameter] - except KeyError: - msg = _('Could not find Identity Provider identifier in ' - 'environment') - raise exception.ValidationError(msg) - if idp_remote_identifier not in idp['remote_ids']: - msg = _('Incoming identity provider identifier not included ' - 'among the accepted identifiers.') - raise exception.Forbidden(msg) - - -def validate_groups_in_backend(group_ids, mapping_id, identity_api): - """Iterate over group ids and make sure they are present in the backend. - - This call is not transactional. - :param group_ids: IDs of the groups to be checked - :type group_ids: list of str - - :param mapping_id: id of the mapping used for this operation - :type mapping_id: str - - :param identity_api: Identity Manager object used for communication with - backend - :type identity_api: identity.Manager - - :raises keystone.exception.MappedGroupNotFound: If the group returned by - mapping was not found in the backend. - - """ - for group_id in group_ids: - try: - identity_api.get_group(group_id) - except exception.GroupNotFound: - raise exception.MappedGroupNotFound( - group_id=group_id, mapping_id=mapping_id) - - -def validate_groups(group_ids, mapping_id, identity_api): - """Check group ids cardinality and check their existence in the backend. - - This call is not transactional. - :param group_ids: IDs of the groups to be checked - :type group_ids: list of str - - :param mapping_id: id of the mapping used for this operation - :type mapping_id: str - - :param identity_api: Identity Manager object used for communication with - backend - :type identity_api: identity.Manager - - :raises keystone.exception.MappedGroupNotFound: If the group returned by - mapping was not found in the backend. - :raises keystone.exception.MissingGroups: If ``group_ids`` cardinality - is 0. - - """ - validate_groups_cardinality(group_ids, mapping_id) - validate_groups_in_backend(group_ids, mapping_id, identity_api) - - -# TODO(marek-denis): Optimize this function, so the number of calls to the -# backend are minimized. -def transform_to_group_ids(group_names, mapping_id, - identity_api, resource_api): - """Transform groups identified by name/domain to their ids - - Function accepts list of groups identified by a name and domain giving - a list of group ids in return. - - Example of group_names parameter:: - - [ - { - "name": "group_name", - "domain": { - "id": "domain_id" - }, - }, - { - "name": "group_name_2", - "domain": { - "name": "domain_name" - } - } - ] - - :param group_names: list of group identified by name and its domain. - :type group_names: list - - :param mapping_id: id of the mapping used for mapping assertion into - local credentials - :type mapping_id: str - - :param identity_api: identity_api object - :param resource_api: resource manager object - - :returns: generator object with group ids - - :raises keystone.exception.MappedGroupNotFound: in case asked group doesn't - exist in the backend. - - """ - def resolve_domain(domain): - """Return domain id. - - Input is a dictionary with a domain identified either by a ``id`` or a - ``name``. In the latter case system will attempt to fetch domain object - from the backend. - - :returns: domain's id - :rtype: str - - """ - domain_id = (domain.get('id') or - resource_api.get_domain_by_name( - domain.get('name')).get('id')) - return domain_id - - for group in group_names: - try: - group_dict = identity_api.get_group_by_name( - group['name'], resolve_domain(group['domain'])) - yield group_dict['id'] - except exception.GroupNotFound: - LOG.debug('Skip mapping group %s; has no entry in the backend', - group['name']) - - -def get_assertion_params_from_env(context): - LOG.debug('Environment variables: %s', context['environment']) - prefix = CONF.federation.assertion_prefix - for k, v in list(context['environment'].items()): - if not k.startswith(prefix): - continue - # These bytes may be decodable as ISO-8859-1 according to Section - # 3.2.4 of RFC 7230. Let's assume that our web server plugins are - # correctly encoding the data. - if not isinstance(v, six.text_type) and getattr(v, 'decode', False): - v = v.decode('ISO-8859-1') - yield (k, v) - - -class RuleProcessor(object): - """A class to process assertions and mapping rules.""" - - class _EvalType(object): - """Mapping rule evaluation types.""" - - ANY_ONE_OF = 'any_one_of' - NOT_ANY_OF = 'not_any_of' - BLACKLIST = 'blacklist' - WHITELIST = 'whitelist' - - def __init__(self, mapping_id, rules): - """Initialize RuleProcessor. - - Example rules can be found at: - :class:`keystone.tests.mapping_fixtures` - - :param mapping_id: id for the mapping - :type mapping_id: string - :param rules: rules from a mapping - :type rules: dict - - """ - self.mapping_id = mapping_id - self.rules = rules - - def process(self, assertion_data): - """Transform assertion to a dictionary. - - The dictionary contains mapping of user name and group ids - based on mapping rules. - - This function will iterate through the mapping rules to find - assertions that are valid. - - :param assertion_data: an assertion containing values from an IdP - :type assertion_data: dict - - Example assertion_data:: - - { - 'Email': 'testacct@example.com', - 'UserName': 'testacct', - 'FirstName': 'Test', - 'LastName': 'Account', - 'orgPersonType': 'Tester' - } - - :returns: dictionary with user and group_ids - - The expected return structure is:: - - { - 'name': 'foobar', - 'group_ids': ['abc123', 'def456'], - 'group_names': [ - { - 'name': 'group_name_1', - 'domain': { - 'name': 'domain1' - } - }, - { - 'name': 'group_name_1_1', - 'domain': { - 'name': 'domain1' - } - }, - { - 'name': 'group_name_2', - 'domain': { - 'id': 'xyz132' - } - } - ] - } - - """ - # Assertions will come in as string key-value pairs, and will use a - # semi-colon to indicate multiple values, i.e. groups. - # This will create a new dictionary where the values are arrays, and - # any multiple values are stored in the arrays. - LOG.debug('assertion data: %s', assertion_data) - assertion = {n: v.split(';') for n, v in assertion_data.items() - if isinstance(v, six.string_types)} - LOG.debug('assertion: %s', assertion) - identity_values = [] - - LOG.debug('rules: %s', self.rules) - for rule in self.rules: - direct_maps = self._verify_all_requirements(rule['remote'], - assertion) - - # If the compare comes back as None, then the rule did not apply - # to the assertion data, go on to the next rule - if direct_maps is None: - continue - - # If there are no direct mappings, then add the local mapping - # directly to the array of saved values. However, if there is - # a direct mapping, then perform variable replacement. - if not direct_maps: - identity_values += rule['local'] - else: - for local in rule['local']: - new_local = self._update_local_mapping(local, direct_maps) - identity_values.append(new_local) - - LOG.debug('identity_values: %s', identity_values) - mapped_properties = self._transform(identity_values) - LOG.debug('mapped_properties: %s', mapped_properties) - return mapped_properties - - def _transform(self, identity_values): - """Transform local mappings, to an easier to understand format. - - Transform the incoming array to generate the return value for - the process function. Generating content for Keystone tokens will - be easier if some pre-processing is done at this level. - - :param identity_values: local mapping from valid evaluations - :type identity_values: array of dict - - Example identity_values:: - - [ - { - 'group': {'id': '0cd5e9'}, - 'user': { - 'email': 'bob@example.com' - }, - }, - { - 'groups': ['member', 'admin', tester'], - 'domain': { - 'name': 'default_domain' - } - }, - { - 'group_ids': ['abc123', 'def456', '0cd5e9'] - } - ] - - :returns: dictionary with user name, group_ids and group_names. - :rtype: dict - - """ - def extract_groups(groups_by_domain): - for groups in list(groups_by_domain.values()): - for group in list({g['name']: g for g in groups}.values()): - yield group - - def normalize_user(user): - """Parse and validate user mapping.""" - user_type = user.get('type') - - if user_type and user_type not in (UserType.EPHEMERAL, - UserType.LOCAL): - msg = _("User type %s not supported") % user_type - raise exception.ValidationError(msg) - - if user_type is None: - user_type = user['type'] = UserType.EPHEMERAL - - if user_type == UserType.EPHEMERAL: - user['domain'] = { - 'id': CONF.federation.federated_domain_name - } - - # initialize the group_ids as a set to eliminate duplicates - user = {} - group_ids = set() - group_names = list() - groups_by_domain = dict() - - # if mapping yield no valid identity values, we should bail right away - # instead of continuing on with a normalized bogus user - if not identity_values: - msg = _("Could not map any federated user properties to identity " - "values. Check debug logs or the mapping used for " - "additional details.") - LOG.warning(msg) - raise exception.ValidationError(msg) - - for identity_value in identity_values: - if 'user' in identity_value: - # if a mapping outputs more than one user name, log it - if user: - LOG.warning(_LW('Ignoring user name')) - else: - user = identity_value.get('user') - if 'group' in identity_value: - group = identity_value['group'] - if 'id' in group: - group_ids.add(group['id']) - elif 'name' in group: - domain = (group['domain'].get('name') or - group['domain'].get('id')) - groups_by_domain.setdefault(domain, list()).append(group) - group_names.extend(extract_groups(groups_by_domain)) - if 'groups' in identity_value: - if 'domain' not in identity_value: - msg = _("Invalid rule: %(identity_value)s. Both 'groups' " - "and 'domain' keywords must be specified.") - msg = msg % {'identity_value': identity_value} - raise exception.ValidationError(msg) - # In this case, identity_value['groups'] is a string - # representation of a list, and we want a real list. This is - # due to the way we do direct mapping substitutions today (see - # function _update_local_mapping() ) - try: - group_names_list = ast.literal_eval( - identity_value['groups']) - except ValueError: - group_names_list = [identity_value['groups']] - domain = identity_value['domain'] - group_dicts = [{'name': name, 'domain': domain} for name in - group_names_list] - - group_names.extend(group_dicts) - if 'group_ids' in identity_value: - # If identity_values['group_ids'] is a string representation - # of a list, parse it to a real list. Also, if the provided - # group_ids parameter contains only one element, it will be - # parsed as a simple string, and not a list or the - # representation of a list. - try: - group_ids.update( - ast.literal_eval(identity_value['group_ids'])) - except (ValueError, SyntaxError): - group_ids.update([identity_value['group_ids']]) - - normalize_user(user) - - return {'user': user, - 'group_ids': list(group_ids), - 'group_names': group_names} - - def _update_local_mapping(self, local, direct_maps): - """Replace any {0}, {1} ... values with data from the assertion. - - :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.federation.utils.DirectMaps - - Example local:: - - {'user': {'name': '{0} {1}', 'email': '{2}'}} - - Example direct_maps:: - - ['Bob', 'Thompson', 'bob@example.com'] - - :returns: new local mapping reference with replaced values. - - The expected return structure is:: - - {'user': {'name': 'Bob Thompson', 'email': 'bob@example.org'}} - - :raises keystone.exception.DirectMappingError: when referring to a - remote match from a local section of a rule - - """ - LOG.debug('direct_maps: %s', direct_maps) - LOG.debug('local: %s', local) - new = {} - for k, v in local.items(): - if isinstance(v, dict): - new_value = self._update_local_mapping(v, direct_maps) - else: - try: - new_value = v.format(*direct_maps) - except IndexError: - raise exception.DirectMappingError( - mapping_id=self.mapping_id) - - new[k] = new_value - return new - - def _verify_all_requirements(self, requirements, assertion): - """Compare remote requirements of a rule against the assertion. - - If a value of ``None`` is returned, the rule with this assertion - doesn't apply. - If an array of zero length is returned, then there are no direct - mappings to be performed, but the rule is valid. - Otherwise, then it will first attempt to filter the values according - to blacklist or whitelist rules and finally return the values in - order, to be directly mapped. - - :param requirements: list of remote requirements from rules - :type requirements: list - - Example requirements:: - - [ - { - "type": "UserName" - }, - { - "type": "orgPersonType", - "any_one_of": [ - "Customer" - ] - }, - { - "type": "ADFS_GROUPS", - "whitelist": [ - "g1", "g2", "g3", "g4" - ] - } - ] - - :param assertion: dict of attributes from an IdP - :type assertion: dict - - Example assertion:: - - { - 'UserName': ['testacct'], - 'LastName': ['Account'], - 'orgPersonType': ['Tester'], - 'Email': ['testacct@example.com'], - 'FirstName': ['Test'], - 'ADFS_GROUPS': ['g1', 'g2'] - } - - :returns: identity values used to update local - :rtype: keystone.federation.utils.DirectMaps or None - - """ - direct_maps = DirectMaps() - - for requirement in requirements: - requirement_type = requirement['type'] - direct_map_values = assertion.get(requirement_type) - regex = requirement.get('regex', False) - - if not direct_map_values: - return None - - any_one_values = requirement.get(self._EvalType.ANY_ONE_OF) - if any_one_values is not None: - if self._evaluate_requirement(any_one_values, - direct_map_values, - self._EvalType.ANY_ONE_OF, - regex): - continue - else: - return None - - not_any_values = requirement.get(self._EvalType.NOT_ANY_OF) - if not_any_values is not None: - if self._evaluate_requirement(not_any_values, - direct_map_values, - self._EvalType.NOT_ANY_OF, - regex): - continue - else: - return None - - # If 'any_one_of' or 'not_any_of' are not found, then values are - # within 'type'. Attempt to find that 'type' within the assertion, - # and filter these values if 'whitelist' or 'blacklist' is set. - blacklisted_values = requirement.get(self._EvalType.BLACKLIST) - whitelisted_values = requirement.get(self._EvalType.WHITELIST) - - # If a blacklist or whitelist is used, we want to map to the - # whole list instead of just its values separately. - if blacklisted_values is not None: - direct_map_values = [v for v in direct_map_values - if v not in blacklisted_values] - elif whitelisted_values is not None: - direct_map_values = [v for v in direct_map_values - if v in whitelisted_values] - - direct_maps.add(direct_map_values) - - LOG.debug('updating a direct mapping: %s', direct_map_values) - - return direct_maps - - def _evaluate_values_by_regex(self, values, assertion_values): - for value in values: - for assertion_value in assertion_values: - if re.search(value, assertion_value): - return True - return False - - def _evaluate_requirement(self, values, assertion_values, - eval_type, regex): - """Evaluate the incoming requirement and assertion. - - If the requirement type does not exist in the assertion data, then - return False. If regex is specified, then compare the values and - assertion values. Otherwise, grab the intersection of the values - and use that to compare against the evaluation type. - - :param values: list of allowed values, defined in the requirement - :type values: list - :param assertion_values: The values from the assertion to evaluate - :type assertion_values: list/string - :param eval_type: determine how to evaluate requirements - :type eval_type: string - :param regex: perform evaluation with regex - :type regex: boolean - - :returns: boolean, whether requirement is valid or not. - - """ - if regex: - any_match = self._evaluate_values_by_regex(values, - assertion_values) - else: - any_match = bool(set(values).intersection(set(assertion_values))) - if any_match and eval_type == self._EvalType.ANY_ONE_OF: - return True - if not any_match and eval_type == self._EvalType.NOT_ANY_OF: - return True - - return False - - -def assert_enabled_identity_provider(federation_api, idp_id): - identity_provider = federation_api.get_idp(idp_id) - if identity_provider.get('enabled') is not True: - msg = _('Identity Provider %(idp)s is disabled') % {'idp': idp_id} - LOG.debug(msg) - raise exception.Forbidden(msg) - - -def assert_enabled_service_provider_object(service_provider): - if service_provider.get('enabled') is not True: - sp_id = service_provider['id'] - msg = _('Service Provider %(sp)s is disabled') % {'sp': sp_id} - LOG.debug(msg) - raise exception.Forbidden(msg) |