Validate domain ownership for v2 tokens
The v2 API is not domain aware, and so the default domain serves to provide an implicit domain scope for v2 API clients. If a v3 token with a user (or project scope) outside the default domain is validated by the v2 API, the user (or project) reference may result in a collision due to the namespacing provided by domains. This patch provides validation that the references being returned to the v2 API are in fact in the default domain, and thus cannot result in namespace collisions. Change-Id: Ia75c260485b2cff3cd6cf5cf39c0ec715b99df10 Closes-Bug: 1475762 Closes-Bug: 1483382
This commit is contained in:
parent
9391f9307e
commit
c4723550aa
@ -223,7 +223,11 @@ class V2Controller(wsgi.Application):
|
||||
@staticmethod
|
||||
def filter_domain_id(ref):
|
||||
"""Remove domain_id since v2 calls are not domain-aware."""
|
||||
ref.pop('domain_id', None)
|
||||
if 'domain_id' in ref:
|
||||
if ref['domain_id'] != CONF.identity.default_domain_id:
|
||||
raise exception.Unauthorized(
|
||||
_('Non-default domain is not supported'))
|
||||
del ref['domain_id']
|
||||
return ref
|
||||
|
||||
@staticmethod
|
||||
@ -276,9 +280,12 @@ class V2Controller(wsgi.Application):
|
||||
def v3_to_v2_user(ref):
|
||||
"""Convert a user_ref from v3 to v2 compatible.
|
||||
|
||||
* v2.0 users are not domain aware, and should have domain_id removed
|
||||
* v2.0 users expect the use of tenantId instead of default_project_id
|
||||
* v2.0 users have a username attribute
|
||||
- v2.0 users are not domain aware, and should have domain_id validated
|
||||
to be the default domain, and then removed.
|
||||
|
||||
- v2.0 users expect the use of tenantId instead of default_project_id.
|
||||
|
||||
- v2.0 users have a username attribute.
|
||||
|
||||
This method should only be applied to user_refs being returned from the
|
||||
v2.0 controller(s).
|
||||
|
@ -120,8 +120,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
|
||||
self.assignment_api.add_user_to_project(self.project2['id'],
|
||||
self.user2['id'])
|
||||
|
||||
# First check a user in that domain can authenticate, via
|
||||
# Both v2 and v3
|
||||
# First check a user in that domain can authenticate. The v2 user
|
||||
# cannot authenticate because they exist outside the default domain.
|
||||
body = {
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
@ -131,7 +131,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase,
|
||||
'tenantId': self.project2['id']
|
||||
}
|
||||
}
|
||||
self.admin_request(path='/v2.0/tokens', method='POST', body=body)
|
||||
self.admin_request(
|
||||
path='/v2.0/tokens', method='POST', body=body, expected_status=401)
|
||||
|
||||
auth_data = self.build_authentication_request(
|
||||
user_id=self.user2['id'],
|
||||
@ -3037,7 +3038,7 @@ class AssignmentV3toV2MethodsTestCase(tests.TestCase):
|
||||
"""Test domain V3 to V2 conversion methods."""
|
||||
def _setup_initial_projects(self):
|
||||
self.project_id = uuid.uuid4().hex
|
||||
self.domain_id = uuid.uuid4().hex
|
||||
self.domain_id = CONF.identity.default_domain_id
|
||||
self.parent_id = uuid.uuid4().hex
|
||||
# Project with only domain_id in ref
|
||||
self.project1 = {'id': self.project_id,
|
||||
@ -3060,7 +3061,7 @@ class AssignmentV3toV2MethodsTestCase(tests.TestCase):
|
||||
def test_v2controller_filter_domain_id(self):
|
||||
# V2.0 is not domain aware, ensure domain_id is popped off the ref.
|
||||
other_data = uuid.uuid4().hex
|
||||
domain_id = uuid.uuid4().hex
|
||||
domain_id = CONF.identity.default_domain_id
|
||||
ref = {'domain_id': domain_id,
|
||||
'other_data': other_data}
|
||||
|
||||
|
@ -33,7 +33,6 @@ from keystone.policy.backends import rules
|
||||
from keystone.tests import unit as tests
|
||||
from keystone.tests.unit import ksfixtures
|
||||
from keystone.tests.unit import test_v3
|
||||
from keystone.tests.unit import utils as test_utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -197,9 +196,9 @@ class TokenAPITests(object):
|
||||
|
||||
# domain-scoped tokens are not supported by v2
|
||||
self.admin_request(
|
||||
method='GET',
|
||||
path='/v2.0/tokens/%s' % v3_token,
|
||||
token=CONF.admin_token,
|
||||
method='GET',
|
||||
expected_status=401)
|
||||
|
||||
def test_v3_v2_intermix_non_default_project_failed(self):
|
||||
@ -209,6 +208,43 @@ class TokenAPITests(object):
|
||||
password=self.default_domain_user['password'],
|
||||
project_id=self.project['id']))
|
||||
|
||||
# v2 cannot reference projects outside the default domain
|
||||
self.admin_request(
|
||||
method='GET',
|
||||
path='/v2.0/tokens/%s' % v3_token,
|
||||
token=CONF.admin_token,
|
||||
expected_status=401)
|
||||
|
||||
def test_v3_v2_intermix_non_default_user_failed(self):
|
||||
self.assignment_api.create_grant(
|
||||
self.role['id'],
|
||||
user_id=self.user['id'],
|
||||
project_id=self.default_domain_project['id'])
|
||||
|
||||
# self.user is in a non-default domain
|
||||
v3_token = self.get_requested_token(self.build_authentication_request(
|
||||
user_id=self.user['id'],
|
||||
password=self.user['password'],
|
||||
project_id=self.default_domain_project['id']))
|
||||
|
||||
# v2 cannot reference projects outside the default domain
|
||||
self.admin_request(
|
||||
method='GET',
|
||||
path='/v2.0/tokens/%s' % v3_token,
|
||||
token=CONF.admin_token,
|
||||
expected_status=401)
|
||||
|
||||
def test_v3_v2_intermix_domain_scope_failed(self):
|
||||
self.assignment_api.create_grant(
|
||||
self.role['id'],
|
||||
user_id=self.default_domain_user['id'],
|
||||
domain_id=self.domain['id'])
|
||||
|
||||
v3_token = self.get_requested_token(self.build_authentication_request(
|
||||
user_id=self.default_domain_user['id'],
|
||||
password=self.default_domain_user['password'],
|
||||
domain_id=self.domain['id']))
|
||||
|
||||
# v2 cannot reference projects outside the default domain
|
||||
self.admin_request(
|
||||
path='/v2.0/tokens/%s' % v3_token,
|
||||
@ -272,8 +308,8 @@ class TokenAPITests(object):
|
||||
body={
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'userId': self.user['id'],
|
||||
'password': self.user['password']
|
||||
'userId': self.default_domain_user['id'],
|
||||
'password': self.default_domain_user['password']
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -300,10 +336,10 @@ class TokenAPITests(object):
|
||||
body={
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'userId': self.user['id'],
|
||||
'password': self.user['password']
|
||||
'userId': self.default_domain_user['id'],
|
||||
'password': self.default_domain_user['password']
|
||||
},
|
||||
'tenantId': self.project['id']
|
||||
'tenantId': self.default_domain_project['id']
|
||||
}
|
||||
})
|
||||
v2_token_data = r.result
|
||||
@ -378,10 +414,10 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase):
|
||||
def _v2_token(self):
|
||||
body = {
|
||||
'auth': {
|
||||
"tenantId": self.project['id'],
|
||||
"tenantId": self.default_domain_project['id'],
|
||||
'passwordCredentials': {
|
||||
'userId': self.user['id'],
|
||||
'password': self.user['password']
|
||||
'userId': self.default_domain_user['id'],
|
||||
'password': self.default_domain_user['password']
|
||||
}
|
||||
}}
|
||||
resp = self.admin_request(path='/v2.0/tokens',
|
||||
@ -540,11 +576,6 @@ class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests):
|
||||
super(TestFernetTokenAPIs, self).setUp()
|
||||
self.doSetUp()
|
||||
|
||||
@test_utils.wip('Failing due to bug 1475762.')
|
||||
def test_v3_v2_intermix_non_default_project_failed(self):
|
||||
super(TestFernetTokenAPIs,
|
||||
self).test_v3_v2_intermix_non_default_project_failed()
|
||||
|
||||
|
||||
class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase):
|
||||
"""Test token revoke using v3 Identity API by token owner and admin."""
|
||||
|
@ -478,27 +478,26 @@ class IdentityV3toV2MethodsTestCase(tests.TestCase):
|
||||
self.user_id = uuid.uuid4().hex
|
||||
self.default_project_id = uuid.uuid4().hex
|
||||
self.tenant_id = uuid.uuid4().hex
|
||||
self.domain_id = uuid.uuid4().hex
|
||||
# User with only default_project_id in ref
|
||||
self.user1 = {'id': self.user_id,
|
||||
'name': self.user_id,
|
||||
'default_project_id': self.default_project_id,
|
||||
'domain_id': self.domain_id}
|
||||
'domain_id': CONF.identity.default_domain_id}
|
||||
# User without default_project_id or tenantId in ref
|
||||
self.user2 = {'id': self.user_id,
|
||||
'name': self.user_id,
|
||||
'domain_id': self.domain_id}
|
||||
'domain_id': CONF.identity.default_domain_id}
|
||||
# User with both tenantId and default_project_id in ref
|
||||
self.user3 = {'id': self.user_id,
|
||||
'name': self.user_id,
|
||||
'default_project_id': self.default_project_id,
|
||||
'tenantId': self.tenant_id,
|
||||
'domain_id': self.domain_id}
|
||||
'domain_id': CONF.identity.default_domain_id}
|
||||
# User with only tenantId in ref
|
||||
self.user4 = {'id': self.user_id,
|
||||
'name': self.user_id,
|
||||
'tenantId': self.tenant_id,
|
||||
'domain_id': self.domain_id}
|
||||
'domain_id': CONF.identity.default_domain_id}
|
||||
|
||||
# Expected result if the user is meant to have a tenantId element
|
||||
self.expected_user = {'id': self.user_id,
|
||||
|
@ -48,7 +48,23 @@ class V2TokenDataHelper(object):
|
||||
token['issued_at'] = v3_token.get('issued_at')
|
||||
token['audit_ids'] = v3_token.get('audit_ids')
|
||||
|
||||
# Bail immediately if this is a domain-scoped token, which is not
|
||||
# supported by the v2 API at all.
|
||||
if 'domain' in v3_token:
|
||||
raise exception.Unauthorized(_(
|
||||
'Domains are not supported by the v2 API. Please use the v3 '
|
||||
'API instead.'))
|
||||
|
||||
# Bail if this is a project-scoped token outside the default domain,
|
||||
# which may result in a namespace collision with a project inside the
|
||||
# default domain.
|
||||
if 'project' in v3_token:
|
||||
if (v3_token['project']['domain']['id'] !=
|
||||
CONF.identity.default_domain_id):
|
||||
raise exception.Unauthorized(_(
|
||||
'Project not found in the default domain (please use the '
|
||||
'v3 API instead): %s') % v3_token['project']['id'])
|
||||
|
||||
# v3 token_data does not contain all tenant attributes
|
||||
tenant = self.resource_api.get_project(
|
||||
v3_token['project']['id'])
|
||||
@ -58,6 +74,16 @@ class V2TokenDataHelper(object):
|
||||
|
||||
# Build v2 user
|
||||
v3_user = v3_token['user']
|
||||
|
||||
# Bail if this is a token outside the default domain,
|
||||
# which may result in a namespace collision with a project inside the
|
||||
# default domain.
|
||||
if ('domain' in v3_user and v3_user['domain']['id'] !=
|
||||
CONF.identity.default_domain_id):
|
||||
raise exception.Unauthorized(_(
|
||||
'User not found in the default domain (please use the v3 API '
|
||||
'instead): %s') % v3_user['id'])
|
||||
|
||||
user = common_controller.V2Controller.v3_to_v2_user(v3_user)
|
||||
|
||||
# Set user roles
|
||||
|
Loading…
x
Reference in New Issue
Block a user