From 8c397fa727c8438c1848bc77b3737768716468da Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Sat, 11 Aug 2018 11:29:59 -0700 Subject: [PATCH] Convert OS-INHERIT API to flask native dispatching Convert OS-INHERIT API to flask native dispatching. NOTE: A minor test change was needed, the test was mis-constructing the URI with multiple slashes. The test now properly constructs the URI using an lstrip when combining the direct_url bits. Change-Id: I0907eb00cdfb9849342220f9b528f94175e71545 Partial-Bug: #1776504 --- keystone/api/__init__.py | 3 + keystone/api/_shared/json_home_relations.py | 5 + keystone/api/os_inherit.py | 399 ++++++++++++++++++++ keystone/assignment/routers.py | 87 +---- keystone/server/flask/application.py | 3 +- keystone/tests/unit/test_v3_assignment.py | 3 +- 6 files changed, 412 insertions(+), 88 deletions(-) create mode 100644 keystone/api/os_inherit.py diff --git a/keystone/api/__init__.py b/keystone/api/__init__.py index 0e56b45aae..4fbf4e95ce 100644 --- a/keystone/api/__init__.py +++ b/keystone/api/__init__.py @@ -17,6 +17,7 @@ from keystone.api import groups from keystone.api import limits from keystone.api import os_ep_filter from keystone.api import os_federation +from keystone.api import os_inherit from keystone.api import os_oauth1 from keystone.api import os_revoke from keystone.api import os_simple_cert @@ -38,6 +39,7 @@ __all__ = ( 'limits', 'os_ep_filter', 'os_federation', + 'os_inherit', 'os_oauth1', 'os_revoke', 'os_simple_cert', @@ -60,6 +62,7 @@ __apis__ = ( limits, os_ep_filter, os_federation, + os_inherit, os_oauth1, os_revoke, os_simple_cert, diff --git a/keystone/api/_shared/json_home_relations.py b/keystone/api/_shared/json_home_relations.py index fcffa277e5..1065b558ce 100644 --- a/keystone/api/_shared/json_home_relations.py +++ b/keystone/api/_shared/json_home_relations.py @@ -65,3 +65,8 @@ os_federation_resource_rel_func = functools.partial( os_federation_parameter_rel_func = functools.partial( json_home.build_v3_extension_parameter_relation, extension_name='OS-FEDERATION', extension_version='1.0') + +# OS-INHERIT "extension" +os_inherit_resource_rel_func = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-INHERIT', extension_version='1.0') diff --git a/keystone/api/os_inherit.py b/keystone/api/os_inherit.py new file mode 100644 index 0000000000..8d1fab3020 --- /dev/null +++ b/keystone/api/os_inherit.py @@ -0,0 +1,399 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This file handles all flask-restful resources for /v3/OS-INHERIT + +import flask_restful +from oslo_log import log +from six.moves import http_client + +from keystone.api._shared import json_home_relations +from keystone.common import json_home +from keystone.common import provider_api +from keystone.common import rbac_enforcer +from keystone import exception +from keystone.server import flask as ks_flask + + +ENFORCER = rbac_enforcer.RBACEnforcer +PROVIDERS = provider_api.ProviderAPIs +LOG = log.getLogger(__name__) + +_build_resource_relation = json_home_relations.os_inherit_resource_rel_func + + +def _build_enforcement_target_attr(role_id=None, user_id=None, group_id=None, + project_id=None, domain_id=None, + allow_non_existing=False): + """Check protection for role grant APIs. + + The policy rule might want to inspect attributes of any of the entities + involved in the grant. So we get these and pass them to the + check_protection() handler in the controller. + + """ + # !!!!!!!!!! WARNING: Security Concern !!!!!!!!!! + # + # NOTE(morgan): This function must handle all expected exceptions, + # including NOT FOUNDs otherwise the exception will be raised up to the + # end user before enforcement, resulting in the exception being returned + # instead of an appropriate 403. In each case, it is logged that a value + # was not found and the target is explicitly set to empty. This allows for + # the enforcement rule to decide what to do (most of the time raise an + # appropriate 403). + # + # ############################################### + + target = {} + if role_id: + try: + target['role'] = PROVIDERS.role_api.get_role(role_id) + except exception.RoleNotFound: + LOG.info('Role (%(role_id)s) not found, Enforcement target of ' + '`role` remaind empty', {'role_id': role_id}) + target['role'] = {} + if user_id: + try: + target['user'] = PROVIDERS.identity_api.get_user(user_id) + except exception.UserNotFound: + if not allow_non_existing: + LOG.info('User (%(user_id)s) was not found. Enforcement target' + ' of `user` remains empty.', {'user_id': user_id}) + target['user'] = {} + else: + try: + target['group'] = PROVIDERS.identity_api.get_group(group_id) + except exception.GroupNotFound: + if not allow_non_existing: + LOG.info('Group (%(group_id)s) was not found. Enforcement ' + 'target of `group` remains empty.', + {'group_id': group_id}) + target['group'] = {} + + # NOTE(lbragstad): This if/else check will need to be expanded in the + # future to handle system hierarchies if that is implemented. + if domain_id: + try: + target['domain'] = PROVIDERS.resource_api.get_domain(domain_id) + except exception.DomainNotFound: + LOG.info('Domain (%(domain_id)s) was not found. Enforcement ' + 'target of `domain` remains empty.', + {'domain_id': domain_id}) + target['domain'] = {} + elif project_id: + try: + target['project'] = PROVIDERS.resource_api.get_project(project_id) + except exception.ProjectNotFound: + LOG.info('Project (%(project_id)s) was not found. Enforcement ' + 'target of `project` remains empty.', + {'project_id': project_id}) + target['project'] = {} + + return target + + +class OSInheritDomainGroupRolesResource(flask_restful.Resource): + def get(self, domain_id, group_id, role_id): + """Check for an inherited grant for a group on a domain. + + GET/HEAD /OS-INHERIT/domains/{domain_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:check_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.get_grant( + domain_id=domain_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def put(self, domain_id, group_id, role_id): + """Create an inherited grant for a group on a domain. + + PUT /OS-INHERIT/domains/{domain_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:create_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.create_grant( + domain_id=domain_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def delete(self, domain_id, group_id, role_id): + """Revoke an inherited grant for a group on a domain. + + DELETE /OS-INHERIT/domains/{domain_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:revoke_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.delete_grant( + domain_id=domain_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + +class OSInheritDomainGroupRolesListResource(flask_restful.Resource): + def get(self, domain_id, group_id): + """List roles (inherited) for a group on a domain. + + GET/HEAD /OS-INHERIT/domains/{domain_id}/groups/{group_id} + /roles/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:list_grants', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, group_id=group_id)) + refs = PROVIDERS.assignment_api.list_grants( + domain_id=domain_id, group_id=group_id, inherited_to_projects=True) + return ks_flask.ResourceBase.wrap_collection( + refs, collection_name='roles') + + +class OSInheritDomainUserRolesResource(flask_restful.Resource): + def get(self, domain_id, user_id, role_id): + """Check for an inherited grant for a user on a domain. + + GET/HEAD /OS-INHERIT/domains/{domain_id}/users/{user_id}/roles + /{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:check_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.get_grant( + domain_id=domain_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def put(self, domain_id, user_id, role_id): + """Create an inherited grant for a user on a domain. + + PUT /OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/{role_id} + /inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:create_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.create_grant( + domain_id=domain_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def delete(self, domain_id, user_id, role_id): + """Revoke a grant from a user on a domain. + + DELETE /OS-INHERIT/domains/{domain_id}/users/{user_id}/roles + /{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:revoke_grant', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.delete_grant( + domain_id=domain_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + +class OSInheritDomainUserRolesListResource(flask_restful.Resource): + def get(self, domain_id, user_id): + """List roles (inherited) for a user on a domain. + + GET/HEAD /OS-INHERIT/domains/{domain_id}/users/{user_id} + /roles/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:list_grants', + target_attr=_build_enforcement_target_attr( + domain_id=domain_id, user_id=user_id)) + refs = PROVIDERS.assignment_api.list_grants( + domain_id=domain_id, user_id=user_id, inherited_to_projects=True) + return ks_flask.ResourceBase.wrap_collection( + refs, collection_name='roles') + + +class OSInheritProjectUserResource(flask_restful.Resource): + def get(self, project_id, user_id, role_id): + """Check for an inherited grant for a user on a project. + + GET/HEAD /OS-INHERIT/projects/{project_id}/users/{user_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:check_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.get_grant( + project_id=project_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def put(self, project_id, user_id, role_id): + """Create an inherited grant for a user on a project. + + PUT /OS-INHERIT/projects/{project_id}/users/{user_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:create_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.create_grant( + project_id=project_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def delete(self, project_id, user_id, role_id): + """Revoke an inherited grant for a user on a project. + + DELETE /OS-INHERIT/projects/{project_id}/users/{user_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:revoke_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, user_id=user_id, role_id=role_id)) + PROVIDERS.assignment_api.delete_grant( + project_id=project_id, user_id=user_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + +class OSInheritProjectGroupResource(flask_restful.Resource): + def get(self, project_id, group_id, role_id): + """Check for an inherited grant for a group on a project. + + GET/HEAD /OS-INHERIT/projects/{project_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:check_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.get_grant( + project_id=project_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def put(self, project_id, group_id, role_id): + """Create an inherited grant for a group on a project. + + PUT /OS-INHERIT/projects/{project_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:create_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.create_grant( + project_id=project_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + def delete(self, project_id, group_id, role_id): + """Revoke an inherited grant for a group on a project. + + DELETE /OS-INHERIT/projects/{project_id}/groups/{group_id} + /roles/{role_id}/inherited_to_projects + """ + ENFORCER.enforce_call( + action='identity:revoke_grant', + target_attr=_build_enforcement_target_attr( + project_id=project_id, group_id=group_id, role_id=role_id)) + PROVIDERS.assignment_api.delete_grant( + project_id=project_id, group_id=group_id, role_id=role_id, + inherited_to_projects=True) + return None, http_client.NO_CONTENT + + +class OSInheritAPI(ks_flask.APIBase): + _name = "OS-INHERIT" + _import_name = __name__ + _api_url_prefix = '/OS-INHERIT' + resources = [] + resource_mapping = [ + ks_flask.construct_resource_map( + resource=OSInheritDomainGroupRolesResource, + url=('/domains//groups//roles' + '//inherited_to_projects'), + resource_kwargs={}, + rel='domain_group_role_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID}), + ks_flask.construct_resource_map( + resource=OSInheritDomainGroupRolesListResource, + url=('/domains//groups//roles' + '/inherited_to_projects'), + resource_kwargs={}, + rel='domain_group_roles_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID}), + ks_flask.construct_resource_map( + resource=OSInheritDomainUserRolesResource, + url=('/domains//users//roles' + '//inherited_to_projects'), + resource_kwargs={}, + rel='domain_user_role_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, + 'role_id': json_home.Parameters.ROLE_ID}), + ks_flask.construct_resource_map( + resource=OSInheritDomainUserRolesListResource, + url=('/domains//users//roles' + '/inherited_to_projects'), + resource_kwargs={}, + rel='domain_user_roles_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID}), + ks_flask.construct_resource_map( + resource=OSInheritProjectUserResource, + url=('projects//users//roles' + '//inherited_to_projects'), + resource_kwargs={}, + rel='project_user_role_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'user_id': json_home.Parameters.USER_ID, + 'role_id': json_home.Parameters.ROLE_ID}), + ks_flask.construct_resource_map( + resource=OSInheritProjectGroupResource, + url=('projects//groups//roles' + '//inherited_to_projects'), + resource_kwargs={}, + rel='project_group_role_inherited_to_projects', + resource_relation_func=_build_resource_relation, + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID}) + ] + + +APIs = (OSInheritAPI,) diff --git a/keystone/assignment/routers.py b/keystone/assignment/routers.py index 2a4a694b2c..ee71173f79 100644 --- a/keystone/assignment/routers.py +++ b/keystone/assignment/routers.py @@ -15,18 +15,11 @@ """WSGI Routers for the Assignment service.""" -import functools - from keystone.assignment import controllers from keystone.common import json_home from keystone.common import wsgi -build_os_inherit_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-INHERIT', extension_version='1.0') - - class Public(wsgi.ComposableRouter): def add_routes(self, mapper): tenant_controller = controllers.TenantAssignment() @@ -38,7 +31,7 @@ class Public(wsgi.ComposableRouter): class Routers(wsgi.RoutersBase): - _path_prefixes = ('users', 'projects', 'domains', 'OS-INHERIT') + _path_prefixes = ('users', 'projects', 'domains') def append_v3_routers(self, mapper, routers): @@ -137,81 +130,3 @@ class Routers(wsgi.RoutersBase): 'domain_id': json_home.Parameters.DOMAIN_ID, 'group_id': json_home.Parameters.GROUP_ID, }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/' - '{role_id}/inherited_to_projects', - get_head_action='check_grant', - put_action='create_grant', - delete_action='revoke_grant', - rel=build_os_inherit_relation( - resource_name='domain_user_role_inherited_to_projects'), - path_vars={ - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'role_id': json_home.Parameters.ROLE_ID, - 'user_id': json_home.Parameters.USER_ID, - }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/' - '{role_id}/inherited_to_projects', - get_head_action='check_grant', - put_action='create_grant', - delete_action='revoke_grant', - rel=build_os_inherit_relation( - resource_name='domain_group_role_inherited_to_projects'), - path_vars={ - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'group_id': json_home.Parameters.GROUP_ID, - 'role_id': json_home.Parameters.ROLE_ID, - }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/' - 'inherited_to_projects', - get_head_action='list_grants', - rel=build_os_inherit_relation( - resource_name='domain_group_roles_inherited_to_projects'), - path_vars={ - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'group_id': json_home.Parameters.GROUP_ID, - }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/' - 'inherited_to_projects', - get_head_action='list_grants', - rel=build_os_inherit_relation( - resource_name='domain_user_roles_inherited_to_projects'), - path_vars={ - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'user_id': json_home.Parameters.USER_ID, - }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/projects/{project_id}/users/{user_id}/roles/' - '{role_id}/inherited_to_projects', - get_head_action='check_grant', - put_action='create_grant', - delete_action='revoke_grant', - rel=build_os_inherit_relation( - resource_name='project_user_role_inherited_to_projects'), - path_vars={ - 'project_id': json_home.Parameters.PROJECT_ID, - 'user_id': json_home.Parameters.USER_ID, - 'role_id': json_home.Parameters.ROLE_ID, - }) - self._add_resource( - mapper, grant_controller, - path='/OS-INHERIT/projects/{project_id}/groups/{group_id}/' - 'roles/{role_id}/inherited_to_projects', - get_head_action='check_grant', - put_action='create_grant', - delete_action='revoke_grant', - rel=build_os_inherit_relation( - resource_name='project_group_role_inherited_to_projects'), - path_vars={ - 'project_id': json_home.Parameters.PROJECT_ID, - 'group_id': json_home.Parameters.GROUP_ID, - 'role_id': json_home.Parameters.ROLE_ID, - }) diff --git a/keystone/server/flask/application.py b/keystone/server/flask/application.py index 2ee5e06bd4..ed39391229 100644 --- a/keystone/server/flask/application.py +++ b/keystone/server/flask/application.py @@ -41,9 +41,10 @@ _MOVED_API_PREFIXES = frozenset( ['credentials', 'endpoints', 'groups', - 'OS-OAUTH1', 'OS-EP-FILTER', 'OS-FEDERATION', + 'OS-INHERIT', + 'OS-OAUTH1', 'OS-REVOKE', 'OS-SIMPLE-CERT', 'OS-TRUST', diff --git a/keystone/tests/unit/test_v3_assignment.py b/keystone/tests/unit/test_v3_assignment.py index e70ec79638..2ef56ac273 100644 --- a/keystone/tests/unit/test_v3_assignment.py +++ b/keystone/tests/unit/test_v3_assignment.py @@ -1693,7 +1693,8 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, # Define URLs direct_url = '%s/users/%s/roles/%s' % ( target_url, self.user_id, role['id']) - inherited_url = '/OS-INHERIT/%s/inherited_to_projects' % direct_url + inherited_url = ('/OS-INHERIT/%s/inherited_to_projects' % + direct_url.lstrip('/')) # Create the direct assignment self.put(direct_url)