Create authentication specific routes

These routes are purely based on your current authentication and bridge
the gap between what is available in the standard identity-api for
fetching scope targets based on user_id and what is required for the
federation APIs.

Implement /auth/projects /auth/domains and move /catalog to
/auth/catalog

Change-Id: I464c0ca5cc9f250d593340e9563de45b077dd4cd
Implements: blueprint auth-specific-data
This commit is contained in:
Jamie Lennox 2014-08-18 18:04:53 +10:00
parent a8b844251b
commit eb25fc6424
15 changed files with 302 additions and 100 deletions

View File

@ -25,8 +25,6 @@
"identity:update_endpoint": "rule:admin_required", "identity:update_endpoint": "rule:admin_required",
"identity:delete_endpoint": "rule:admin_required", "identity:delete_endpoint": "rule:admin_required",
"identity:get_catalog": "",
"identity:get_domain": "rule:admin_required", "identity:get_domain": "rule:admin_required",
"identity:list_domains": "rule:admin_required", "identity:list_domains": "rule:admin_required",
"identity:create_domain": "rule:admin_required", "identity:create_domain": "rule:admin_required",
@ -139,6 +137,10 @@
"identity:delete_mapping": "rule:admin_required", "identity:delete_mapping": "rule:admin_required",
"identity:update_mapping": "rule:admin_required", "identity:update_mapping": "rule:admin_required",
"identity:get_auth_catalog": "",
"identity:get_auth_projects": "",
"identity:get_auth_domains": "",
"identity:list_projects_for_groups": "", "identity:list_projects_for_groups": "",
"identity:list_domains_for_groups": "", "identity:list_domains_for_groups": "",

View File

@ -28,8 +28,6 @@
"identity:update_endpoint": "rule:cloud_admin", "identity:update_endpoint": "rule:cloud_admin",
"identity:delete_endpoint": "rule:cloud_admin", "identity:delete_endpoint": "rule:cloud_admin",
"identity:get_catalog": "",
"identity:get_domain": "rule:cloud_admin", "identity:get_domain": "rule:cloud_admin",
"identity:list_domains": "rule:cloud_admin", "identity:list_domains": "rule:cloud_admin",
"identity:create_domain": "rule:cloud_admin", "identity:create_domain": "rule:cloud_admin",
@ -152,6 +150,10 @@
"identity:delete_mapping": "rule:admin_required", "identity:delete_mapping": "rule:admin_required",
"identity:update_mapping": "rule:admin_required", "identity:update_mapping": "rule:admin_required",
"identity:get_auth_catalog": "",
"identity:get_auth_projects": "",
"identity:get_auth_domains": "",
"identity:list_projects_for_groups": "", "identity:list_projects_for_groups": "",
"identity:list_domains_for_groups": "", "identity:list_domains_for_groups": "",

View File

@ -205,6 +205,9 @@ class Assignment(kvs.Base, assignment.Driver):
return project_refs return project_refs
def list_domains_for_user(self, user_id, group_ids, hints):
raise exception.NotImplemented()
def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None): def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
raise exception.NotImplemented() raise exception.NotImplemented()

View File

@ -162,6 +162,9 @@ class Assignment(assignment.Driver):
def list_projects_for_groups(self, group_ids): def list_projects_for_groups(self, group_ids):
raise exception.NotImplemented() raise exception.NotImplemented()
def list_domains_for_user(self, user_id, group_ids, hints):
raise exception.NotImplemented()
def list_domains_for_groups(self, group_ids): def list_domains_for_groups(self, group_ids):
raise exception.NotImplemented() raise exception.NotImplemented()

View File

@ -289,6 +289,28 @@ class Assignment(keystone_assignment.Driver):
return _project_ids_to_dicts(session, project_ids) return _project_ids_to_dicts(session, project_ids)
def list_domains_for_user(self, user_id, group_ids, hints):
with sql.transaction() as session:
query = session.query(Domain)
query = query.join(RoleAssignment,
Domain.id == RoleAssignment.target_id)
filters = []
if user_id:
filters.append(sqlalchemy.and_(
RoleAssignment.actor_id == user_id,
RoleAssignment.type == AssignmentType.USER_DOMAIN))
if group_ids:
filters.append(sqlalchemy.and_(
RoleAssignment.actor_id.in_(group_ids),
RoleAssignment.type == AssignmentType.GROUP_DOMAIN))
if not filters:
return []
query = query.filter(sqlalchemy.or_(*filters))
return [ref.to_dict() for ref in query.all()]
def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None): def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None):
if project_id is not None: if project_id is not None:

