diff --git a/etc/policy.v3cloudsample.json b/etc/policy.v3cloudsample.json index 25a2aabcc..6285d2009 100644 --- a/etc/policy.v3cloudsample.json +++ b/etc/policy.v3cloudsample.json @@ -3,7 +3,7 @@ "cloud_admin": [["rule:admin_required", "domain_id:admin_domain_id"]], "service_role": [["role:service"]], "service_or_admin": [["rule:admin_required"], ["rule:service_role"]], - "owner" : [["user_id:%(user_id)s"]], + "owner" : [["user_id:%(user_id)s"], ["user_id:%(target.entity.user_id)s"]], "admin_or_owner": [["rule:admin_required"], ["rule:owner"]], "admin_or_cloud_admin": [["rule:admin_required"], ["rule:cloud_admin"]], @@ -85,7 +85,7 @@ "identity:update_policy": [["rule:cloud_admin"]], "identity:delete_policy": [["rule:cloud_admin"]], - "identity:check_token": [["rule:admin_required"]], + "identity:check_token": [["rule:admin_or_owner"]], "identity:validate_token": [["rule:service_or_admin"]], "identity:validate_token_head": [["rule:service_or_admin"]], "identity:revocation_list": [["rule:service_or_admin"]], diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 735055cf7..faadc09d5 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -128,18 +128,27 @@ def protected(callback=None): action = 'identity:%s' % f.__name__ creds = _build_policy_check_credentials(self, action, context, kwargs) + + policy_dict = {} + # Check to see if we need to include the target entity in our # policy checks. We deduce this by seeing if the class has # specified a get_member() method and that kwargs contains the # appropriate entity id. - policy_dict = {} if (hasattr(self, 'get_member_from_driver') and self.get_member_from_driver is not None): - key = '%s_id' % self.member_name - if key in kwargs: - ref = self.get_member_from_driver(kwargs[key]) - policy_dict = {'target': - {self.member_name: ref}} + key = '%s_id' % self.member_name + if key in kwargs: + ref = self.get_member_from_driver(kwargs[key]) + policy_dict['target'] = {self.member_name: ref} + + if context.get('subject_token_id') is not None: + token_ref = self.token_api.get_token( + context['subject_token_id']) + policy_dict.setdefault('target', {}) + policy_dict['target'].setdefault(self.member_name, {}) + policy_dict['target'][self.member_name]['user_id'] = ( + token_ref['user_id']) # Add in the kwargs, which means that any entity provided as a # parameter for calls like create and update will be included. diff --git a/keystone/tests/test_auth.py b/keystone/tests/test_auth.py index e0bfecea3..7a69eda84 100644 --- a/keystone/tests/test_auth.py +++ b/keystone/tests/test_auth.py @@ -432,7 +432,7 @@ class AuthWithToken(AuthTest): # Check the token is now invalid self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.controller.validate_token, dict(is_admin=True, query_string={}), token_id=token_id) diff --git a/keystone/tests/test_backend.py b/keystone/tests/test_backend.py index f3571e66a..b944d2028 100644 --- a/keystone/tests/test_backend.py +++ b/keystone/tests/test_backend.py @@ -2834,30 +2834,30 @@ class TokenCacheInvalidation(object): def _check_unscoped_tokens_are_invalid(self): self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_token, self.unscoped_token_id) self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_v2_token, self.unscoped_token_id) def _check_scoped_tokens_are_invalid(self): self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_token, self.scoped_token_id) self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_token, self.scoped_token_id, self.tenant['id']) self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_v2_token, self.scoped_token_id) self.assertRaises( - exception.Unauthorized, + exception.TokenNotFound, self.token_provider_api.validate_v2_token, self.scoped_token_id, self.tenant['id']) diff --git a/keystone/tests/test_content_types.py b/keystone/tests/test_content_types.py index 86952d052..201643831 100644 --- a/keystone/tests/test_content_types.py +++ b/keystone/tests/test_content_types.py @@ -402,6 +402,15 @@ class CoreApiTests(object): token=token) self.assertValidAuthenticationResponse(r) + def test_invalid_token_404(self): + token = self.get_scoped_token() + self.admin_request( + path='/v2.0/tokens/%(token_id)s' % { + 'token_id': 'invalid', + }, + token=token, + expected_status=404) + def test_validate_token_service_role(self): self.metadata_foobar = self.identity_api.add_role_to_user_and_project( self.user_foo['id'], diff --git a/keystone/tests/test_token_provider.py b/keystone/tests/test_token_provider.py index 437672aa4..c93bd731c 100644 --- a/keystone/tests/test_token_provider.py +++ b/keystone/tests/test_token_provider.py @@ -801,13 +801,13 @@ class TestTokenProvider(tests.TestCase): 'my.package.MyProvider') def test_provider_token_expiration_validation(self): - self.assertRaises(exception.Unauthorized, + self.assertRaises(exception.TokenNotFound, self.token_provider_api._is_valid_token, SAMPLE_V2_TOKEN_EXPIRED) - self.assertRaises(exception.Unauthorized, + self.assertRaises(exception.TokenNotFound, self.token_provider_api._is_valid_token, SAMPLE_V3_TOKEN_EXPIRED) - self.assertRaises(exception.Unauthorized, + self.assertRaises(exception.TokenNotFound, self.token_provider_api._is_valid_token, SAMPLE_MALFORMED_TOKEN) self.assertEqual( diff --git a/keystone/tests/test_v3_auth.py b/keystone/tests/test_v3_auth.py index 55a25f177..57384796b 100644 --- a/keystone/tests/test_v3_auth.py +++ b/keystone/tests/test_v3_auth.py @@ -323,8 +323,7 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase): def test_revoke_token(self): headers = {'X-Subject-Token': self.get_scoped_token()} self.delete('/auth/tokens', headers=headers, expected_status=204) - self.head('/auth/tokens', headers=headers, expected_status=401) - + self.head('/auth/tokens', headers=headers, expected_status=404) # make sure we have a CRL r = self.get('/auth/tokens/OS-PKI/revoked') self.assertIn('signed', r.result) @@ -352,6 +351,118 @@ class TestUUIDTokenAPIs(TestPKITokenAPIs): pass +class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): + """Test token revoke using v3 Identity API by token owner and admin.""" + def setUp(self): + """Setup for Test Cases. + One domain A + Two users userNormalA and userAdminA + + """ + super(TestTokenRevokeSelfAndAdmin, self).setUp() + + self.domainA = self.new_domain_ref() + self.identity_api.create_domain(self.domainA['id'], self.domainA) + + self.userAdminA = self.new_user_ref(domain_id=self.domainA['id']) + self.userAdminA['password'] = uuid.uuid4().hex + self.identity_api.create_user(self.userAdminA['id'], self.userAdminA) + + self.userNormalA = self.new_user_ref( + domain_id=self.domainA['id']) + self.userNormalA['password'] = uuid.uuid4().hex + self.identity_api.create_user(self.userNormalA['id'], self.userNormalA) + + self.role1 = self.new_role_ref() + self.role1['name'] = 'admin' + self.identity_api.create_role(self.role1['id'], self.role1) + + self.identity_api.create_grant(self.role1['id'], + user_id=self.userAdminA['id'], + domain_id=self.domainA['id']) + + # Finally, switch to the v3 sample policy file + self.orig_policy_file = CONF.policy_file + from keystone.policy.backends import rules + rules.reset() + self.opt(policy_file=tests.etcdir('policy.v3cloudsample.json')) + + def test_user_revokes_own_token(self): + r = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.userNormalA['id'], + password=self.userNormalA['password'], + user_domain_id=self.domainA['id'])) + + user_token = r.headers.get('X-Subject-Token') + self.assertNotEmpty(user_token) + headers = {'X-Subject-Token': user_token} + + r = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.userAdminA['id'], + password=self.userAdminA['password'], + domain_name=self.domainA['name'])) + adminA_token = r.headers.get('X-Subject-Token') + + self.head('/auth/tokens', headers=headers, expected_status=204, + token=adminA_token) + self.head('/auth/tokens', headers=headers, expected_status=204, + token=user_token) + self.delete('/auth/tokens', headers=headers, expected_status=204, + token=user_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.head('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.delete('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.delete('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.head('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + + def test_admin_revokes_user_token(self): + r = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.userNormalA['id'], + password=self.userNormalA['password'], + user_domain_id=self.domainA['id'])) + + user_token = r.headers.get('X-Subject-Token') + self.assertNotEmpty(user_token) + headers = {'X-Subject-Token': user_token} + + r = self.post( + '/auth/tokens', + body=self.build_authentication_request( + user_id=self.userAdminA['id'], + password=self.userAdminA['password'], + domain_name=self.domainA['name'])) + adminA_token = r.headers.get('X-Subject-Token') + + self.head('/auth/tokens', headers=headers, expected_status=204, + token=adminA_token) + self.head('/auth/tokens', headers=headers, expected_status=204, + token=user_token) + self.delete('/auth/tokens', headers=headers, expected_status=204, + token=adminA_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.head('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.delete('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.head('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + + class TestTokenRevoking(test_v3.RestfulTestCase): """Test token revocation on the v3 Identity API.""" @@ -517,7 +628,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): self.delete(grant_url) self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=401) + expected_status=404) def test_deleting_role_revokes_token(self): """Test deleting a role revokes token. @@ -632,16 +743,16 @@ class TestTokenRevoking(test_v3.RestfulTestCase): # Check the tokens that used role1 is invalid self.head('/auth/tokens', headers={'X-Subject-Token': tokenA}, - expected_status=401) + expected_status=404) self.head('/auth/tokens', headers={'X-Subject-Token': tokenB}, - expected_status=401) + expected_status=404) self.head('/auth/tokens', headers={'X-Subject-Token': tokenD}, - expected_status=401) + expected_status=404) self.head('/auth/tokens', headers={'X-Subject-Token': tokenE}, - expected_status=401) + expected_status=404) # ...but the one using role2 is still valid self.head('/auth/tokens', @@ -701,7 +812,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): # user should no longer have access to the project self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=401) + expected_status=404) resp = self.post( '/auth/tokens', body=self.build_authentication_request( @@ -731,7 +842,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): # user should no longer have access to the project self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=401) + expected_status=404) resp = self.post( '/auth/tokens', body=self.build_authentication_request( @@ -792,10 +903,10 @@ class TestTokenRevoking(test_v3.RestfulTestCase): self.delete(grant_url) self.head('/auth/tokens', headers={'X-Subject-Token': token1}, - expected_status=401) + expected_status=404) self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=401) + expected_status=404) # But user3's token should still be valid self.head('/auth/tokens', headers={'X-Subject-Token': token3}, @@ -872,7 +983,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): 'user_id': self.user1['id']}) self.head('/auth/tokens', headers={'X-Subject-Token': token1}, - expected_status=401) + expected_status=404) # But user2's token should still be valid self.head('/auth/tokens', headers={'X-Subject-Token': token2}, @@ -883,7 +994,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): 'user_id': self.user2['id']}) self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=401) + expected_status=404) def test_removing_role_assignment_does_not_affect_other_users(self): """Revoking a role from one user should not affect other users.""" @@ -918,7 +1029,7 @@ class TestTokenRevoking(test_v3.RestfulTestCase): # authorization for the first user should now fail self.head('/auth/tokens', headers={'X-Subject-Token': user1_token}, - expected_status=401) + expected_status=404) self.post( '/auth/tokens', body=self.build_authentication_request( diff --git a/keystone/tests/test_v3_oauth1.py b/keystone/tests/test_v3_oauth1.py index 915761b1f..73a34d722 100644 --- a/keystone/tests/test_v3_oauth1.py +++ b/keystone/tests/test_v3_oauth1.py @@ -365,7 +365,7 @@ class AuthTokenTests(OAuthFlowTests): headers = {'X-Subject-Token': self.keystone_token_id, 'X-Auth-Token': self.keystone_token_id} self.get('/auth/tokens', headers=headers, - expected_status=401) + expected_status=404) def test_deleting_consumer_also_deletes_tokens(self): self.test_oauth_flow() @@ -386,7 +386,7 @@ class AuthTokenTests(OAuthFlowTests): headers = {'X-Subject-Token': self.keystone_token_id, 'X-Auth-Token': self.keystone_token_id} self.head('/auth/tokens', headers=headers, - expected_status=401) + expected_status=404) def test_change_user_password_also_deletes_tokens(self): self.test_oauth_flow() diff --git a/keystone/token/provider.py b/keystone/token/provider.py index ef1ae88f0..5000850c1 100644 --- a/keystone/token/provider.py +++ b/keystone/token/provider.py @@ -190,8 +190,11 @@ class Manager(manager.Manager): LOG.exception(_('Unexpected error or malformed token determining ' 'token expiry: %s') % token) - # Token is expired, we have a malformed token, or something went wrong. - raise exception.Unauthorized(_('Failed to validate token')) + # FIXME(morganfainberg): This error message needs to be updated to + # reflect the token couldn't be found, but this change needs to wait + # until Icehouse due to string freeze in Havana. This should be: + # "Failed to find valid token" or something similar. + raise exception.TokenNotFound(_('Failed to validate token')) def _token_belongs_to(self, token, belongs_to): """Check if the token belongs to the right tenant. @@ -292,7 +295,7 @@ class Provider(object): :param token_id: identity of the token :type token_id: string :returns: token_data - :raises: keystone.exception.Unauthorized + :raises: keystone.exception.TokenNotFound """ raise exception.NotImplemented() @@ -304,7 +307,7 @@ class Provider(object): :param token_id: identity of the token :type token_id: string :returns: token data - :raises: keystone.exception.Unauthorized + :raises: keystone.exception.TokenNotFound """ raise exception.NotImplemented() @@ -317,6 +320,6 @@ class Provider(object): :param belongs_to: project_id token belongs to :type belongs_to: string :returns: token data - :raises: keystone.exception.Unauthorized + :raises: keystone.exception.TokenNotFound """ raise exception.NotImplemented() diff --git a/keystone/token/providers/uuid.py b/keystone/token/providers/uuid.py index 9f77d33fa..afbacc789 100644 --- a/keystone/token/providers/uuid.py +++ b/keystone/token/providers/uuid.py @@ -459,11 +459,8 @@ class Provider(token.provider.Provider): def _verify_token(self, token_id): """Verify the given token and return the token_ref.""" - try: - token_ref = self.token_api.get_token(token_id) - return self._verify_token_ref(token_ref) - except exception.TokenNotFound: - raise exception.Unauthorized() + token_ref = self.token_api.get_token(token_id) + return self._verify_token_ref(token_ref) def _verify_token_ref(self, token_ref): """Verify and return the given token_ref.""" @@ -551,9 +548,9 @@ class Provider(token.provider.Provider): token_data = self.v2_token_data_helper.format_token( token_ref, roles_ref, catalog_ref) return token_data - except (exception.ValidationError, exception.TokenNotFound) as e: + except exception.ValidationError as e: LOG.exception(_('Failed to validate token')) - raise exception.Unauthorized(e) + raise exception.TokenNotFound(e) def validate_v3_token(self, token_id): try: @@ -561,7 +558,6 @@ class Provider(token.provider.Provider): token_data = self._validate_v3_token_ref(token_ref) return token_data except (exception.ValidationError, - exception.TokenNotFound, exception.UserNotFound): LOG.exception(_('Failed to validate token'))