summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDolph Mathews <dolph.mathews@gmail.com>2015-07-31 20:31:54 +0000
committerDolph Mathews <dolph.mathews@gmail.com>2015-08-24 12:45:49 +0000
commit9dfad21201251364c6d205e8e79813bfe78e6107 (patch)
tree83d754a8898ca4a513baf5ff5665a685a536c444
parentaa3354e73bda53bbe8c354b644ed37e8b100a7d8 (diff)
downloadkeystone-9dfad21201251364c6d205e8e79813bfe78e6107.tar.gz
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. Conflicts: - keystone/tests/unit/test_v3_auth.py: A readability refactor has landed in master. Those changes have not been backported to stable/kilo. Change-Id: Ia75c260485b2cff3cd6cf5cf39c0ec715b99df10 Depends-On: Ia7ca08bca612b4555f6b4d9098cd7db6c540b1c4 Closes-Bug: 1475762 Closes-Bug: 1483382 (cherry picked from commit c4723550aa95be403ff591dd132c9024549eff10)
-rw-r--r--keystone/common/controller.py15
-rw-r--r--keystone/tests/unit/test_v3_assignment.py11
-rw-r--r--keystone/tests/unit/test_v3_auth.py71
-rw-r--r--keystone/tests/unit/test_v3_identity.py9
-rw-r--r--keystone/token/providers/common.py26
5 files changed, 98 insertions, 34 deletions
diff --git a/keystone/common/controller.py b/keystone/common/controller.py
index 5667eff56..50f3f330b 100644
--- a/keystone/common/controller.py
+++ b/keystone/common/controller.py
@@ -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).
diff --git a/keystone/tests/unit/test_v3_assignment.py b/keystone/tests/unit/test_v3_assignment.py
index df923cfb8..98142865b 100644
--- a/keystone/tests/unit/test_v3_assignment.py
+++ b/keystone/tests/unit/test_v3_assignment.py
@@ -205,8 +205,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': {
@@ -216,7 +216,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'],
@@ -3007,7 +3008,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,
@@ -3030,7 +3031,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}
diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py
index dd272f5f7..faa15b6ab 100644
--- a/keystone/tests/unit/test_v3_auth.py
+++ b/keystone/tests/unit/test_v3_auth.py
@@ -32,7 +32,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
@@ -216,12 +215,49 @@ class TokenAPITests(object):
project_id=self.project['id'])
token = self.get_requested_token(auth_data)
- # now validate the v3 token with v2 API
- path = '/v2.0/tokens/%s' % (token)
- self.admin_request(path=path,
- token='ADMIN',
- method='GET',
- expected_status=401)
+ # v2 cannot reference projects outside the default domain
+ self.admin_request(
+ method='GET',
+ path='/v2.0/tokens/%s' % token,
+ token='ADMIN',
+ 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,
+ token=CONF.admin_token,
+ method='GET',
+ expected_status=401)
def test_v3_v2_unscoped_token_intermix(self):
auth_data = self.build_authentication_request(
@@ -274,8 +310,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']
}
}}
resp = self.admin_request(path='/v2.0/tokens',
@@ -297,10 +333,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']
}}
resp = self.admin_request(path='/v2.0/tokens',
method='POST',
@@ -370,10 +406,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."""
diff --git a/keystone/tests/unit/test_v3_identity.py b/keystone/tests/unit/test_v3_identity.py
index dfb40e5fc..e00908290 100644
--- a/keystone/tests/unit/test_v3_identity.py
+++ b/keystone/tests/unit/test_v3_identity.py
@@ -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,
diff --git a/keystone/token/providers/common.py b/keystone/token/providers/common.py
index 717e1495c..1b3a17add 100644
--- a/keystone/token/providers/common.py
+++ b/keystone/token/providers/common.py
@@ -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