View File

@ -70,6 +70,12 @@ class Manager(manager.Manager):
super(Manager, self).__init__(assignment_driver) super(Manager, self).__init__(assignment_driver)
def _get_group_ids_for_user_id(self, user_id):
# TODO(morganfainberg): Implement a way to get only group_ids
# instead of the more expensive to_dict() call for each record.
return [x['id'] for
x in self.identity_api.list_groups_for_user(user_id)]
@notifications.created(_PROJECT) @notifications.created(_PROJECT)
def create_project(self, tenant_id, tenant): def create_project(self, tenant_id, tenant):
tenant = tenant.copy() tenant = tenant.copy()
@ -151,10 +157,7 @@ class Manager(manager.Manager):
""" """
def _get_group_project_roles(user_id, project_ref): def _get_group_project_roles(user_id, project_ref):
# TODO(morganfainberg): Implement a way to get only group_ids group_ids = self._get_group_ids_for_user_id(user_id)
# instead of the more expensive to_dict() call for each record.
group_ids = [group['id'] for group in
self.identity_api.list_groups_for_user(user_id)]
return self.driver.get_group_project_roles( return self.driver.get_group_project_roles(
group_ids, group_ids,
project_ref['id'], project_ref['id'],
@ -199,10 +202,10 @@ class Manager(manager.Manager):
def _get_group_domain_roles(user_id, domain_id): def _get_group_domain_roles(user_id, domain_id):
role_list = [] role_list = []
group_refs = self.identity_api.list_groups_for_user(user_id) group_ids = self._get_group_ids_for_user_id(user_id)
for x in group_refs: for group_id in group_ids:
try: try:
metadata_ref = self._get_metadata(group_id=x['id'], metadata_ref = self._get_metadata(group_id=group_id,
domain_id=domain_id) domain_id=domain_id)
role_list += self._roles_from_role_dicts( role_list += self._roles_from_role_dicts(
metadata_ref.get('roles', {}), False) metadata_ref.get('roles', {}), False)
@ -289,9 +292,7 @@ class Manager(manager.Manager):
# list here and pass it in. The rest of the detailed logic of listing # list here and pass it in. The rest of the detailed logic of listing
# projects for a user is pushed down into the driver to enable # projects for a user is pushed down into the driver to enable
# optimization with the various backend technologies (SQL, LDAP etc.). # optimization with the various backend technologies (SQL, LDAP etc.).
group_ids = self._get_group_ids_for_user_id(user_id)
group_ids = [x['id'] for
x in self.identity_api.list_groups_for_user(user_id)]
return self.driver.list_projects_for_user( return self.driver.list_projects_for_user(
user_id, group_ids, hints or driver_hints.Hints()) user_id, group_ids, hints or driver_hints.Hints())
@ -319,6 +320,19 @@ class Manager(manager.Manager):
def list_domains(self, hints=None): def list_domains(self, hints=None):
return self.driver.list_domains(hints or driver_hints.Hints()) return self.driver.list_domains(hints or driver_hints.Hints())
# TODO(henry-nash): We might want to consider list limiting this at some
# point in the future.
def list_domains_for_user(self, user_id, hints=None):
# NOTE(henry-nash): In order to get a complete list of user domains,
# the driver will need to look at group assignments. To avoid cross
# calling between the assignment and identity driver we get the group
# list here and pass it in. The rest of the detailed logic of listing
# projects for a user is pushed down into the driver to enable
# optimization with the various backend technologies (SQL, LDAP etc.).
group_ids = self._get_group_ids_for_user_id(user_id)
return self.driver.list_domains_for_user(
user_id, group_ids, hints or driver_hints.Hints())
@notifications.disabled('domain', public=False) @notifications.disabled('domain', public=False)
def _disable_domain(self, domain_id): def _disable_domain(self, domain_id):
self.token_api.delete_tokens_for_domain(domain_id) self.token_api.delete_tokens_for_domain(domain_id)
@ -894,6 +908,22 @@ class Driver(object):
""" """
raise exception.NotImplemented() # pragma: no cover raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod
def list_domains_for_user(self, user_id, group_ids, hints):
"""List all domains associated with a given user.
:param user_id: the user in question
:param group_ids: the groups this user is a member of. This list is
built in the Manager, so that the driver itself
does not have to call across to identity.
:param hints: filter hints which the driver should
implement if at all possible.
:returns: a list of domain_refs or an empty list.
"""
raise exception.NotImplemented() # pragma: no cover
@abc.abstractmethod @abc.abstractmethod
def list_domains_for_groups(self, group_ids): def list_domains_for_groups(self, group_ids):
"""List domains accessible to specified groups. """List domains accessible to specified groups.

View File

@ -18,6 +18,8 @@ from keystoneclient.common import cms
from oslo.utils import timeutils from oslo.utils import timeutils
import six import six
from keystone.assignment import controllers as assignment_controllers
from keystone.common import authorization
from keystone.common import controller from keystone.common import controller
from keystone.common import dependency from keystone.common import dependency
from keystone.common import wsgi from keystone.common import wsgi
@ -338,8 +340,8 @@ class AuthInfo(object):
self._scope_data = (domain_id, project_id, trust) self._scope_data = (domain_id, project_id, trust)
@dependency.requires('assignment_api', 'identity_api', 'token_provider_api', @dependency.requires('assignment_api', 'catalog_api', 'identity_api',
'trust_api') 'token_provider_api', 'trust_api')
class Auth(controller.V3Controller): class Auth(controller.V3Controller):
# Note(atiwari): From V3 auth controller code we are # Note(atiwari): From V3 auth controller code we are
@ -537,6 +539,84 @@ class Auth(controller.V3Controller):
return {'signed': signed_text} return {'signed': signed_text}
def get_auth_context(self, context):
# TODO(dolphm): this method of accessing the auth context is terrible,
# but context needs to be refactored to always have reasonable values.
env_context = context.get('environment', {})
return env_context.get(authorization.AUTH_CONTEXT_ENV, {})
def _combine_lists_uniquely(self, a, b):
# it's most likely that only one of these will be filled so avoid
# the combination if possible.
if a and b:
return dict((x['id'], x) for x in a + b).values()
else:
return a or b
@controller.protected()
def get_auth_projects(self, context):
auth_context = self.get_auth_context(context)
user_id = auth_context.get('user_id')
user_refs = []
if user_id:
try:
user_refs = self.assignment_api.list_projects_for_user(user_id)
except exception.UserNotFound:
# federated users have an id but they don't link to anything
pass
group_ids = auth_context.get('group_ids')
grp_refs = []
if group_ids:
grp_refs = self.assignment_api.list_projects_for_groups(group_ids)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return assignment_controllers.ProjectV3.wrap_collection(context, refs)
@controller.protected()
def get_auth_domains(self, context):
auth_context = self.get_auth_context(context)
user_id = auth_context.get('user_id')
user_refs = []
if user_id:
try:
user_refs = self.assignment_api.list_domains_for_user(user_id)
except exception.UserNotFound:
# federated users have an id but they don't link to anything
pass
group_ids = auth_context.get('group_ids')
grp_refs = []
if group_ids:
grp_refs = self.assignment_api.list_domains_for_groups(group_ids)
refs = self._combine_lists_uniquely(user_refs, grp_refs)
return assignment_controllers.DomainV3.wrap_collection(context, refs)
@controller.protected()
def get_auth_catalog(self, context):
auth_context = self.get_auth_context(context)
user_id = auth_context.get('user_id')
project_id = auth_context.get('project_id')
if not project_id:
raise exception.Forbidden(
_('A project-scoped token is required to produce a service '
'catalog.'))
# The V3Controller base methods mostly assume that you're returning
# either a collection or a single element from a collection, neither of
# which apply to the catalog. Because this is a special case, this
# re-implements a tiny bit of work done by the base controller (such as
# self-referential link building) to avoid overriding or refactoring
# several private methods.
return {
'catalog': self.catalog_api.get_v3_catalog(user_id, project_id),
'links': {'self': self.base_url(context, path='auth/catalog')}
}
# FIXME(gyee): not sure if it belongs here or keystone.common. Park it here # FIXME(gyee): not sure if it belongs here or keystone.common. Park it here
# for now. # for now.

View File

@ -37,3 +37,18 @@ class Routers(wsgi.RoutersBase):
mapper, auth_controller, mapper, auth_controller,
path='/auth/tokens/OS-PKI/revoked', path='/auth/tokens/OS-PKI/revoked',
get_action='revocation_list') get_action='revocation_list')
self._add_resource(
mapper, auth_controller,
path='/auth/catalog',
get_action='get_auth_catalog')
self._add_resource(
mapper, auth_controller,
path='/auth/projects',
get_action='get_auth_projects')
self._add_resource(
mapper, auth_controller,
path='/auth/domains',
get_action='get_auth_domains')

