summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarek Denis <marek.denis@cern.ch>2014-02-05 00:09:30 +0000
committerMarek Denis <marek.denis@cern.ch>2014-02-27 13:59:24 +0100
commit986c3eb08aa019a5793074fd7bade83972135271 (patch)
treeaf8c3792a200e22cd230ee80ebf7b4dee1ad87a7
parentb5a26b35ac9f05d24d77440f5a5f75d4bf31e69c (diff)
downloadkeystone-986c3eb08aa019a5793074fd7bade83972135271.tar.gz
Support authentication via SAML 2.0 assertions
This patch will support authentication via SAML 2.0 assertions. A new authentication plugin will allow external users to authenticate with keystone, provided the incoming assertion is valid. The file keystone/contrib/federation/controllers.py was extended with two new controllers.V3Controller classes: *) DomainV3 which handles /v3/OS-FEDERATION/domains API call and returns list of domains a user can access based on the provided list of groups. *) ProjectV3 which handles /v3/OS-FEDERATION/projects API call and returns list of project a user can access based on the provided list of groups. Change-Id: I89f70e3a24e825e21580772c088c6fd5c44f3b63 Implements: blueprint saml-id
-rw-r--r--etc/policy.json6
-rw-r--r--etc/policy.v3cloudsample.json6
-rw-r--r--keystone/assignment/backends/kvs.py9
-rw-r--r--keystone/assignment/backends/ldap.py9
-rw-r--r--keystone/assignment/backends/sql.py46
-rw-r--r--keystone/assignment/core.py41
-rw-r--r--keystone/auth/controllers.py5
-rw-r--r--keystone/auth/plugins/saml2.py88
-rw-r--r--keystone/common/authorization.py3
-rw-r--r--keystone/common/sql/core.py1
-rw-r--r--keystone/contrib/federation/backends/sql.py8
-rw-r--r--keystone/contrib/federation/controllers.py47
-rw-r--r--keystone/contrib/federation/core.py20
-rw-r--r--keystone/contrib/federation/routers.py14
-rw-r--r--keystone/contrib/federation/utils.py5
-rw-r--r--keystone/tests/mapping_fixtures.py56
-rw-r--r--keystone/tests/test_overrides.conf3
-rw-r--r--keystone/tests/test_v3_federation.py616
-rw-r--r--keystone/token/providers/common.py62
19 files changed, 1026 insertions, 19 deletions
diff --git a/etc/policy.json b/etc/policy.json
index 4928fb746..8b308df11 100644
--- a/etc/policy.json
+++ b/etc/policy.json
@@ -124,5 +124,9 @@
"identity:get_mapping": "rule:admin_required",
"identity:list_mappings": "rule:admin_required",
"identity:delete_mapping": "rule:admin_required",
- "identity:update_mapping": "rule:admin_required"
+ "identity:update_mapping": "rule:admin_required",
+
+ "identity:list_projects_for_groups": "",
+ "identity:list_domains_for_groups": ""
+
}
diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json
index 984f27142..85527124d 100644
--- a/etc/policy.v3cloudsample.json
+++ b/etc/policy.v3cloudsample.json
@@ -136,5 +136,9 @@
"identity:get_mapping": "rule:admin_required",
"identity:list_mappings": "rule:admin_required",
"identity:delete_mapping": "rule:admin_required",
- "identity:update_mapping": "rule:admin_required"
+ "identity:update_mapping": "rule:admin_required",
+
+ "identity:list_projects_for_groups": "",
+ "identity:list_domains_for_groups": ""
+
}
diff --git a/keystone/assignment/backends/kvs.py b/keystone/assignment/backends/kvs.py
index 7c9cd2964..aec2dbe99 100644
--- a/keystone/assignment/backends/kvs.py
+++ b/keystone/assignment/backends/kvs.py
@@ -160,6 +160,15 @@ class Assignment(kvs.Base, assignment.Driver):
return project_refs
+ def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
+ raise exception.NotImplemented()
+
+ def list_projects_for_groups(self, group_ids):
+ raise exception.NotImplemented()
+
+ def list_domains_for_groups(self, group_ids):
+ raise exception.NotImplemented()
+
def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
self.get_project(tenant_id)
self.get_role(role_id)
diff --git a/keystone/assignment/backends/ldap.py b/keystone/assignment/backends/ldap.py
index 3f7bfb305..6427ea11d 100644
--- a/keystone/assignment/backends/ldap.py
+++ b/keystone/assignment/backends/ldap.py
@@ -143,6 +143,15 @@ class Assignment(assignment.Driver):
return [self._set_default_domain(x) for x in
self.project.get_user_projects(user_dn, associations)]
+ def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
+ raise exception.NotImplemented()
+
+ def list_projects_for_groups(self, group_ids):
+ raise exception.NotImplemented()
+
+ def list_domains_for_groups(self, group_ids):
+ raise exception.NotImplemented()
+
def list_user_ids_for_project(self, tenant_id):
self.get_project(tenant_id)
tenant_dn = self.project._id_to_dn(tenant_id)
diff --git a/keystone/assignment/backends/sql.py b/keystone/assignment/backends/sql.py
index 76b4f7b9c..595658bb5 100644
--- a/keystone/assignment/backends/sql.py
+++ b/keystone/assignment/backends/sql.py
@@ -279,6 +279,52 @@ class Assignment(assignment.Driver):
return _project_ids_to_dicts(session, project_ids)
+ def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
+
+ if project_id is not None:
+ assignment_type = AssignmentType.GROUP_PROJECT
+ target_id = project_id
+ elif domain_id is not None:
+ assignment_type = AssignmentType.GROUP_DOMAIN
+ target_id = domain_id
+ else:
+ raise AttributeError(_("Must specify either domain or project"))
+
+ sql_constraints = sql.and_(
+ RoleAssignment.type == assignment_type,
+ RoleAssignment.target_id == target_id,
+ Role.id == RoleAssignment.role_id,
+ RoleAssignment.actor_id.in_(group_ids))
+
+ session = db_session.get_session()
+ with session.begin():
+ query = session.query(Role).filter(
+ sql_constraints).distinct()
+ return [role.to_dict() for role in query.all()]
+
+ def _list_entities_for_groups(self, group_ids, entity):
+ if entity == Domain:
+ assignment_type = AssignmentType.GROUP_DOMAIN
+ else:
+ assignment_type = AssignmentType.GROUP_PROJECT
+
+ group_sql_conditions = sql.and_(
+ RoleAssignment.type == assignment_type,
+ entity.id == RoleAssignment.target_id,
+ RoleAssignment.actor_id.in_(group_ids))
+
+ session = db_session.get_session()
+ with session.begin():
+ query = session.query(entity).filter(
+ group_sql_conditions)
+ return [x.to_dict() for x in query.all()]
+
+ def list_projects_for_groups(self, group_ids):
+ return self._list_entities_for_groups(group_ids, Project)
+
+ def list_domains_for_groups(self, group_ids):
+ return self._list_entities_for_groups(group_ids, Domain)
+
def add_role_to_user_and_project(self, user_id, tenant_id, role_id):
with sql.transaction() as session:
self._get_project(session, tenant_id)
diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py
index 8d2d5f4d7..325eb2a97 100644
--- a/keystone/assignment/core.py
+++ b/keystone/assignment/core.py
@@ -829,6 +829,47 @@ class Driver(object):
raise exception.NotImplemented()
@abc.abstractmethod
+ def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
+ """List all the roles assigned to groups on either domain or
+ project.
+
+ If the project_id is not None, this value will be used, no matter what
+ was specified in the domain_id.
+
+ :param group_ids: iterable with group ids
+ :param project_id: id of the project
+ :param domain_id: id of the domain
+
+ :raises: AttributeError: In case both project_id and domain_id are set
+ to None
+
+ :returns: a list of Role entities matching groups and
+ project_id or domain_id
+
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
+ def list_projects_for_groups(self, group_ids):
+ """List projects accessible to specified groups.
+
+ :param group_ids: List of group ids.
+ :returns: List of projects accessible to specified groups.
+
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
+ def list_domains_for_groups(self, group_ids):
+ """List domains accessible to specified groups.
+
+ :param group_ids: List of group ids.
+ :returns: List of domains accessible to specified groups.
+
+ """
+ raise exception.NotImplemented()
+
+ @abc.abstractmethod
def get_project(self, project_id):
"""Get a project by ID.
diff --git a/keystone/auth/controllers.py b/keystone/auth/controllers.py
index b9771c975..b75e4d0e3 100644
--- a/keystone/auth/controllers.py
+++ b/keystone/auth/controllers.py
@@ -21,6 +21,7 @@ from keystone.common import controller
from keystone.common import dependency
from keystone.common import wsgi
from keystone import config
+from keystone.contrib import federation
from keystone import exception
from keystone.openstack.common import importutils
from keystone.openstack.common import log
@@ -343,6 +344,10 @@ class Auth(controller.V3Controller):
# scope is specified
return
+ # Skip scoping when unscoped federated token is being issued
+ if federation.IDENTITY_PROVIDER in auth_context:
+ return
+
# fill in default_project_id if it is available
try:
user_ref = self.identity_api.get_user(auth_context['user_id'])
diff --git a/keystone/auth/plugins/saml2.py b/keystone/auth/plugins/saml2.py
new file mode 100644
index 000000000..c7c2d76f4
--- /dev/null
+++ b/keystone/auth/plugins/saml2.py
@@ -0,0 +1,88 @@
+# 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 six.moves.urllib import parse
+
+from keystone import auth
+from keystone.common import dependency
+from keystone import config
+from keystone.contrib import federation
+from keystone.contrib.federation import utils
+from keystone import exception
+from keystone.openstack.common import jsonutils
+from keystone.openstack.common import log
+from keystone.openstack.common import timeutils
+
+
+CONF = config.CONF
+LOG = log.getLogger(__name__)
+
+
+@dependency.requires('token_api', 'federation_api')
+class Saml2(auth.AuthMethodHandler):
+
+ method = 'saml2'
+
+ def authenticate(self, context, auth_payload, auth_context):
+ """Authenticate federated user and return an authentication context.
+
+ :param context: keystone's request context
+ :param auth_payload: the content of the authentication for a
+ given method
+ :param auth_context: user authentication context, a dictionary
+ shared by all plugins.
+
+ In addition to ``user_id`` in ``auth_context``, the ``saml2`` plugin
+ also sets ``group_ids``, ``identity_provider`` and ``protocol``.
+ These values are required for issuing an unscoped federated token.
+ When scoping the federated tokens, the plugin sets
+ ``federated_token``, this entry stores the unscoped token.
+
+ """
+
+ if 'id' in auth_payload:
+ fields = self._handle_scoped_token(auth_payload)
+ else:
+ fields = self._handle_unscoped_token(context, auth_payload)
+
+ auth_context.update(fields)
+
+ def _handle_scoped_token(self, auth_payload):
+ token_ref = self.token_api.get_token(auth_payload['id'])
+ self._validate_expiration(token_ref)
+ groups = token_ref['user'][federation.GROUPS]
+ return {
+ 'user_id': token_ref['user_id'],
+ 'group_ids': [group['id'] for group in groups]
+ }
+
+ def _handle_unscoped_token(self, context, auth_payload):
+ assertion = context['environment']
+
+ identity_provider = auth_payload['identity_provider']
+ protocol = auth_payload['protocol']
+
+ mapping = self.federation_api.get_mapping_from_idp_and_protocol(
+ identity_provider, protocol)
+ rules = jsonutils.loads(mapping['rules'])
+ rule_processor = utils.RuleProcessor(rules)
+ mapped_properties = rule_processor.process(assertion)
+ return {
+ 'user_id': parse.quote(mapped_properties['name']),
+ 'group_ids': mapped_properties['group_ids'],
+ federation.IDENTITY_PROVIDER: identity_provider,
+ federation.PROTOCOL: protocol
+ }
+
+ def _validate_expiration(self, token_ref):
+ if timeutils.utcnow() > token_ref['expires']:
+ raise exception.Unauthorized(_('Federation token is expired'))
diff --git a/keystone/common/authorization.py b/keystone/common/authorization.py
index 3d97b540b..7d324e101 100644
--- a/keystone/common/authorization.py
+++ b/keystone/common/authorization.py
@@ -34,6 +34,7 @@ It is a dictionary with the following attributes:
* ``domain_id`` (optional): domain ID of the scoped domain if auth is
domain-scoped
* ``roles`` (optional): list of role names for the given scope
+* ``group_ids``: list of group IDs for which the API user has membership
"""
@@ -81,6 +82,8 @@ def v3_token_to_auth_context(token):
creds['roles'] = []
for role in token_data['roles']:
creds['roles'].append(role['name'])
+ creds['group_ids'] = [
+ g['id'] for g in token_data['user'].get('OS-FEDERATION:groups', [])]
return creds
diff --git a/keystone/common/sql/core.py b/keystone/common/sql/core.py
index dbb1172b4..ecbb6506f 100644
--- a/keystone/common/sql/core.py
+++ b/keystone/common/sql/core.py
@@ -58,6 +58,7 @@ relationship = sql.orm.relationship
joinedload = sql.orm.joinedload
# Suppress flake8's unused import warning for flag_modified:
flag_modified = flag_modified
+and_ = sql.and_
def initialize():
diff --git a/keystone/contrib/federation/backends/sql.py b/keystone/contrib/federation/backends/sql.py
index b49181e74..b9c50239d 100644
--- a/keystone/contrib/federation/backends/sql.py
+++ b/keystone/contrib/federation/backends/sql.py
@@ -246,3 +246,11 @@ class Federation(core.Driver):
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):
+ session = db_session.get_session()
+ with session.begin():
+ 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()
diff --git a/keystone/contrib/federation/controllers.py b/keystone/contrib/federation/controllers.py
index d3f914998..0c4fd5012 100644
--- a/keystone/contrib/federation/controllers.py
+++ b/keystone/contrib/federation/controllers.py
@@ -12,6 +12,7 @@
"""Extensions supporting Federation."""
+from keystone.common import authorization
from keystone.common import controller
from keystone.common import dependency
from keystone.common import wsgi
@@ -237,3 +238,49 @@ class MappingController(_ControllerBase):
utils.validate_mapping_structure(mapping)
mapping_ref = self.federation_api.update_mapping(mapping_id, mapping)
return MappingController.wrap_member(context, mapping_ref)
+
+
+@dependency.requires('assignment_api')
+class DomainV3(controller.V3Controller):
+ collection_name = 'domains'
+ member_name = 'domain'
+
+ def __init__(self):
+ super(DomainV3, self).__init__()
+ self.get_member_from_driver = self.assignment_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')
+class ProjectV3(controller.V3Controller):
+ collection_name = 'projects'
+ member_name = 'project'
+
+ def __init__(self):
+ super(ProjectV3, self).__init__()
+ self.get_member_from_driver = self.assignment_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 ProjectV3.wrap_collection(context, projects)
diff --git a/keystone/contrib/federation/core.py b/keystone/contrib/federation/core.py
index 4c8df4e61..5c1cd5c88 100644
--- a/keystone/contrib/federation/core.py
+++ b/keystone/contrib/federation/core.py
@@ -41,6 +41,11 @@ EXTENSION_DATA = {
extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
+FEDERATION = 'OS-FEDERATION'
+GROUPS = 'OS-FEDERATION:groups'
+IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider'
+PROTOCOL = 'OS-FEDERATION:protocol'
+
@dependency.provider('federation_api')
class Manager(manager.Manager):
@@ -195,3 +200,18 @@ class Driver(object):
"""
raise exception.NotImplemented()
+
+ @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,
+ keystone.exception.FederatedProtocolNotFound,
+ :returns: mapping_ref
+
+ """
+ raise exception.NotImplemented()
diff --git a/keystone/contrib/federation/routers.py b/keystone/contrib/federation/routers.py
index c38735707..4ce137e10 100644
--- a/keystone/contrib/federation/routers.py
+++ b/keystone/contrib/federation/routers.py
@@ -55,6 +55,8 @@ class FederationExtension(wsgi.ExtensionRouter):
idp_controller = controllers.IdentityProvider()
protocol_controller = controllers.FederationProtocol()
mapping_controller = controllers.MappingController()
+ project_controller = controllers.ProjectV3()
+ domain_controller = controllers.DomainV3()
# Identity Provider CRUD operations
@@ -156,3 +158,15 @@ class FederationExtension(wsgi.ExtensionRouter):
controller=mapping_controller,
action='update_mapping',
conditions=dict(method=['PATCH']))
+
+ mapper.connect(
+ self._construct_url('domains'),
+ controller=domain_controller,
+ action='list_domains_for_groups',
+ conditions=dict(method=['GET']))
+
+ mapper.connect(
+ self._construct_url('projects'),
+ controller=project_controller,
+ action='list_projects_for_groups',
+ conditions=dict(method=['GET']))
diff --git a/keystone/contrib/federation/utils.py b/keystone/contrib/federation/utils.py
index 8c0753c87..646ce992f 100644
--- a/keystone/contrib/federation/utils.py
+++ b/keystone/contrib/federation/utils.py
@@ -191,7 +191,10 @@ class RuleProcessor(object):
new_local = self._update_local_mapping(local, direct_maps)
identity_values.append(new_local)
- return self._transform(identity_values)
+ mapped_properties = self._transform(identity_values)
+ if mapped_properties.get('name') is None:
+ raise exception.Unauthorized(_("Could not map user"))
+ return mapped_properties
def _transform(self, identity_values):
"""Transform local mappings, to an easier to understand format.
diff --git a/keystone/tests/mapping_fixtures.py b/keystone/tests/mapping_fixtures.py
index 0a343b1ca..938fdf7bd 100644
--- a/keystone/tests/mapping_fixtures.py
+++ b/keystone/tests/mapping_fixtures.py
@@ -28,10 +28,18 @@ MAPPING_SMALL = {
"group": {
"id": EMPLOYEE_GROUP_ID
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"not_any_of": [
"Contractor",
@@ -52,10 +60,18 @@ MAPPING_SMALL = {
"group": {
"id": CONTRACTOR_GROUP_ID
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"any_one_of": [
"Contractor",
@@ -148,10 +164,18 @@ MAPPING_LARGE = {
"group": {
"id": DEVELOPER_GROUP_ID
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"any_one_of": [
"Tester"
@@ -278,10 +302,18 @@ MAPPING_EXTRA_REMOTE_PROPS_NOT_ANY_OF = {
"group": {
"id": "0cd5e9"
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"not_any_of": [
"SubContractor"
@@ -301,10 +333,18 @@ MAPPING_EXTRA_REMOTE_PROPS_ANY_ONE_OF = {
"group": {
"id": "0cd5e9"
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"any_one_of": [
"SubContractor"
@@ -324,10 +364,18 @@ MAPPING_EXTRA_REMOTE_PROPS_JUST_TYPE = {
"group": {
"id": "0cd5e9"
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"invalid_type": "xyz"
}
@@ -344,6 +392,11 @@ MAPPING_EXTRA_RULES_PROPS = {
"group": {
"id": "0cd5e9"
}
+ },
+ {
+ "user": {
+ "name": "{0}"
+ }
}
],
"invalid_type": {
@@ -351,6 +404,9 @@ MAPPING_EXTRA_RULES_PROPS = {
},
"remote": [
{
+ "type": "UserName"
+ },
+ {
"type": "orgPersonType",
"not_any_of": [
"SubContractor"
diff --git a/keystone/tests/test_overrides.conf b/keystone/tests/test_overrides.conf
index d1f7bae14..dcef29e44 100644
--- a/keystone/tests/test_overrides.conf
+++ b/keystone/tests/test_overrides.conf
@@ -29,8 +29,9 @@ ca_certs = examples/pki/certs/cacert.pem
backends = keystone.tests.test_kvs.KVSBackendForcedKeyMangleFixture, keystone.tests.test_kvs.KVSBackendFixture
[auth]
-methods = external,password,token,oauth1
+methods = external,password,token,oauth1,saml2
oauth1 = keystone.auth.plugins.oauth1.OAuth
+saml2 = keystone.auth.plugins.saml2.Saml2
[paste_deploy]
config_file = keystone-paste.ini
diff --git a/keystone/tests/test_v3_federation.py b/keystone/tests/test_v3_federation.py
index 291c866a8..76a972695 100644
--- a/keystone/tests/test_v3_federation.py
+++ b/keystone/tests/test_v3_federation.py
@@ -13,10 +13,12 @@
import random
import uuid
+from keystone.auth import controllers as auth_controllers
from keystone.common.sql import migration_helpers
from keystone import config
from keystone import contrib
from keystone.contrib.federation import utils as mapping_utils
+from keystone import exception
from keystone.openstack.common.db.sqlalchemy import migration
from keystone.openstack.common import importutils
from keystone.openstack.common import jsonutils
@@ -600,8 +602,8 @@ class MappingRuleEngineTests(FederationTests):
rp = mapping_utils.RuleProcessor(mapping['rules'])
values = rp.process(assertion)
- fn = mapping_fixtures.ADMIN_ASSERTION.get('FirstName')
- ln = mapping_fixtures.ADMIN_ASSERTION.get('LastName')
+ fn = assertion.get('FirstName')
+ ln = assertion.get('LastName')
full_name = '%s %s' % (fn, ln)
group_ids = values.get('group_ids')
@@ -611,24 +613,21 @@ class MappingRuleEngineTests(FederationTests):
self.assertEqual(name, full_name)
def test_rule_engine_no_regex_match(self):
- """Should return no values, the email of the tester won't match.
+ """Should deny authorization, the email of the tester won't match.
This will not match since the email in the assertion will fail
the regex test. It is set to match any @example.com address.
But the incoming value is set to eviltester@example.org.
+ RuleProcessor should raise exception.Unauthorized exception.
"""
mapping = mapping_fixtures.MAPPING_LARGE
assertion = mapping_fixtures.BAD_TESTER_ASSERTION
rp = mapping_utils.RuleProcessor(mapping['rules'])
- values = rp.process(assertion)
- group_ids = values.get('group_ids')
- name = values.get('name')
-
- self.assertIsNone(name)
- self.assertEqual(group_ids, [])
+ self.assertRaises(exception.Unauthorized,
+ rp.process, assertion)
def test_rule_engine_any_one_of_many_rules(self):
"""Should return group CONTRACTOR_GROUP_ID.
@@ -645,10 +644,11 @@ class MappingRuleEngineTests(FederationTests):
rp = mapping_utils.RuleProcessor(mapping['rules'])
values = rp.process(assertion)
+ user_name = assertion.get('UserName')
group_ids = values.get('group_ids')
name = values.get('name')
- self.assertIsNone(name)
+ self.assertEqual(user_name, name)
self.assertIn(mapping_fixtures.CONTRACTOR_GROUP_ID, group_ids)
def test_rule_engine_not_any_of_and_direct_mapping(self):
@@ -665,7 +665,7 @@ class MappingRuleEngineTests(FederationTests):
rp = mapping_utils.RuleProcessor(mapping['rules'])
values = rp.process(assertion)
- user_name = mapping_fixtures.CUSTOMER_ASSERTION.get('UserName')
+ user_name = assertion.get('UserName')
group_ids = values.get('group_ids')
name = values.get('name')
@@ -685,11 +685,11 @@ class MappingRuleEngineTests(FederationTests):
assertion = mapping_fixtures.EMPLOYEE_ASSERTION
rp = mapping_utils.RuleProcessor(mapping['rules'])
values = rp.process(assertion)
-
+ user_name = assertion.get('UserName')
group_ids = values.get('group_ids')
name = values.get('name')
- self.assertIsNone(name)
+ self.assertEqual(name, user_name)
self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids)
def test_rule_engine_regex_match_and_many_groups(self):
@@ -706,10 +706,596 @@ class MappingRuleEngineTests(FederationTests):
assertion = mapping_fixtures.TESTER_ASSERTION
rp = mapping_utils.RuleProcessor(mapping['rules'])
values = rp.process(assertion)
-
+ user_name = assertion.get('UserName')
group_ids = values.get('group_ids')
name = values.get('name')
- self.assertIsNone(name)
+ self.assertEqual(user_name, name)
self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids)
self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids)
+
+
+class FederatedTokenTests(FederationTests):
+
+ IDP = 'ORG_IDP'
+ PROTOCOL = 'saml2'
+ AUTH_METHOD = 'saml2'
+ USER = 'user@ORGANIZATION'
+
+ UNSCOPED_V3_SAML2_REQ = {
+ "identity": {
+ "methods": [AUTH_METHOD],
+ AUTH_METHOD: {
+ "identity_provider": IDP,
+ "protocol": PROTOCOL
+ }
+ }
+ }
+
+ AUTH_URL = '/auth/tokens'
+
+ def setUp(self):
+ super(FederationTests, self).setUp()
+ self.load_sample_data()
+ self.load_federation_sample_data()
+
+ def idp_ref(self, id=None):
+ idp = {
+ 'id': id or uuid.uuid4().hex,
+ 'enabled': True,
+ 'description': uuid.uuid4().hex
+ }
+ return idp
+
+ def proto_ref(self, mapping_id=None):
+ proto = {
+ 'id': uuid.uuid4().hex,
+ 'mapping_id': mapping_id or uuid.uuid4().hex
+ }
+ return proto
+
+ def mapping_ref(self, rules=None):
+ return {
+ 'id': uuid.uuid4().hex,
+ 'rules': rules or self.rules['rules']
+ }
+
+ def _scope_request(self, unscoped_token_id, scope, scope_id):
+ return {
+ 'auth': {
+ 'identity': {
+ 'methods': [
+ self.AUTH_METHOD
+ ],
+ self.AUTH_METHOD: {
+ 'id': unscoped_token_id
+ }
+ },
+ 'scope': {
+ scope: {
+ 'id': scope_id
+ }
+ }
+ }
+ }
+
+ def _project(self, project):
+ return (project['id'], project['name'])
+
+ def _roles(self, roles):
+ return set([(r['id'], r['name']) for r in roles])
+
+ def _check_projects_and_roles(self, token, roles, projects):
+ """Check whether the projects and the roles match."""
+ token_roles = token.get('roles')
+ if token_roles is None:
+ raise AssertionError('Roles not found in the token')
+ token_roles = self._roles(token_roles)
+ roles_ref = self._roles(roles)
+ self.assertEqual(token_roles, roles_ref)
+
+ token_projects = token.get('project')
+ if token_projects is None:
+ raise AssertionError('Projects not found in the token')
+ token_projects = self._project(token_projects)
+ projects_ref = self._project(projects)
+ self.assertEqual(token_projects, projects_ref)
+
+ def _check_scoped_token_attributes(self, token):
+ def xor_project_domain(iterable):
+ return sum(('project' in iterable, 'domain' in iterable)) % 2
+
+ for obj in ('user', 'catalog', 'expires_at', 'issued_at',
+ 'methods', 'roles'):
+ self.assertIn(obj, token)
+ # Check for either project or domain
+ if not xor_project_domain(token.keys()):
+ raise AssertionError("You must specify either"
+ "project or domain.")
+
+ def _issue_unscoped_token(self, assertion='EMPLOYEE_ASSERTION'):
+ api = auth_controllers.Auth()
+ context = {'environment': {}}
+ self._inject_assertion(context, assertion)
+ r = api.authenticate_for_token(context, self.UNSCOPED_V3_SAML2_REQ)
+ return r
+
+ def test_issue_unscoped_token(self):
+ r = self._issue_unscoped_token()
+ self.assertIsNotNone(r.headers.get('X-Subject-Token'))
+
+ def test_scope_to_project_once(self):
+ r = self.post(self.AUTH_URL,
+ body=self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
+ token_resp = r.result['token']
+ project_id = token_resp['project']['id']
+ self.assertEqual(project_id, self.proj_employees['id'])
+ self._check_scoped_token_attributes(token_resp)
+ roles_ref = [self.role_employee]
+ projects_ref = self.proj_employees
+ self._check_projects_and_roles(token_resp, roles_ref, projects_ref)
+
+ def scope_to_bad_project(self):
+ """Scope unscoped token with a project we don't have access to."""
+
+ self.post(self.AUTH_URL,
+ body=self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER,
+ expected_status=401)
+
+ def test_scope_to_project_multiple_times(self):
+ """Try to scope the unscoped token multiple times.
+
+ The new tokens should be scoped to:
+
+ * Customers' project
+ * Employees' project
+
+ """
+
+ bodies = (self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN,
+ self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN)
+ project_ids = (self.proj_employees['id'],
+ self.proj_customers['id'])
+ for body, project_id_ref in zip(bodies, project_ids):
+ r = self.post(self.AUTH_URL, body=body)
+ token_resp = r.result['token']
+ project_id = token_resp['project']['id']
+ self.assertEqual(project_id, project_id_ref)
+ self._check_scoped_token_attributes(token_resp)
+
+ def test_scope_token_from_nonexistent_unscoped_token(self):
+ """Try to scope token from non-existent unscoped token."""
+ self.post(self.AUTH_URL,
+ body=self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN,
+ expected_status=404)
+
+ def test_issue_token_from_rules_without_user(self):
+ api = auth_controllers.Auth()
+ context = {'environment': {}}
+ self._inject_assertion(context, 'BAD_TESTER_ASSERTION')
+ self.assertRaises(exception.Unauthorized,
+ api.authenticate_for_token,
+ context, self.UNSCOPED_V3_SAML2_REQ)
+
+ def test_issue_token_with_nonexistent_group(self):
+ r = self._issue_unscoped_token(assertion='CONTRACTOR_ASSERTION')
+ groups = r.json['token']['user']['OS-FEDERATION:groups']
+ group_ids = set(g['id'] for g in groups)
+ group_ids_ref = set(['bad_group_id'])
+ self.assertEqual(group_ids, group_ids_ref)
+
+ def test_scope_to_domain_once(self):
+ r = self.post(self.AUTH_URL,
+ body=self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER)
+ token_resp = r.result['token']
+ domain_id = token_resp['domain']['id']
+ self.assertEqual(domain_id, self.domainA['id'])
+ self._check_scoped_token_attributes(token_resp)
+
+ def test_scope_to_domain_multiple_tokens(self):
+ """Issue multiple tokens scoping to different domains.
+
+ The new tokens should be scoped to:
+
+ * domainA
+ * domainB
+ * domainC
+
+ """
+ bodies = (self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN,
+ self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN,
+ self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN)
+ domain_ids = (self.domainA['id'],
+ self.domainB['id'],
+ self.domainC['id'])
+
+ for body, domain_id_ref in zip(bodies, domain_ids):
+ r = self.post(self.AUTH_URL, body=body)
+ token_resp = r.result['token']
+ domain_id = token_resp['domain']['id']
+ self.assertEqual(domain_id, domain_id_ref)
+ self._check_scoped_token_attributes(token_resp)
+
+ def test_list_projects(self):
+ url = '/OS-FEDERATION/projects'
+
+ token = (self.tokens['CUSTOMER_ASSERTION'],
+ self.tokens['EMPLOYEE_ASSERTION'],
+ self.tokens['ADMIN_ASSERTION'])
+
+ projects_refs = (set([self.proj_customers['id']]),
+ set([self.proj_employees['id'],
+ self.project_all['id']]),
+ set([self.proj_employees['id'],
+ self.project_all['id'],
+ self.proj_customers['id']]))
+
+ for token, projects_ref in zip(token, projects_refs):
+ r = self.get(url, token=token)
+ projects_resp = r.result['projects']
+ projects = set(p['id'] for p in projects_resp)
+ self.assertEqual(projects, projects_ref)
+
+ def test_list_domains(self):
+ url = '/OS-FEDERATION/domains'
+
+ tokens = (self.tokens['CUSTOMER_ASSERTION'],
+ self.tokens['EMPLOYEE_ASSERTION'],
+ self.tokens['ADMIN_ASSERTION'])
+
+ domain_refs = (set([self.domainA['id']]),
+ set([self.domainA['id'],
+ self.domainB['id']]),
+ set([self.domainA['id'],
+ self.domainB['id'],
+ self.domainC['id']]))
+
+ for token, domains_ref in zip(tokens, domain_refs):
+ r = self.get(url, token=token)
+ domains_resp = r.result['domains']
+ domains = set(p['id'] for p in domains_resp)
+ self.assertEqual(domains, domains_ref)
+
+ def test_full_workflow(self):
+ """Test 'standard' workflow for granting access tokens.
+
+ * Issue unscoped token
+ * List available projects based on groups
+ * Scope token to a one of available projects
+
+ """
+
+ r = self._issue_unscoped_token()
+ employee_unscoped_token_id = r.headers.get('X-Subject-Token')
+ r = self.get('/OS-FEDERATION/projects',
+ token=employee_unscoped_token_id)
+ projects = r.result['projects']
+ random_project = random.randint(0, len(projects)) - 1
+ project = projects[random_project]
+
+ v3_scope_request = self._scope_request(employee_unscoped_token_id,
+ 'project', project['id'])
+
+ r = self.post(self.AUTH_URL, body=v3_scope_request)
+ token_resp = r.result['token']
+ project_id = token_resp['project']['id']
+ self.assertEqual(project_id, project['id'])
+ self._check_scoped_token_attributes(token_resp)
+
+ def load_federation_sample_data(self):
+ """Inject additional data."""
+
+ # Create and add domains
+ self.domainA = self.new_domain_ref()
+ self.assignment_api.create_domain(self.domainA['id'],
+ self.domainA)
+
+ self.domainB = self.new_domain_ref()
+ self.assignment_api.create_domain(self.domainB['id'],
+ self.domainB)
+
+ self.domainC = self.new_domain_ref()
+ self.assignment_api.create_domain(self.domainC['id'],
+ self.domainC)
+
+ # Create and add projects
+ self.proj_employees = self.new_project_ref(
+ domain_id=self.domainA['id'])
+ self.assignment_api.create_project(self.proj_employees['id'],
+ self.proj_employees)
+ self.proj_customers = self.new_project_ref(
+ domain_id=self.domainA['id'])
+ self.assignment_api.create_project(self.proj_customers['id'],
+ self.proj_customers)
+
+ self.project_all = self.new_project_ref(
+ domain_id=self.domainA['id'])
+ self.assignment_api.create_project(self.project_all['id'],
+ self.project_all)
+
+ # Create and add groups
+ self.group_employees = self.new_group_ref(
+ domain_id=self.domainA['id'])
+ self.identity_api.create_group(self.group_employees['id'],
+ self.group_employees)
+
+ self.group_customers = self.new_group_ref(
+ domain_id=self.domainA['id'])
+ self.identity_api.create_group(self.group_customers['id'],
+ self.group_customers)
+
+ self.group_admins = self.new_group_ref(
+ domain_id=self.domainA['id'])
+ self.identity_api.create_group(self.group_admins['id'],
+ self.group_admins)
+
+ # Create and add roles
+ self.role_employee = self.new_role_ref()
+ self.assignment_api.create_role(self.role_employee['id'],
+ self.role_employee)
+ self.role_customer = self.new_role_ref()
+ self.assignment_api.create_role(self.role_customer['id'],
+ self.role_customer)
+
+ self.role_admin = self.new_role_ref()
+ self.assignment_api.create_role(self.role_admin['id'],
+ self.role_admin)
+
+ # Employees can access
+ # * proj_employees
+ # * project_all
+ self.assignment_api.create_grant(self.role_employee['id'],
+ group_id=self.group_employees['id'],
+ project_id=self.proj_employees['id'])
+ self.assignment_api.create_grant(self.role_employee['id'],
+ group_id=self.group_employees['id'],
+ project_id=self.project_all['id'])
+ # Customers can access
+ # * proj_customers
+ self.assignment_api.create_grant(self.role_customer['id'],
+ group_id=self.group_customers['id'],
+ project_id=self.proj_customers['id'])
+
+ # Admins can access:
+ # * proj_customers
+ # * proj_employees
+ # * project_all
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ project_id=self.proj_customers['id'])
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ project_id=self.proj_employees['id'])
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ project_id=self.project_all['id'])
+
+ self.assignment_api.create_grant(self.role_customer['id'],
+ group_id=self.group_customers['id'],
+ domain_id=self.domainA['id'])
+
+ # Customers can access:
+ # * domain A
+ self.assignment_api.create_grant(self.role_customer['id'],
+ group_id=self.group_customers['id'],
+ domain_id=self.domainA['id'])
+
+ # Employees can access:
+ # * domain A
+ # * domain B
+
+ self.assignment_api.create_grant(self.role_employee['id'],
+ group_id=self.group_employees['id'],
+ domain_id=self.domainA['id'])
+ self.assignment_api.create_grant(self.role_employee['id'],
+ group_id=self.group_employees['id'],
+ domain_id=self.domainB['id'])
+
+ # Admins can access:
+ # * domain A
+ # * domain B
+ # * domain C
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ domain_id=self.domainA['id'])
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ domain_id=self.domainB['id'])
+
+ self.assignment_api.create_grant(self.role_admin['id'],
+ group_id=self.group_admins['id'],
+ domain_id=self.domainC['id'])
+ self.rules = {
+ 'rules': [
+ {
+ 'local': [
+ {
+ 'group': {
+ 'id': self.group_employees['id']
+ }
+ },
+ {
+ 'user': {
+ 'name': '{0}'
+ }
+ }
+ ],
+ 'remote': [
+ {
+ 'type': 'UserName'
+ },
+ {
+ 'type': 'orgPersonType',
+ 'any_one_of': [
+ 'Employee'
+ ]
+ }
+ ]
+ },
+ {
+ 'local': [
+ {
+ 'group': {
+ 'id': self.group_customers['id']
+ }
+ },
+ {
+ 'user': {
+ 'name': '{0}'
+ }
+ }
+ ],
+ 'remote': [
+ {
+ 'type': 'UserName'
+ },
+ {
+ 'type': 'orgPersonType',
+ 'any_one_of': [
+ 'Customer'
+ ]
+ }
+ ]
+ },
+ {
+ 'local': [
+ {
+ 'group': {
+ 'id': self.group_admins['id']
+ }
+ },
+ {
+ 'group': {
+ 'id': self.group_employees['id']
+ }
+ },
+ {
+ 'group': {
+ 'id': self.group_customers['id']
+ }
+ },
+
+ {
+ 'user': {
+ 'name': '{0}'
+ }
+ }
+ ],
+ 'remote': [
+ {
+ 'type': 'UserName'
+ },
+ {
+ 'type': 'orgPersonType',
+ 'any_one_of': [
+ 'Admin',
+ 'Chief'
+ ]
+ }
+ ]
+ },
+ {
+ 'local': [
+ {
+ 'group': {
+ 'id': 'bad_group_id'
+ }
+ },
+ {
+ 'user': {
+ 'name': '{0}'
+ }
+ }
+ ],
+ 'remote': [
+ {
+ 'type': 'UserName',
+ },
+ {
+ 'type': 'FirstName',
+ 'any_one_of': [
+ 'Jill'
+ ]
+ },
+ {
+ 'type': 'LastName',
+ 'any_one_of': [
+ 'Smith'
+ ]
+ }
+ ]
+ },
+
+ ]
+ }
+
+ # Add IDP
+ self.idp = self.idp_ref(id=self.IDP)
+ self.federation_api.create_idp(self.idp['id'],
+ self.idp)
+
+ # Add a mapping
+ self.mapping = self.mapping_ref()
+ self.federation_api.create_mapping(self.mapping['id'],
+ self.mapping)
+ # Add protocols
+ self.proto_saml = self.proto_ref(mapping_id=self.mapping['id'])
+ self.proto_saml['id'] = self.PROTOCOL
+ self.federation_api.create_protocol(self.idp['id'],
+ self.proto_saml['id'],
+ self.proto_saml)
+ # Generate fake tokens
+ context = {'environment': {}}
+
+ self.tokens = {}
+ VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION',
+ 'ADMIN_ASSERTION')
+ api = auth_controllers.Auth()
+ for variant in VARIANTS:
+ self._inject_assertion(context, variant)
+ r = api.authenticate_for_token(context, self.UNSCOPED_V3_SAML2_REQ)
+ self.tokens[variant] = r.headers.get('X-Subject-Token')
+
+ self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = self._scope_request(
+ uuid.uuid4().hex, 'project', self.proj_customers['id'])
+
+ self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = self._scope_request(
+ self.tokens['EMPLOYEE_ASSERTION'], 'project',
+ self.proj_employees['id'])
+
+ self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request(
+ self.tokens['ADMIN_ASSERTION'], 'project',
+ self.proj_employees['id'])
+
+ self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request(
+ self.tokens['ADMIN_ASSERTION'], 'project',
+ self.proj_customers['id'])
+
+ self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = self._scope_request(
+ self.tokens['CUSTOMER_ASSERTION'], 'project',
+ self.proj_employees['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request(
+ self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainA['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request(
+ self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainB['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request(
+ self.tokens['CUSTOMER_ASSERTION'], 'domain',
+ self.domainB['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request(
+ self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request(
+ self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id'])
+
+ self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request(
+ self.tokens['ADMIN_ASSERTION'], 'domain',
+ self.domainC['id'])
+
+ def _inject_assertion(self, context, variant):
+ assertion = getattr(mapping_fixtures, variant)
+ context['environment'].update(assertion)
+ context['query_string'] = []
diff --git a/keystone/token/providers/common.py b/keystone/token/providers/common.py
index 9551720cf..910f08d19 100644
--- a/keystone/token/providers/common.py
+++ b/keystone/token/providers/common.py
@@ -16,9 +16,11 @@ import json
import sys
import six
+from six.moves.urllib import parse
from keystone.common import dependency
from keystone import config
+from keystone.contrib import federation
from keystone import exception
from keystone import token
from keystone.token import provider
@@ -169,6 +171,34 @@ class V3TokenDataHelper(object):
user_id, project_id)
return [self.assignment_api.get_role(role_id) for role_id in roles]
+ def _populate_roles_for_groups(self, group_ids,
+ project_id=None, domain_id=None,
+ user_id=None):
+ def _check_roles(roles, user_id, project_id, domain_id):
+ # User was granted roles so simply exit this function.
+ if roles:
+ return
+ if project_id:
+ msg = _('User %(user_id)s has no access '
+ 'to project %(project_id)s') % {
+ 'user_id': user_id,
+ 'project_id': project_id}
+ elif domain_id:
+ msg = _('User %(user_id)s has no access '
+ 'to domain %(domain_id)s') % {
+ 'user_id': user_id,
+ 'domain_id': domain_id}
+ # Since no roles were found a user is not authorized to
+ # perform any operations. Raise an exception with
+ # appropriate error message.
+ raise exception.Unauthorized(msg)
+
+ roles = self.assignment_api.get_roles_for_groups(group_ids,
+ project_id,
+ domain_id)
+ _check_roles(roles, user_id, project_id, domain_id)
+ return roles
+
def _populate_user(self, token_data, user_id, trust):
if 'user' in token_data:
# no need to repopulate user if it already exists
@@ -391,6 +421,11 @@ class BaseProvider(provider.Provider):
'trust_id' in metadata_ref):
trust = self.trust_api.get_trust(metadata_ref['trust_id'])
+ token_ref = None
+ if 'saml2' in method_names:
+ token_ref = self._handle_saml2_tokens(auth_context, project_id,
+ domain_id)
+
access_token = None
if 'oauth1' in method_names:
if self.oauth_api:
@@ -408,6 +443,7 @@ class BaseProvider(provider.Provider):
expires=expires_at,
trust=trust,
bind=auth_context.get('bind') if auth_context else None,
+ token=token_ref,
include_catalog=include_catalog,
access_token=access_token)
@@ -451,6 +487,32 @@ class BaseProvider(provider.Provider):
return (token_id, token_data)
+ def _handle_saml2_tokens(self, auth_context, project_id, domain_id):
+ user_id = auth_context['user_id']
+ group_ids = auth_context['group_ids']
+ token_data = {
+ 'user': {
+ 'id': user_id,
+ 'name': parse.unquote(user_id)
+ }
+ }
+
+ if project_id or domain_id:
+ roles = self.v3_token_data_helper._populate_roles_for_groups(
+ group_ids, project_id, domain_id)
+ token_data.update({'roles': roles})
+ else:
+ idp = auth_context[federation.IDENTITY_PROVIDER]
+ protocol = auth_context[federation.PROTOCOL]
+ token_data['user'].update({
+ federation.FEDERATION: {
+ 'identity_provider': {'id': idp},
+ 'protocol': {'id': protocol}
+ },
+ federation.GROUPS: [{'id': x} for x in group_ids]
+ })
+ return token_data
+
def _verify_token(self, token_id):
"""Verify the given token and return the token_ref."""
token_ref = self.token_api.get_token(token_id)