Implement GET /v3/auth/system

Keystone has APIs for retrieving projects and domains based on the
role assignments a user has on projects and domains. We should
introduce similar functionality for system assignments. This will
make discovering system access for users and client easier.

bp system-scope

Change-Id: Iab577fcd1b57b8b5593c3f9d50a772466383a999
This commit is contained in:
Lance Bragstad 2017-12-29 17:04:53 +00:00
parent 9cd5f198da
commit a50fafd246
12 changed files with 242 additions and 4 deletions

View File

@ -921,4 +921,55 @@ Example
~~~~~~~
.. literalinclude:: ./samples/admin/get-available-domain-scopes-response.json
:language: javascript
:language: javascript
Get available system scopes
===========================
.. rest_method:: GET /v3/auth/system
New in version 3.10
This call returns the list of systems that are available to be scoped
to based on the X-Auth-Token provided in the request.
Relationship: ``https://docs.openstack.org/api/openstack-identity/3/rel/auth_system``
Request
-------
Parameters
~~~~~~~~~~
.. rest_parameters:: parameters.yaml
- X-Auth-Token: X-Auth-Token
Response
--------
Parameters
~~~~~~~~~~
.. rest_parameters:: parameters.yaml
- links: domain_link_response_body
- system: response_body_system_required
Status Codes
~~~~~~~~~~~~
.. rest_status_code:: success status.yaml
- 200
.. rest_status_code:: error status.yaml
- 401
- 400
Example
~~~~~~~
.. literalinclude:: ./samples/admin/get-available-system-scopes-response.json
:language: javascript

View File

@ -1315,6 +1315,12 @@ response_body_project_tags_required:
in: body
required: true
type: array
response_body_system_required:
description: |
A list of systems to access based on role assignments.
in: body
required: true
type: array
role:
description: |
A ``role`` object, containing:

View File

@ -0,0 +1,10 @@
{
"system": [
{
"all": true
}
],
"links": {
"self": "https://example.com/identity/v3/auth/system"
}
}

View File

@ -190,6 +190,7 @@ identity:delete_service_provider DELETE /v3/OS-FEDERAT
identity:get_auth_catalog GET /v3/auth/catalog
identity:get_auth_projects GET /v3/auth/projects
identity:get_auth_domains GET /v3/auth/domains
identity:get_auth_system GET /v3/auth/system
identity:list_projects_for_user GET /v3/OS-FEDERATION/projects
identity:list_domains_for_user GET /v3/OS-FEDERATION/domains

View File

@ -215,6 +215,7 @@
"identity:get_auth_catalog": "",
"identity:get_auth_projects": "",
"identity:get_auth_domains": "",
"identity:get_auth_system": "",
"identity:list_projects_for_user": "",
"identity:list_domains_for_user": "",

View File

@ -21,6 +21,7 @@ from keystone.auth import core
from keystone.auth import schema
from keystone.common import authorization
from keystone.common import controller
from keystone.common import provider_api
from keystone.common import utils
from keystone.common import validation
from keystone.common import wsgi
@ -34,6 +35,7 @@ from keystone.resource import controllers as resource_controllers
LOG = log.getLogger(__name__)
CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
def validate_issue_token_auth(auth=None):
@ -398,6 +400,54 @@ class Auth(controller.V3Controller):
return resource_controllers.DomainV3.wrap_collection(
request.context_dict, refs)
@controller.protected()
def get_auth_system(self, request):
user_id = request.auth_context.get('user_id')
group_ids = request.auth_context.get('group_ids')
user_assignments = []
if user_id:
try:
user_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_user(
user_id
)
)
except exception.UserNotFound: # nosec
# federated users have an id but they don't link to anything
pass
group_assignments = []
if group_ids:
group_assignments = (
PROVIDERS.assignment_api.list_system_grants_for_group(
group_ids
)
)
assignments = self._combine_lists_uniquely(
user_assignments, group_assignments
)
if assignments:
response = {
'system': [{'all': True}],
'links': {
'self': self.base_url(
request.context_dict, path='auth/system'
)
}
}
else:
response = {
'system': [],
'links': {
'self': self.base_url(
request.context_dict, path='auth/system'
)
}
}
return response
@controller.protected()
def get_auth_catalog(self, request):
user_id = request.auth_context.get('user_id')

View File

@ -55,3 +55,9 @@ class Routers(wsgi.RoutersBase):
path='/auth/domains',
get_head_action='get_auth_domains',
rel=json_home.build_v3_resource_relation('auth_domains'))
self._add_resource(
mapper, auth_controller,
path='/auth/system',
get_head_action='get_auth_system',
rel=json_home.build_v3_resource_relation('auth_system'))

View File

@ -61,6 +61,21 @@ auth_policies = [
'method': 'HEAD'
}
]
),
policy.DocumentedRuleDefault(
name=base.IDENTITY % 'get_auth_system',
check_str='',
description='List systems a user has access to via role assignments.',
operations=[
{
'path': '/v3/auth/system',
'method': 'GET'
},
{
'path': '/v3/auth/system',
'method': 'HEAD'
}
]
)
]