View File

@ -17,7 +17,6 @@ import uuid
import six import six
from keystone.common import authorization
from keystone.common import controller from keystone.common import controller
from keystone.common import dependency from keystone.common import dependency
from keystone.common import wsgi from keystone.common import wsgi
@ -139,36 +138,6 @@ class Endpoint(controller.V2Controller):
raise exception.EndpointNotFound(endpoint_id=endpoint_id) raise exception.EndpointNotFound(endpoint_id=endpoint_id)
@dependency.requires('catalog_api')
class CatalogV3(controller.V3Controller):
collection_name = 'catalog'
@controller.protected()
def get_catalog(self, context):
# TODO(dolphm): this method of accessing the auth context is terrible,
# but context needs to be refactored to always have reasonable values.
env_context = context.get('environment', {})
auth_context = env_context.get(authorization.AUTH_CONTEXT_ENV, {})
user_id = auth_context.get('user_id')
project_id = auth_context.get('project_id')
if not user_id or not project_id:
raise exception.Forbidden(
_('A project-scoped token is required to produce a service '
'catalog.'))
# The V3Controller base methods mostly assume that you're returning
# either a collection or a single element from a collection, neither of
# which apply to the catalog. Because this is a special case, this
# re-implements a tiny bit of work done by the base controller (such as
# self-referential link building) to avoid overriding or refactoring
# several private methods.
return {
'catalog': self.catalog_api.get_v3_catalog(user_id, project_id),
'links': {
'self': CatalogV3.base_url(context)}}
@dependency.requires('catalog_api') @dependency.requires('catalog_api')
class RegionV3(controller.V3Controller): class RegionV3(controller.V3Controller):
collection_name = 'regions' collection_name = 'regions'

