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
This commit is contained in:
parent
b5a26b35ac
commit
986c3eb08a
|
@ -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": ""
|
||||
|
||||
}
|
||||
|
|
|
@ -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": ""
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -828,6 +828,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.
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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'))
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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']))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -28,9 +28,17 @@ MAPPING_SMALL = {
|
|||
"group": {
|
||||
"id": EMPLOYEE_GROUP_ID
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"not_any_of": [
|
||||
|
@ -52,9 +60,17 @@ MAPPING_SMALL = {
|
|||
"group": {
|
||||
"id": CONTRACTOR_GROUP_ID
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"any_one_of": [
|
||||
|
@ -148,9 +164,17 @@ MAPPING_LARGE = {
|
|||
"group": {
|
||||
"id": DEVELOPER_GROUP_ID
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"any_one_of": [
|
||||
|
@ -278,9 +302,17 @@ MAPPING_EXTRA_REMOTE_PROPS_NOT_ANY_OF = {
|
|||
"group": {
|
||||
"id": "0cd5e9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"not_any_of": [
|
||||
|
@ -301,9 +333,17 @@ MAPPING_EXTRA_REMOTE_PROPS_ANY_ONE_OF = {
|
|||
"group": {
|
||||
"id": "0cd5e9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"any_one_of": [
|
||||
|
@ -324,9 +364,17 @@ MAPPING_EXTRA_REMOTE_PROPS_JUST_TYPE = {
|
|||
"group": {
|
||||
"id": "0cd5e9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"invalid_type": "xyz"
|
||||
|
@ -344,12 +392,20 @@ MAPPING_EXTRA_RULES_PROPS = {
|
|||
"group": {
|
||||
"id": "0cd5e9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user": {
|
||||
"name": "{0}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"invalid_type": {
|
||||
"id": "xyz",
|
||||
},
|
||||
"remote": [
|
||||
{
|
||||
"type": "UserName"
|
||||
},
|
||||
{
|
||||
"type": "orgPersonType",
|
||||
"not_any_of": [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'] = []
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue