aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/contrib/federation
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/contrib/federation')
-rw-r--r--keystone-moon/keystone/contrib/federation/backends/sql.py71
-rw-r--r--keystone-moon/keystone/contrib/federation/constants.py15
-rw-r--r--keystone-moon/keystone/contrib/federation/controllers.py142
-rw-r--r--keystone-moon/keystone/contrib/federation/core.py18
-rw-r--r--keystone-moon/keystone/contrib/federation/idp.py128
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py9
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py10
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py6
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py7
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py7
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py8
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py41
-rw-r--r--keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py39
-rw-r--r--keystone-moon/keystone/contrib/federation/routers.py50
-rw-r--r--keystone-moon/keystone/contrib/federation/schema.py3
-rw-r--r--keystone-moon/keystone/contrib/federation/utils.py66
16 files changed, 428 insertions, 192 deletions
diff --git a/keystone-moon/keystone/contrib/federation/backends/sql.py b/keystone-moon/keystone/contrib/federation/backends/sql.py
index f2c124d0..ed07c08f 100644
--- a/keystone-moon/keystone/contrib/federation/backends/sql.py
+++ b/keystone-moon/keystone/contrib/federation/backends/sql.py
@@ -17,6 +17,7 @@ from oslo_serialization import jsonutils
from keystone.common import sql
from keystone.contrib.federation import core
from keystone import exception
+from sqlalchemy import orm
class FederationProtocolModel(sql.ModelBase, sql.DictBase):
@@ -44,13 +45,53 @@ class FederationProtocolModel(sql.ModelBase, sql.DictBase):
class IdentityProviderModel(sql.ModelBase, sql.DictBase):
__tablename__ = 'identity_provider'
- attributes = ['id', 'remote_id', 'enabled', 'description']
- mutable_attributes = frozenset(['description', 'enabled', 'remote_id'])
+ attributes = ['id', 'enabled', 'description', 'remote_ids']
+ mutable_attributes = frozenset(['description', 'enabled', 'remote_ids'])
id = sql.Column(sql.String(64), primary_key=True)
- remote_id = sql.Column(sql.String(256), nullable=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):
@@ -75,6 +116,7 @@ class MappingModel(sql.ModelBase, sql.DictBase):
@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):
@@ -82,20 +124,23 @@ class MappingModel(sql.ModelBase, sql.DictBase):
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', 'sp_url']
+ attributes = ['auth_url', 'id', 'enabled', 'description',
+ 'relay_state_prefix', 'sp_url']
mutable_attributes = frozenset(['auth_url', 'description', 'enabled',
- 'sp_url'])
+ '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):
@@ -123,6 +168,7 @@ class Federation(core.Driver):
def delete_idp(self, idp_id):
with sql.transaction() as session:
+ self._delete_assigned_protocols(session, idp_id)
idp_ref = self._get_idp(session, idp_id)
session.delete(idp_ref)
@@ -133,7 +179,7 @@ class Federation(core.Driver):
return idp_ref
def _get_idp_from_remote_id(self, session, remote_id):
- q = session.query(IdentityProviderModel)
+ q = session.query(IdPRemoteIdsModel)
q = q.filter_by(remote_id=remote_id)
try:
return q.one()
@@ -153,8 +199,8 @@ class Federation(core.Driver):
def get_idp_from_remote_id(self, remote_id):
with sql.transaction() as session:
- idp_ref = self._get_idp_from_remote_id(session, remote_id)
- return idp_ref.to_dict()
+ ref = self._get_idp_from_remote_id(session, remote_id)
+ return ref.to_dict()
def update_idp(self, idp_id, idp):
with sql.transaction() as session:
@@ -214,6 +260,11 @@ class Federation(core.Driver):
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)
@@ -225,7 +276,7 @@ class Federation(core.Driver):
def create_mapping(self, mapping_id, mapping):
ref = {}
ref['id'] = mapping_id
- ref['rules'] = jsonutils.dumps(mapping.get('rules'))
+ ref['rules'] = mapping.get('rules')
with sql.transaction() as session:
mapping_ref = MappingModel.from_dict(ref)
session.add(mapping_ref)
@@ -250,7 +301,7 @@ class Federation(core.Driver):
def update_mapping(self, mapping_id, mapping):
ref = {}
ref['id'] = mapping_id
- ref['rules'] = jsonutils.dumps(mapping.get('rules'))
+ ref['rules'] = mapping.get('rules')
with sql.transaction() as session:
mapping_ref = self._get_mapping(session, mapping_id)
old_mapping = mapping_ref.to_dict()
diff --git a/keystone-moon/keystone/contrib/federation/constants.py b/keystone-moon/keystone/contrib/federation/constants.py
new file mode 100644
index 00000000..afb38494
--- /dev/null
+++ b/keystone-moon/keystone/contrib/federation/constants.py
@@ -0,0 +1,15 @@
+# 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/contrib/federation/controllers.py b/keystone-moon/keystone/contrib/federation/controllers.py
index 6066a33f..912d45d5 100644
--- a/keystone-moon/keystone/contrib/federation/controllers.py
+++ b/keystone-moon/keystone/contrib/federation/controllers.py
@@ -10,7 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""Extensions supporting Federation."""
+"""Workflow logic for the Federation service."""
import string
@@ -55,9 +55,9 @@ class IdentityProvider(_ControllerBase):
collection_name = 'identity_providers'
member_name = 'identity_provider'
- _mutable_parameters = frozenset(['description', 'enabled', 'remote_id'])
+ _mutable_parameters = frozenset(['description', 'enabled', 'remote_ids'])
_public_parameters = frozenset(['id', 'enabled', 'description',
- 'remote_id', 'links'
+ 'remote_ids', 'links'
])
@classmethod
@@ -247,6 +247,36 @@ class MappingController(_ControllerBase):
@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: exception.ValidationError: ``origin`` query parameter was not
+ specified. The URL is deemed invalid.
+ :raises: 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'].get('origin')
+ host = urllib.parse.unquote_plus(origin)
+ else:
+ msg = _('Request must have an origin query parameter')
+ LOG.error(msg)
+ raise exception.ValidationError(msg)
+
+ if host not in CONF.federation.trusted_dashboard:
+ 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, identity_provider, protocol):
"""Authenticate from dedicated url endpoint.
@@ -268,33 +298,23 @@ class Auth(auth_controllers.Auth):
def federated_sso_auth(self, context, protocol_id):
try:
- remote_id_name = CONF.federation.remote_id_attribute
+ 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)
- if 'origin' in context['query_string']:
- origin = context['query_string'].get('origin')
- host = urllib.parse.unquote_plus(origin)
- else:
- msg = _('Request must have an origin query parameter')
- LOG.error(msg)
- raise exception.ValidationError(msg)
+ host = self._get_sso_origin_host(context)
- if host in CONF.federation.trusted_dashboard:
- ref = self.federation_api.get_idp_from_remote_id(remote_id)
- identity_provider = ref['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)
- else:
- msg = _('%(host)s is not a trusted dashboard host')
- msg = msg % {'host': host}
- LOG.error(msg)
- raise exception.Unauthorized(msg)
+ 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 render_html_response(self, host, token_id):
"""Forms an HTML Form from a template with autosubmit."""
@@ -309,45 +329,77 @@ class Auth(auth_controllers.Auth):
return webob.Response(body=body, status='200',
headerlist=headers)
- @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
- """
-
+ 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.get('sp_url')
- auth_url = service_provider.get('auth_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)
- subject = token_ref.user_name
- roles = token_ref.role_names
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, roles,
- project)
+ 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=[('Content-Type', 'text/xml'),
- ('X-sp-url',
- six.binary_type(sp_url)),
- ('X-auth-url',
- six.binary_type(auth_url))])
+ 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.get('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')
@@ -404,15 +456,17 @@ class ServiceProvider(_ControllerBase):
member_name = 'service_provider'
_mutable_parameters = frozenset(['auth_url', 'description', 'enabled',
- 'sp_url'])
+ 'relay_state_prefix', 'sp_url'])
_public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description',
- 'links', 'sp_url'])
+ '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)
ServiceProvider.check_immutable_params(service_provider)
sp_ref = self.federation_api.create_sp(sp_id, service_provider)
response = ServiceProvider.wrap_member(context, sp_ref)
diff --git a/keystone-moon/keystone/contrib/federation/core.py b/keystone-moon/keystone/contrib/federation/core.py
index b596cff7..2ab75ecb 100644
--- a/keystone-moon/keystone/contrib/federation/core.py
+++ b/keystone-moon/keystone/contrib/federation/core.py
@@ -10,7 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""Extension supporting Federation."""
+"""Main entry point into the Federation service."""
import abc
@@ -21,6 +21,7 @@ import six
from keystone.common import dependency
from keystone.common import extension
from keystone.common import manager
+from keystone.contrib.federation import utils
from keystone import exception
@@ -41,11 +42,6 @@ EXTENSION_DATA = {
extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
-FEDERATION = 'OS-FEDERATION'
-IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider'
-PROTOCOL = 'OS-FEDERATION:protocol'
-FEDERATED_DOMAIN_KEYWORD = 'Federated'
-
@dependency.provider('federation_api')
class Manager(manager.Manager):
@@ -55,6 +51,9 @@ class Manager(manager.Manager):
dynamically calls the backend.
"""
+
+ driver_namespace = 'keystone.federation'
+
def __init__(self):
super(Manager, self).__init__(CONF.federation.driver)
@@ -84,6 +83,13 @@ class Manager(manager.Manager):
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(rules)
+ mapped_properties = rule_processor.process(assertion_data)
+ return mapped_properties, mapping['id']
+
@six.add_metaclass(abc.ABCMeta)
class Driver(object):
diff --git a/keystone-moon/keystone/contrib/federation/idp.py b/keystone-moon/keystone/contrib/federation/idp.py
index bf400135..739fc01a 100644
--- a/keystone-moon/keystone/contrib/federation/idp.py
+++ b/keystone-moon/keystone/contrib/federation/idp.py
@@ -17,17 +17,24 @@ 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
-import xmldsig
+xmldsig = importutils.try_import("saml2.xmldsig")
+if not xmldsig:
+ xmldsig = importutils.try_import("xmldsig")
+from keystone.common import utils
from keystone import exception
from keystone.i18n import _, _LE
-from keystone.openstack.common import fileutils
LOG = log.getLogger(__name__)
@@ -40,8 +47,8 @@ class SAMLGenerator(object):
def __init__(self):
self.assertion_id = uuid.uuid4().hex
- def samlize_token(self, issuer, recipient, user, roles, project,
- expires_in=None):
+ 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
@@ -50,10 +57,14 @@ class SAMLGenerator(object):
: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
@@ -64,8 +75,8 @@ class SAMLGenerator(object):
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, roles,
- project)
+ 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()
@@ -84,7 +95,7 @@ class SAMLGenerator(object):
expires_in = CONF.saml.assertion_expiration_time
now = timeutils.utcnow()
future = now + datetime.timedelta(seconds=expires_in)
- return timeutils.isotime(future, subsecond=True)
+ return utils.isotime(future, subsecond=True)
def _create_status(self):
"""Create an object that represents a SAML Status.
@@ -150,58 +161,64 @@ class SAMLGenerator(object):
subject.name_id = name_id
return subject
- def _create_attribute_statement(self, user, roles, project):
+ def _create_attribute_statement(self, user, user_domain_name, roles,
+ project, project_domain_name):
"""Create an object that represents a SAML AttributeStatement.
- <ns0:AttributeStatement
- xmlns:ns0="urn:oasis:names:tc:SAML:2.0:assertion"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <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_projects">
+ <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>
:return: XML <AttributeStatement> object
"""
- openstack_user = 'openstack_user'
- user_attribute = saml.Attribute()
- user_attribute.name = openstack_user
- user_value = saml.AttributeValue()
- user_value.set_text(user)
- user_attribute.attribute_value = user_value
-
- openstack_roles = 'openstack_roles'
- roles_attribute = saml.Attribute()
- roles_attribute.name = openstack_roles
-
- for role in roles:
- role_value = saml.AttributeValue()
- role_value.set_text(role)
- roles_attribute.attribute_value.append(role_value)
-
- openstack_project = 'openstack_project'
- project_attribute = saml.Attribute()
- project_attribute.name = openstack_project
- project_value = saml.AttributeValue()
- project_value.set_text(project)
- project_attribute.attribute_value = project_value
+
+ 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):
@@ -224,7 +241,7 @@ class SAMLGenerator(object):
"""
authn_statement = saml.AuthnStatement()
- authn_statement.authn_instant = timeutils.isotime()
+ authn_statement.authn_instant = utils.isotime()
authn_statement.session_index = uuid.uuid4().hex
authn_statement.session_not_on_or_after = expiration_time
@@ -261,7 +278,7 @@ class SAMLGenerator(object):
"""
assertion = saml.Assertion()
assertion.id = self.assertion_id
- assertion.issue_instant = timeutils.isotime()
+ assertion.issue_instant = utils.isotime()
assertion.version = '2.0'
assertion.issuer = issuer
assertion.signature = signature
@@ -289,7 +306,7 @@ class SAMLGenerator(object):
response = samlp.Response()
response.id = uuid.uuid4().hex
response.destination = recipient
- response.issue_instant = timeutils.isotime()
+ response.issue_instant = utils.isotime()
response.version = '2.0'
response.issuer = issuer
response.status = status
@@ -397,6 +414,7 @@ def _sign_assertion(assertion):
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
@@ -405,15 +423,19 @@ def _sign_assertion(assertion):
nspair={'saml': saml2.NAMESPACE,
'xmldsig': xmldsig.NAMESPACE}))
command_list.append(file_path)
- stdout = subprocess.check_output(command_list)
+ stdout = subprocess.check_output(command_list,
+ stderr=subprocess.STDOUT)
except Exception as e:
msg = _LE('Error when signing assertion, reason: %(reason)s')
msg = msg % {'reason': e}
+ if hasattr(e, 'output'):
+ msg += ' output: %(output)s' % {'output': e.output}
LOG.error(msg)
raise exception.SAMLSigningError(reason=e)
finally:
try:
- os.remove(file_path)
+ if file_path:
+ os.remove(file_path)
except OSError:
pass
@@ -556,3 +578,31 @@ class MetadataGenerator(object):
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/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py
index cfb6f2c4..9a4d574b 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py
@@ -40,12 +40,3 @@ def upgrade(migrate_engine):
mysql_charset='utf8')
federation_protocol_table.create(migrate_engine, checkfirst=True)
-
-
-def downgrade(migrate_engine):
- meta = sql.MetaData()
- meta.bind = migrate_engine
- tables = ['federation_protocol', 'identity_provider']
- for table_name in tables:
- table = sql.Table(table_name, meta, autoload=True)
- table.drop()
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py
index f827f9a9..9a155f5c 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py
@@ -25,13 +25,3 @@ def upgrade(migrate_engine):
mysql_engine='InnoDB',
mysql_charset='utf8')
mapping_table.create(migrate_engine, checkfirst=True)
-
-
-def downgrade(migrate_engine):
- meta = sql.MetaData()
- meta.bind = migrate_engine
- # Drop previously created tables
- tables = ['mapping']
- for table_name in tables:
- table = sql.Table(table_name, meta, autoload=True)
- table.drop()
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py
index eb8b2378..1731b0d3 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py
@@ -27,9 +27,3 @@ def upgrade(migrate_engine):
values(mapping_id=''))
migrate_engine.execute(stmt)
federation_protocol.c.mapping_id.alter(nullable=False)
-
-
-def downgrade(migrate_engine):
- meta = sa.MetaData(bind=migrate_engine)
- federation_protocol = sa.Table('federation_protocol', meta, autoload=True)
- federation_protocol.c.mapping_id.alter(nullable=True)
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py
index dbe5d1f1..2e0aaf93 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py
@@ -21,10 +21,3 @@ def upgrade(migrate_engine):
idp_table = utils.get_table(migrate_engine, 'identity_provider')
remote_id = sql.Column('remote_id', sql.String(256), nullable=True)
idp_table.create_column(remote_id)
-
-
-def downgrade(migrate_engine):
- meta = sql.MetaData()
- meta.bind = migrate_engine
- idp_table = utils.get_table(migrate_engine, 'identity_provider')
- idp_table.drop_column('remote_id')
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py
index bff6a252..1594f893 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py
@@ -29,10 +29,3 @@ def upgrade(migrate_engine):
mysql_charset='utf8')
sp_table.create(migrate_engine, checkfirst=True)
-
-
-def downgrade(migrate_engine):
- meta = sql.MetaData()
- meta.bind = migrate_engine
- table = sql.Table('service_provider', meta, autoload=True)
- table.drop()
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py
index 8a42ce3a..dc18f548 100644
--- a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py
@@ -38,11 +38,3 @@ def upgrade(migrate_engine):
sp_table.c.auth_url.alter(nullable=False)
sp_table.c.sp_url.alter(nullable=False)
-
-
-def downgrade(migrate_engine):
- meta = sql.MetaData()
- meta.bind = migrate_engine
- sp_table = sql.Table(_SP_TABLE_NAME, meta, autoload=True)
- sp_table.c.auth_url.alter(nullable=True)
- sp_table.c.sp_url.alter(nullable=True)
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py
new file mode 100644
index 00000000..cd571245
--- /dev/null
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/007_add_remote_id_table.py
@@ -0,0 +1,41 @@
+# 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 sqlalchemy as orm
+
+
+def upgrade(migrate_engine):
+ meta = orm.MetaData()
+ meta.bind = migrate_engine
+ idp_table = orm.Table('identity_provider', meta, autoload=True)
+ remote_id_table = orm.Table(
+ 'idp_remote_ids',
+ meta,
+ orm.Column('idp_id',
+ orm.String(64),
+ orm.ForeignKey('identity_provider.id',
+ ondelete='CASCADE')),
+ orm.Column('remote_id',
+ orm.String(255),
+ primary_key=True),
+ mysql_engine='InnoDB',
+ mysql_charset='utf8')
+
+ remote_id_table.create(migrate_engine, checkfirst=True)
+
+ select = orm.sql.select([idp_table.c.id, idp_table.c.remote_id])
+ for identity in migrate_engine.execute(select):
+ remote_idp_entry = {'idp_id': identity.id,
+ 'remote_id': identity.remote_id}
+ remote_id_table.insert(remote_idp_entry).execute()
+
+ idp_table.drop_column('remote_id')
diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py
new file mode 100644
index 00000000..150dcfed
--- /dev/null
+++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/008_add_relay_state_to_sp.py
@@ -0,0 +1,39 @@
+# 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_config import cfg
+from oslo_db.sqlalchemy import utils
+import sqlalchemy as sql
+
+
+CONF = cfg.CONF
+_SP_TABLE_NAME = 'service_provider'
+_RELAY_STATE_PREFIX = 'relay_state_prefix'
+
+
+def upgrade(migrate_engine):
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+
+ idp_table = utils.get_table(migrate_engine, _SP_TABLE_NAME)
+ relay_state_prefix_default = CONF.saml.relay_state_prefix
+ relay_state_prefix = sql.Column(_RELAY_STATE_PREFIX, sql.String(256),
+ nullable=False,
+ server_default=relay_state_prefix_default)
+ idp_table.create_column(relay_state_prefix)
+
+
+def downgrade(migrate_engine):
+ meta = sql.MetaData()
+ meta.bind = migrate_engine
+ idp_table = utils.get_table(migrate_engine, _SP_TABLE_NAME)
+ idp_table.drop_column(_RELAY_STATE_PREFIX)
diff --git a/keystone-moon/keystone/contrib/federation/routers.py b/keystone-moon/keystone/contrib/federation/routers.py
index 9a6224b7..d8fa8175 100644
--- a/keystone-moon/keystone/contrib/federation/routers.py
+++ b/keystone-moon/keystone/contrib/federation/routers.py
@@ -36,44 +36,45 @@ class FederationExtension(wsgi.V3ExtensionRouter):
The API looks like::
- PUT /OS-FEDERATION/identity_providers/$identity_provider
+ PUT /OS-FEDERATION/identity_providers/{idp_id}
GET /OS-FEDERATION/identity_providers
- GET /OS-FEDERATION/identity_providers/$identity_provider
- DELETE /OS-FEDERATION/identity_providers/$identity_provider
- PATCH /OS-FEDERATION/identity_providers/$identity_provider
+ 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/
- $identity_provider/protocols/$protocol
+ {idp_id}/protocols/{protocol_id}
GET /OS-FEDERATION/identity_providers/
- $identity_provider/protocols
+ {idp_id}/protocols
GET /OS-FEDERATION/identity_providers/
- $identity_provider/protocols/$protocol
+ {idp_id}/protocols/{protocol_id}
PATCH /OS-FEDERATION/identity_providers/
- $identity_provider/protocols/$protocol
+ {idp_id}/protocols/{protocol_id}
DELETE /OS-FEDERATION/identity_providers/
- $identity_provider/protocols/$protocol
+ {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
+ 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/$service_provider
+ PUT /OS-FEDERATION/service_providers/{sp_id}
GET /OS-FEDERATION/service_providers
- GET /OS-FEDERATION/service_providers/$service_provider
- DELETE /OS-FEDERATION/service_providers/$service_provider
- PATCH /OS-FEDERATION/service_providers/$service_provider
+ GET /OS-FEDERATION/service_providers/{sp_id}
+ DELETE /OS-FEDERATION/service_providers/{sp_id}
+ PATCH /OS-FEDERATION/service_providers/{sp_id}
- GET /OS-FEDERATION/identity_providers/$identity_provider/
- protocols/$protocol/auth
- POST /OS-FEDERATION/identity_providers/$identity_provider/
- protocols/$protocol/auth
+ GET /OS-FEDERATION/identity_providers/{identity_provider}/
+ protocols/{protocol}/auth
+ POST /OS-FEDERATION/identity_providers/{identity_provider}/
+ protocols/{protocol}/auth
POST /auth/OS-FEDERATION/saml2
+ POST /auth/OS-FEDERATION/saml2/ecp
GET /OS-FEDERATION/saml2/metadata
GET /auth/OS-FEDERATION/websso/{protocol_id}
@@ -191,6 +192,8 @@ class FederationExtension(wsgi.V3ExtensionRouter):
path=self._construct_url('projects'),
get_action='list_projects_for_groups',
rel=build_resource_relation(resource_name='projects'))
+
+ # Auth operations
self._add_resource(
mapper, auth_controller,
path=self._construct_url('identity_providers/{identity_provider}/'
@@ -202,8 +205,6 @@ class FederationExtension(wsgi.V3ExtensionRouter):
'identity_provider': IDP_ID_PARAMETER_RELATION,
'protocol': PROTOCOL_ID_PARAMETER_RELATION,
})
-
- # Auth operations
self._add_resource(
mapper, auth_controller,
path='/auth' + self._construct_url('saml2'),
@@ -211,6 +212,11 @@ class FederationExtension(wsgi.V3ExtensionRouter):
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'),
diff --git a/keystone-moon/keystone/contrib/federation/schema.py b/keystone-moon/keystone/contrib/federation/schema.py
index 645e1129..17818a98 100644
--- a/keystone-moon/keystone/contrib/federation/schema.py
+++ b/keystone-moon/keystone/contrib/federation/schema.py
@@ -58,7 +58,8 @@ _service_provider_properties = {
'auth_url': parameter_types.url,
'sp_url': parameter_types.url,
'description': validation.nullable(parameter_types.description),
- 'enabled': parameter_types.boolean
+ 'enabled': parameter_types.boolean,
+ 'relay_state_prefix': validation.nullable(parameter_types.description)
}
service_provider_create = {
diff --git a/keystone-moon/keystone/contrib/federation/utils.py b/keystone-moon/keystone/contrib/federation/utils.py
index 939fe9a0..b0db3cdd 100644
--- a/keystone-moon/keystone/contrib/federation/utils.py
+++ b/keystone-moon/keystone/contrib/federation/utils.py
@@ -21,7 +21,6 @@ from oslo_log import log
from oslo_utils import timeutils
import six
-from keystone.contrib import federation
from keystone import exception
from keystone.i18n import _, _LW
@@ -191,14 +190,37 @@ def validate_groups_cardinality(group_ids, mapping_id):
raise exception.MissingGroups(mapping_id=mapping_id)
-def validate_idp(idp, assertion):
- """Check if the IdP providing the assertion is the one registered for
- the mapping
+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:
+ 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):
+ """Validate the IdP providing the assertion is registered for the mapping.
"""
- remote_id_parameter = CONF.federation.remote_id_attribute
- if not remote_id_parameter or not idp['remote_id']:
- LOG.warning(_LW('Impossible to identify the IdP %s '),
- idp['id'])
+
+ 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
@@ -206,10 +228,9 @@ def validate_idp(idp, assertion):
idp_remote_identifier = assertion[remote_id_parameter]
except KeyError:
msg = _('Could not find Identity Provider identifier in '
- 'environment, check [federation] remote_id_attribute '
- 'for details.')
+ 'environment')
raise exception.ValidationError(msg)
- if idp_remote_identifier != idp['remote_id']:
+ if idp_remote_identifier not in idp['remote_ids']:
msg = _('Incoming identity provider identifier not included '
'among the accepted identifiers.')
raise exception.Forbidden(msg)
@@ -265,7 +286,7 @@ def validate_groups(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, assignment_api):
+ identity_api, resource_api):
"""Transform groups identitified by name/domain to their ids
Function accepts list of groups identified by a name and domain giving
@@ -296,7 +317,7 @@ def transform_to_group_ids(group_names, mapping_id,
:type mapping_id: str
:param identity_api: identity_api object
- :param assignment_api: assignment_api object
+ :param resource_api: resource manager object
:returns: generator object with group ids
@@ -317,7 +338,7 @@ def transform_to_group_ids(group_names, mapping_id,
"""
domain_id = (domain.get('id') or
- assignment_api.get_domain_by_name(
+ resource_api.get_domain_by_name(
domain.get('name')).get('id'))
return domain_id
@@ -334,7 +355,7 @@ def transform_to_group_ids(group_names, mapping_id,
def get_assertion_params_from_env(context):
LOG.debug('Environment variables: %s', context['environment'])
prefix = CONF.federation.assertion_prefix
- for k, v in context['environment'].items():
+ for k, v in list(context['environment'].items()):
if k.startswith(prefix):
yield (k, v)
@@ -487,8 +508,8 @@ class RuleProcessor(object):
"""
def extract_groups(groups_by_domain):
- for groups in groups_by_domain.values():
- for group in {g['name']: g for g in groups}.values():
+ 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):
@@ -506,8 +527,7 @@ class RuleProcessor(object):
if user_type == UserType.EPHEMERAL:
user['domain'] = {
- 'id': (CONF.federation.federated_domain_name or
- federation.FEDERATED_DOMAIN_KEYWORD)
+ 'id': CONF.federation.federated_domain_name
}
# initialize the group_ids as a set to eliminate duplicates
@@ -586,7 +606,7 @@ class RuleProcessor(object):
LOG.debug('direct_maps: %s', direct_maps)
LOG.debug('local: %s', local)
new = {}
- for k, v in six.iteritems(local):
+ for k, v in local.items():
if isinstance(v, dict):
new_value = self._update_local_mapping(v, direct_maps)
else:
@@ -644,7 +664,7 @@ class RuleProcessor(object):
}
:returns: identity values used to update local
- :rtype: keystone.contrib.federation.utils.DirectMaps
+ :rtype: keystone.contrib.federation.utils.DirectMaps or None
"""
@@ -686,10 +706,10 @@ class RuleProcessor(object):
# If a blacklist or whitelist is used, we want to map to the
# whole list instead of just its values separately.
- if blacklisted_values:
+ 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:
+ elif whitelisted_values is not None:
direct_map_values = [v for v in direct_map_values
if v in whitelisted_values]