View File

@ -35,8 +35,3 @@ class Routers(wsgi.RoutersBase):
'services', 'service')) 'services', 'service'))
routers.append(router.Router(controllers.EndpointV3(), routers.append(router.Router(controllers.EndpointV3(),
'endpoints', 'endpoint')) 'endpoints', 'endpoint'))
self._add_resource(
mapper, controllers.CatalogV3(),
path='/catalog',
get_action='get_catalog')

View File

@ -311,6 +311,66 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests):
self.assertNotIn('default_project_id', user_ref) self.assertNotIn('default_project_id', user_ref)
session.close() session.close()
def test_list_domains_for_user(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain['id'], domain)
user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex,
'domain_id': domain['id'], 'enabled': True}
test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(test_domain1['id'], test_domain1)
test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(test_domain2['id'], test_domain2)
user = self.identity_api.create_user(user)
user_domains = self.assignment_api.list_domains_for_user(user['id'])
self.assertEqual(0, len(user_domains))
self.assignment_api.create_grant(user_id=user['id'],
domain_id=test_domain1['id'],
role_id=self.role_member['id'])
self.assignment_api.create_grant(user_id=user['id'],
domain_id=test_domain2['id'],
role_id=self.role_member['id'])
user_domains = self.assignment_api.list_domains_for_user(user['id'])
self.assertThat(user_domains, matchers.HasLength(2))
def test_list_domains_for_user_with_grants(self):
# Create two groups each with a role on a different domain, and
# make user1 a member of both groups. Both these new domains
# should now be included, along with any direct user grants.
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(domain['id'], domain)
user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex,
'domain_id': domain['id'], 'enabled': True}
user = self.identity_api.create_user(user)
group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']}
group1 = self.identity_api.create_group(group1)
group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']}
group2 = self.identity_api.create_group(group2)
test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(test_domain1['id'], test_domain1)
test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(test_domain2['id'], test_domain2)
test_domain3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex}
self.assignment_api.create_domain(test_domain3['id'], test_domain3)
self.identity_api.add_user_to_group(user['id'], group1['id'])
self.identity_api.add_user_to_group(user['id'], group2['id'])
# Create 3 grants, one user grant, the other two as group grants
self.assignment_api.create_grant(user_id=user['id'],
domain_id=test_domain1['id'],
role_id=self.role_member['id'])
self.assignment_api.create_grant(group_id=group1['id'],
domain_id=test_domain2['id'],
role_id=self.role_admin['id'])
self.assignment_api.create_grant(group_id=group2['id'],
domain_id=test_domain3['id'],
role_id=self.role_admin['id'])
user_domains = self.assignment_api.list_domains_for_user(user['id'])
self.assertThat(user_domains, matchers.HasLength(3))
class SqlTrust(SqlTests, test_backend.TrustTests): class SqlTrust(SqlTests, test_backend.TrustTests):
pass pass

