set `is_admin` on tokens for admin project

Adds two new configuration value:

admin_project_name
admin_project_domain_name

If both values are set, and tokens requested for
projects (only, not domains) that match both will have an
additional value in them;  `is_admin_project=true`

DocImpact
-- Configuration changes need documentation
APIImpact
-- Adds optional return values in token validation calls
SecurityImpact
-- Should be helpful in making access control decisions

Implements: blueprint is-admin-project
Partial-Bug: #968696

Change-Id: Ic9cf9862739381a30130b4be87075f726736ff88
This commit is contained in:
Adam Young 2015-10-11 23:15:52 -04:00
parent 933e118eee
commit e7023697a8
5 changed files with 113 additions and 0 deletions

View File

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

View File

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

View File

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

View File

@ -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 = []

View File

@ -0,0 +1,14 @@
---
features:
- >
[`bug 96869 <https://bugs.launchpad.net/keystone/+bug/968696>`_]
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.