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:
Dolph Mathews 2015-07-31 20:31:54 +00:00 committed by yeeg
parent 9391f9307e
commit c4723550aa
5 changed files with 93 additions and 29 deletions

View File

@ -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).

View File

@ -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}

View File

@ -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."""

View File

@ -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,

View File

@ -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