View File

@ -742,7 +742,7 @@ class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase,
self.assertIsInstance(resp.json['links'], dict) self.assertIsInstance(resp.json['links'], dict)
self.assertEqual(['self'], resp.json['links'].keys()) self.assertEqual(['self'], resp.json['links'].keys())
self.assertEqual( self.assertEqual(
'http://localhost/v3/catalog', 'http://localhost/v3/auth/catalog',
resp.json['links']['self']) resp.json['links']['self'])
def assertValidCatalog(self, entity): def assertValidCatalog(self, entity):

View File

@ -19,6 +19,7 @@ import uuid
from keystoneclient.common import cms from keystoneclient.common import cms
from oslo.utils import timeutils from oslo.utils import timeutils
from testtools import matchers
from testtools import testcase from testtools import testcase
from keystone import auth from keystone import auth
@ -3412,3 +3413,56 @@ class TestAuthContext(tests.TestCase):
self.auth_context[attr_name] = attr_val_1 self.auth_context[attr_name] = attr_val_1
self.auth_context[attr_name] = attr_val_2 self.auth_context[attr_name] = attr_val_2
self.assertEqual(attr_val_2, self.auth_context[attr_name]) self.assertEqual(attr_val_2, self.auth_context[attr_name])
class TestAuthSpecificData(test_v3.RestfulTestCase):
def test_get_catalog_project_scoped_token(self):
"""Call ``GET /auth/catalog`` with a project-scoped token."""
r = self.get(
'/auth/catalog',
expected_status=200)
self.assertValidCatalogResponse(r)
def test_get_catalog_domain_scoped_token(self):
"""Call ``GET /auth/catalog`` with a domain-scoped token."""
# grant a domain role to a user
self.put(path='/domains/%s/users/%s/roles/%s' % (
self.domain['id'], self.user['id'], self.role['id']))
self.get(
'/auth/catalog',
auth=self.build_authentication_request(
user_id=self.user['id'],
password=self.user['password'],
domain_id=self.domain['id']),
expected_status=403)
def test_get_catalog_unscoped_token(self):
"""Call ``GET /auth/catalog`` with an unscoped token."""
self.get(
'/auth/catalog',
auth=self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password']),
expected_status=403)
def test_get_catalog_no_token(self):
"""Call ``GET /auth/catalog`` without a token."""
self.get(
'/auth/catalog',
noauth=True,
expected_status=401)
def test_get_projects_project_scoped_token(self):
r = self.get('/auth/projects', expected_status=200)
self.assertThat(r.json['projects'], matchers.HasLength(1))
self.assertValidProjectListResponse(r)
def test_get_domains_project_scoped_token(self):
self.put(path='/domains/%s/users/%s/roles/%s' % (
self.domain['id'], self.user['id'], self.role['id']))
r = self.get('/auth/domains', expected_status=200)
self.assertThat(r.json['domains'], matchers.HasLength(1))
self.assertValidDomainListResponse(r)

