Explicit Unscoped

Allow a user to explicitly request an unscoped token

Adjoining unit tests show that the same request without the
unscoped keyword fetch a project scoped token.

bp: explicit-unscoped

Change-Id: Ife13b4851e23088803c51d88209b371329814066
This commit is contained in:
Adam Young 2014-12-17 12:40:54 -05:00 committed by ayoung
parent c21244f6f8
commit e8ac71f036
3 changed files with 55 additions and 42 deletions

View File

@ -137,12 +137,12 @@ class AuthInfo(object):
def __init__(self, context, auth=None):
self.context = context
self.auth = auth
self._scope_data = (None, None, None)
# self._scope_data is (domain_id, project_id, trust_ref)
# project scope: (None, project_id, None)
# domain scope: (domain_id, None, None)
# trust scope: (None, None, trust_ref)
# unscoped: (None, None, None)
self._scope_data = (None, None, None, None)
# self._scope_data is (domain_id, project_id, trust_ref, unscoped)
# project scope: (None, project_id, None, None)
# domain scope: (domain_id, None, None, None)
# trust scope: (None, None, trust_ref, None)
# unscoped: (None, None, None, 'unscoped')
def _assert_project_is_enabled(self, project_ref):
# ensure the project is enabled
@ -227,17 +227,20 @@ class AuthInfo(object):
return
if sum(['project' in self.auth['scope'],
'domain' in self.auth['scope'],
'unscoped' in self.auth['scope'],
'OS-TRUST:trust' in self.auth['scope']]) != 1:
raise exception.ValidationError(
attribute='project, domain, or OS-TRUST:trust',
attribute='project, domain, OS-TRUST:trust or unscoped',
target='scope')
if 'unscoped' in self.auth['scope']:
self._scope_data = (None, None, None, 'unscoped')
return
if 'project' in self.auth['scope']:
project_ref = self._lookup_project(self.auth['scope']['project'])
self._scope_data = (None, project_ref['id'], None)
self._scope_data = (None, project_ref['id'], None, None)
elif 'domain' in self.auth['scope']:
domain_ref = self._lookup_domain(self.auth['scope']['domain'])
self._scope_data = (domain_ref['id'], None, None)
self._scope_data = (domain_ref['id'], None, None, None)
elif 'OS-TRUST:trust' in self.auth['scope']:
if not CONF.trust.enabled:
raise exception.Forbidden('Trusts are disabled.')
@ -247,9 +250,9 @@ class AuthInfo(object):
if trust_ref.get('project_id') is not None:
project_ref = self._lookup_project(
{'id': trust_ref['project_id']})
self._scope_data = (None, project_ref['id'], trust_ref)
self._scope_data = (None, project_ref['id'], trust_ref, None)
else:
self._scope_data = (None, None, trust_ref)
self._scope_data = (None, None, trust_ref, None)
def _validate_auth_methods(self):
if 'identity' not in self.auth:
@ -312,20 +315,22 @@ class AuthInfo(object):
Verify and return the scoping information.
:returns: (domain_id, project_id, trust_ref).
If scope to a project, (None, project_id, None)
:returns: (domain_id, project_id, trust_ref, unscoped).
If scope to a project, (None, project_id, None, None)
will be returned.
If scoped to a domain, (domain_id, None, None)
If scoped to a domain, (domain_id, None, None, None)
will be returned.
If scoped to a trust, (None, project_id, trust_ref),
If scoped to a trust, (None, project_id, trust_ref, None),
Will be returned, where the project_id comes from the
trust definition.
If unscoped, (None, None, None) will be returned.
If unscoped, (None, None, None, 'unscoped') will be
returned.
"""
return self._scope_data
def set_scope(self, domain_id=None, project_id=None, trust=None):
def set_scope(self, domain_id=None, project_id=None, trust=None,
unscoped=None):
"""Set scope information."""
if domain_id and project_id:
msg = _('Scoping to both domain and project is not allowed')
@ -336,7 +341,7 @@ class AuthInfo(object):
if project_id and trust:
msg = _('Scoping to both project and trust is not allowed')
raise ValueError(msg)
self._scope_data = (domain_id, project_id, trust)
self._scope_data = (domain_id, project_id, trust, unscoped)
@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
@ -373,7 +378,7 @@ class Auth(controller.V3Controller):
if auth_context.get('access_token_id'):
auth_info.set_scope(None, auth_context['project_id'], None)
self._check_and_set_default_scoping(auth_info, auth_context)
(domain_id, project_id, trust) = auth_info.get_scope()
(domain_id, project_id, trust, unscoped) = auth_info.get_scope()
method_names = auth_info.get_method_names()
method_names += auth_context.get('method_names', [])
@ -402,7 +407,7 @@ class Auth(controller.V3Controller):
raise exception.Unauthorized(e)
def _check_and_set_default_scoping(self, auth_info, auth_context):
(domain_id, project_id, trust) = auth_info.get_scope()
(domain_id, project_id, trust, unscoped) = auth_info.get_scope()
if trust:
project_id = trust['project_id']
if domain_id or project_id or trust:
@ -413,6 +418,10 @@ class Auth(controller.V3Controller):
if federation.IDENTITY_PROVIDER in auth_context:
return
# Do not scope if request is for explicitly unscoped token
if unscoped is not None:
return
# fill in default_project_id if it is available
try:
user_ref = self.identity_api.get_user(auth_context['user_id'])

View File

@ -41,8 +41,11 @@ class AuthTestMixin(object):
"""To hold auth building helper functions."""
def build_auth_scope(self, project_id=None, project_name=None,
project_domain_id=None, project_domain_name=None,
domain_id=None, domain_name=None, trust_id=None):
domain_id=None, domain_name=None, trust_id=None,
unscoped=None):
scope_data = {}
if unscoped:
scope_data['unscoped'] = {}
if project_id or project_name:
scope_data['project'] = {}
if project_id:

View File

@ -1711,8 +1711,7 @@ class TestAuth(test_v3.RestfulTestCase):
r = self.v3_authenticate_token(auth_data)
self.assertValidProjectScopedTokenResponse(r)
def test_default_project_id_scoped_token_with_user_id(self):
# create a second project to work with
def _second_project_as_default(self):
ref = self.new_project_ref(domain_id=self.domain_id)
r = self.post('/projects', body={'project': ref})
project = self.assertValidProjectResponse(r, ref)
@ -1731,6 +1730,11 @@ class TestAuth(test_v3.RestfulTestCase):
body=body)
self.assertValidUserResponse(r)
return project
def test_default_project_id_scoped_token_with_user_id(self):
project = self._second_project_as_default()
# attempt to authenticate without requesting a project
auth_data = self.build_authentication_request(
user_id=self.user['id'],
@ -1740,24 +1744,7 @@ class TestAuth(test_v3.RestfulTestCase):
self.assertEqual(r.result['token']['project']['id'], project['id'])
def test_default_project_id_scoped_token_with_user_id_no_catalog(self):
# create a second project to work with
ref = self.new_project_ref(domain_id=self.domain_id)
r = self.post('/projects', body={'project': ref})
project = self.assertValidProjectResponse(r, ref)
# grant the user a role on the project
self.put(
'/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % {
'user_id': self.user['id'],
'project_id': project['id'],
'role_id': self.role['id']})
# set the user's preferred project
body = {'user': {'default_project_id': project['id']}}
r = self.patch('/users/%(user_id)s' % {
'user_id': self.user['id']},
body=body)
self.assertValidUserResponse(r)
project = self._second_project_as_default()
# attempt to authenticate without requesting a project
auth_data = self.build_authentication_request(
@ -1767,6 +1754,20 @@ class TestAuth(test_v3.RestfulTestCase):
self.assertValidProjectScopedTokenResponse(r, require_catalog=False)
self.assertEqual(r.result['token']['project']['id'], project['id'])
def test_explicit_unscoped_token(self):
self._second_project_as_default()
# attempt to authenticate without requesting a project
auth_data = self.build_authentication_request(
user_id=self.user['id'],
password=self.user['password'],
unscoped="unscoped")
r = self.post('/auth/tokens', body=auth_data, noauth=True)
self.assertIsNone(r.result['token'].get('project'))
self.assertIsNone(r.result['token'].get('domain'))
self.assertIsNone(r.result['token'].get('scope'))
def test_implicit_project_id_scoped_token_with_user_id_no_catalog(self):
# attempt to authenticate without requesting a project
auth_data = self.build_authentication_request(