View File

@ -4703,6 +4703,102 @@ class TestAuthSpecificData(test_v3.RestfulTestCase):
self.head('/auth/domains', expected_status=http_client.OK)
def test_get_system_roles_with_unscoped_token(self):
path = '/system/users/%(user_id)s/roles/%(role_id)s' % {
'user_id': self.user['id'],
'role_id': self.role_id
}
self.put(path=path)
unscoped_request = self.build_authentication_request(
user_id=self.user['id'], password=self.user['password']
)
r = self.post('/auth/tokens', body=unscoped_request)
unscoped_token = r.headers.get('X-Subject-Token')
self.assertValidUnscopedTokenResponse(r)
response = self.get('/auth/system', token=unscoped_token)
self.assertTrue(response.json_body['system'][0]['all'])
self.head(
'/auth/system', token=unscoped_token,
expected_status=http_client.OK
)
def test_get_system_roles_returns_empty_list_without_system_roles(self):
# A user without a system role assignment shouldn't expect an empty
# list when calling /v3/auth/system regardless of calling the API with
# an unscoped token or a project-scoped token.
unscoped_request = self.build_authentication_request(
user_id=self.user['id'], password=self.user['password']
)
r = self.post('/auth/tokens', body=unscoped_request)
unscoped_token = r.headers.get('X-Subject-Token')
self.assertValidUnscopedTokenResponse(r)
response = self.get('/auth/system', token=unscoped_token)
self.assertEqual(response.json_body['system'], [])
self.head(
'/auth/system', token=unscoped_token,
expected_status=http_client.OK
)
project_scoped_request = self.build_authentication_request(
user_id=self.user['id'], password=self.user['password'],
project_id=self.project_id
)
r = self.post('/auth/tokens', body=project_scoped_request)
project_scoped_token = r.headers.get('X-Subject-Token')
self.assertValidProjectScopedTokenResponse(r)
response = self.get('/auth/system', token=project_scoped_token)
self.assertEqual(response.json_body['system'], [])
self.head(
'/auth/system', token=project_scoped_token,
expected_status=http_client.OK
)
def test_get_system_roles_with_project_scoped_token(self):
path = '/system/users/%(user_id)s/roles/%(role_id)s' % {
'user_id': self.user['id'],
'role_id': self.role_id
}
self.put(path=path)
self.put(path='/domains/%s/users/%s/roles/%s' % (
self.domain['id'], self.user['id'], self.role['id']))
domain_scoped_request = self.build_authentication_request(
user_id=self.user['id'], password=self.user['password'],
domain_id=self.domain['id']
)
r = self.post('/auth/tokens', body=domain_scoped_request)
domain_scoped_token = r.headers.get('X-Subject-Token')
self.assertValidDomainScopedTokenResponse(r)
response = self.get('/auth/system', token=domain_scoped_token)
self.assertTrue(response.json_body['system'][0]['all'])
self.head(
'/auth/system', token=domain_scoped_token,
expected_status=http_client.OK
)
def test_get_system_roles_with_domain_scoped_token(self):
path = '/system/users/%(user_id)s/roles/%(role_id)s' % {
'user_id': self.user['id'],
'role_id': self.role_id
}
self.put(path=path)
project_scoped_request = self.build_authentication_request(
user_id=self.user['id'], password=self.user['password'],
project_id=self.project_id
)
r = self.post('/auth/tokens', body=project_scoped_request)
project_scoped_token = r.headers.get('X-Subject-Token')
self.assertValidProjectScopedTokenResponse(r)
response = self.get('/auth/system', token=project_scoped_token)
self.assertTrue(response.json_body['system'][0]['all'])
self.head(
'/auth/system', token=project_scoped_token,
expected_status=http_client.OK
)
class TestTrustAuthFernetTokenProvider(TrustAPIBehavior, TestTrustChain):
def config_overrides(self):

View File

@ -71,7 +71,7 @@ v3_MEDIA_TYPES = [
]
v3_EXPECTED_RESPONSE = {
"id": "v3.9",
"id": "v3.10",
"status": "stable",
"updated": "2018-02-28T00:00:00Z",
"links": [
@ -184,6 +184,8 @@ V3_JSON_HOME_RESOURCES = {
'href': '/auth/projects'},
json_home.build_v3_resource_relation('auth_domains'): {
'href': '/auth/domains'},
json_home.build_v3_resource_relation('auth_system'): {
'href': '/auth/system'},
json_home.build_v3_resource_relation('credential'): {
'href-template': '/credentials/{credential_id}',
'href-vars': {

View File

@ -12,4 +12,4 @@
def release_string():
return 'v3.9'
return 'v3.10'

View File

@ -141,7 +141,7 @@ class Version(wsgi.Application):
if 'v3' in _VERSIONS:
versions['v3'] = {
'id': 'v3.9',
'id': 'v3.10',
'status': 'stable',
'updated': '2018-02-28T00:00:00Z',
'links': [