View File

@ -24,43 +24,6 @@ from keystone.tests import test_v3
class CatalogTestCase(test_v3.RestfulTestCase): class CatalogTestCase(test_v3.RestfulTestCase):
"""Test service & endpoint CRUD.""" """Test service & endpoint CRUD."""
def test_get_catalog_project_scoped_token(self):
"""Call ``GET /catalog`` with a project-scoped token."""
r = self.get(
'/catalog',
expected_status=200)
self.assertValidCatalogResponse(r)
def test_get_catalog_domain_scoped_token(self):
"""Call ``GET /catalog`` with a domain-scoped token."""
# grant a domain role to a user
self.put(path='/domains/%s/users/%s/roles/%s' % (
self.domain['id'], self.user['id'], self.role['id']))
self.get(
'/catalog',
auth=self.build_authentication_request(
user_id=self.user['id'],
password=self.user['password'],
domain_id=self.domain['id']),
expected_status=403)
def test_get_catalog_unscoped_token(self):
"""Call ``GET /catalog`` with an unscoped token."""
self.get(
'/catalog',
auth=self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password']),
expected_status=403)
def test_get_catalog_no_token(self):
"""Call ``GET /catalog`` without a token."""
self.get(
'/catalog',
noauth=True,
expected_status=401)
# region crud tests # region crud tests
def test_create_region_with_id(self): def test_create_region_with_id(self):

View File

@ -1022,7 +1022,7 @@ class FederatedTokenTests(FederationTests):
self._check_scoped_token_attributes(token_resp) self._check_scoped_token_attributes(token_resp)
def test_list_projects(self): def test_list_projects(self):
url = '/OS-FEDERATION/projects' urls = ('/OS-FEDERATION/projects', '/auth/projects')
token = (self.tokens['CUSTOMER_ASSERTION'], token = (self.tokens['CUSTOMER_ASSERTION'],
self.tokens['EMPLOYEE_ASSERTION'], self.tokens['EMPLOYEE_ASSERTION'],
@ -1036,13 +1036,15 @@ class FederatedTokenTests(FederationTests):
self.proj_customers['id']])) self.proj_customers['id']]))
for token, projects_ref in zip(token, projects_refs): for token, projects_ref in zip(token, projects_refs):
r = self.get(url, token=token) for url in urls:
projects_resp = r.result['projects'] r = self.get(url, token=token)
projects = set(p['id'] for p in projects_resp) projects_resp = r.result['projects']
self.assertEqual(projects, projects_ref) projects = set(p['id'] for p in projects_resp)
self.assertEqual(projects, projects_ref,
'match failed for url %s' % url)
def test_list_domains(self): def test_list_domains(self):
url = '/OS-FEDERATION/domains' urls = ('/OS-FEDERATION/domains', '/auth/domains')
tokens = (self.tokens['CUSTOMER_ASSERTION'], tokens = (self.tokens['CUSTOMER_ASSERTION'],
self.tokens['EMPLOYEE_ASSERTION'], self.tokens['EMPLOYEE_ASSERTION'],
@ -1056,10 +1058,12 @@ class FederatedTokenTests(FederationTests):
self.domainC['id']])) self.domainC['id']]))
for token, domains_ref in zip(tokens, domain_refs): for token, domains_ref in zip(tokens, domain_refs):
r = self.get(url, token=token) for url in urls:
domains_resp = r.result['domains'] r = self.get(url, token=token)
domains = set(p['id'] for p in domains_resp) domains_resp = r.result['domains']
self.assertEqual(domains, domains_ref) domains = set(p['id'] for p in domains_resp)
self.assertEqual(domains, domains_ref,
'match failed for url %s' % url)
def test_full_workflow(self): def test_full_workflow(self):
"""Test 'standard' workflow for granting access tokens. """Test 'standard' workflow for granting access tokens.