diff --git a/keystone/common/config.py b/keystone/common/config.py index 903625daa6..88cce3cc90 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -386,6 +386,17 @@ FILE_OPTIONS = { group='assignment')], help='Maximum number of entities that will be returned ' 'in a resource collection.'), + cfg.StrOpt('admin_project_domain_name', + help='Name of the domain that contains the special ' + 'project for performing administrative operations on ' + 'remote services. Tokens scoped to this project will ' + 'contain the key/value `is_admin_project=true`. Defaults ' + 'to None.'), + cfg.StrOpt('admin_project_name', + help='Special project for performing administrative ' + 'operations on remote services. Tokens scoped to ' + 'this project will contain the key/value ' + '`is_admin_project=true`. Defaults to None.'), ], 'domain_config': [ cfg.StrOpt('driver', diff --git a/keystone/tests/unit/test_v3.py b/keystone/tests/unit/test_v3.py index 8b0934d519..fbd4ddb812 100644 --- a/keystone/tests/unit/test_v3.py +++ b/keystone/tests/unit/test_v3.py @@ -572,6 +572,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, require_catalog = kwargs.pop('require_catalog', True) endpoint_filter = kwargs.pop('endpoint_filter', False) ep_filter_assoc = kwargs.pop('ep_filter_assoc', 0) + is_admin_project = kwargs.pop('is_admin_project', False) token = self.assertValidTokenResponse(r, *args, **kwargs) if require_catalog: @@ -599,6 +600,11 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, self.assertIn('id', role) self.assertIn('name', role) + if is_admin_project: + self.assertIs(True, token['is_admin_project']) + else: + self.assertNotIn('is_admin_project', token) + return token def assertValidProjectScopedTokenResponse(self, r, *args, **kwargs): diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py index e22511e01c..d61d7de844 100644 --- a/keystone/tests/unit/test_v3_auth.py +++ b/keystone/tests/unit/test_v3_auth.py @@ -413,6 +413,76 @@ class TokenAPITests(object): headers={'X-Subject-Token': v3_token}) self.assertValidProjectScopedTokenResponse(r, require_catalog=False) + def test_is_admin_token_by_ids(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + + def test_is_admin_token_by_names(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_domain_name=self.domain['name'], + project_name=self.project['name'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + + def test_token_for_non_admin_project_is_not_admin(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=uuid.uuid4().hex) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + + def test_token_for_non_admin_domain_same_project_name_is_not_admin(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=uuid.uuid4().hex, + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + + def test_only_admin_project_set_acts_as_non_admin(self): + self.config_fixture.config( + group='resource', + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def config_overrides(self): diff --git a/keystone/token/providers/common.py b/keystone/token/providers/common.py index fd87b3de4e..c0a0272b9a 100644 --- a/keystone/token/providers/common.py +++ b/keystone/token/providers/common.py @@ -253,6 +253,16 @@ class V3TokenDataHelper(object): return filtered_project def _populate_scope(self, token_data, domain_id, project_id): + # TODO(ayoung): Support the ability for a project acting as a domain + # to be the admin project once the rest of the code for domains + # acting as projects is merged. Code will likely be: + # (r.admin_project_name == None and project['is_domain'] == True + # and project['name'] == r.admin_project_domain_name) + def _is_admin_project(project): + r = CONF.resource + return (project['name'] == r.admin_project_name and + project['domain']['name'] == r.admin_project_domain_name) + if 'domain' in token_data or 'project' in token_data: # scope already exist, no need to populate it again return @@ -261,6 +271,8 @@ class V3TokenDataHelper(object): token_data['domain'] = self._get_filtered_domain(domain_id) if project_id: token_data['project'] = self._get_filtered_project(project_id) + if _is_admin_project(token_data['project']): + token_data['is_admin_project'] = True def _get_roles_for_user(self, user_id, domain_id, project_id): roles = [] diff --git a/releasenotes/notes/is-admin-24b34238c83b3a82.yaml b/releasenotes/notes/is-admin-24b34238c83b3a82.yaml new file mode 100644 index 0000000000..6505fbd416 --- /dev/null +++ b/releasenotes/notes/is-admin-24b34238c83b3a82.yaml @@ -0,0 +1,14 @@ +--- +features: + - > + [`bug 96869 `_] + A pair of configuration options have been added to the ``[resource]`` + section to specify a special ``admin`` project: + ``admin_project_domain_name`` and ``admin_project_name``. If these are + defined, any scoped token issued for that project will have an additional + identifier ``is_admin_project`` added to the token. This identifier can then + be checked by the policy rules in the policy files of the services when + evaluating access control policy for an API. Keystone does not yet + support the ability for a project acting as a domain to be the + admin project. That will be added once the rest of the code for + domains acting as projects is merged.