diff --git a/keystone/api/__init__.py b/keystone/api/__init__.py index ca99cdddfc..8d22946773 100644 --- a/keystone/api/__init__.py +++ b/keystone/api/__init__.py @@ -20,6 +20,7 @@ from keystone.api import os_revoke from keystone.api import os_simple_cert from keystone.api import regions from keystone.api import registered_limits +from keystone.api import roles from keystone.api import services from keystone.api import trusts @@ -34,6 +35,7 @@ __all__ = ( 'os_simple_cert', 'regions', 'registered_limits', + 'roles', 'services', 'trusts', ) @@ -49,6 +51,7 @@ __apis__ = ( os_simple_cert, regions, registered_limits, + roles, services, trusts, ) diff --git a/keystone/api/roles.py b/keystone/api/roles.py new file mode 100644 index 0000000000..2d568035c3 --- /dev/null +++ b/keystone/api/roles.py @@ -0,0 +1,332 @@ +# 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/roles + +import flask +import flask_restful +from six.moves import http_client + +from keystone.assignment import schema +from keystone.common import json_home +from keystone.common import provider_api +from keystone.common import rbac_enforcer +from keystone.common import validation +import keystone.conf +from keystone.server import flask as ks_flask + + +CONF = keystone.conf.CONF +ENFORCER = rbac_enforcer.RBACEnforcer +PROVIDERS = provider_api.ProviderAPIs + + +class RoleResource(ks_flask.ResourceBase): + collection_key = 'roles' + member_key = 'role' + + def __init__(self): + super(RoleResource, self).__init__() + self.get_member_from_driver = PROVIDERS.role_api.get_role + + def _is_domain_role(self, role): + return bool(role.get('domain_id')) + + def get(self, role_id=None): + """Get role or list roles. + + GET/HEAD /v3/roles + GET/HEAD /v3/roles/{role_id} + """ + if role_id is not None: + return self._get_role(role_id) + return self._list_roles() + + def _get_role(self, role_id): + err = None + role = {} + try: + role = PROVIDERS.role_api.get_role(role_id) + except Exception as e: # nosec + # We don't raise out here, we raise out after enforcement, this + # ensures we do not leak role existence. Do nothing yet, process + # enforcement before raising out an error. + err = e + finally: + # NOTE(morgan): There are a couple of cases to be aware of here + # if there is an exception (e is not None), then we are enforcing + # on "get_role" to be safe. If the role is not a "domain_role", + # we are enforcing on "get_role". If the role is "domain_role" we + # are inforcing on "get_domain_role" + if err is not None or not self._is_domain_role(role): + ENFORCER.enforce_call(action='identity:get_role') + if err: + # reraise the error after enforcement if needed. + raise err + else: + ENFORCER.enforce_call(action='identity:get_domain_role', + member_target_type='role', + member_target=role) + return self.wrap_member(role) + + def _list_roles(self): + filters = ['name', 'domain_id'] + domain_filter = flask.request.args.get('domain_id') + if domain_filter: + ENFORCER.enforce_call(action='identity:list_domain_roles', + filters=filters) + else: + ENFORCER.enforce_call(action='identity:list_roles', + filters=filters) + + hints = self.build_driver_hints(filters) + if not domain_filter: + # NOTE(jamielennox): To handle the default case of not domain_id + # defined the role_assignment backend does some hackery to + # distinguish between global and domain scoped roles. This backend + # behaviour relies upon a value of domain_id being set (not just + # defaulting to None). Manually set the filter if its not + # provided. + hints.add_filter('domain_id', None) + refs = PROVIDERS.role_api.list_roles(hints=hints) + return self.wrap_collection(refs) + + def post(self): + """Create role. + + POST /v3/roles + """ + role = self.request_body_json.get('role', {}) + if self._is_domain_role(role): + ENFORCER.enforce_call(action='identity:create_domain_role') + else: + ENFORCER.enforce_call(action='identity:create_role') + validation.lazy_validate(schema.role_create, role) + if role['name'] == CONF.member_role_name: + # Use the configured member role ID when creating the configured + # member role name. This avoids the potential of creating + # a "member" role with an unexpected ID. + role['id'] = CONF.member_role_id + else: + role = self._assign_unique_id(role) + role = self._normalize_dict(role) + ref = PROVIDERS.role_api.create_role( + role['id'], role, initiator=self.audit_initiator) + return self.wrap_member(ref), http_client.CREATED + + def patch(self, role_id): + """Update role. + + PATCH /v3/roles/{role_id} + """ + err = None + role = {} + try: + role = PROVIDERS.role_api.get_role(role_id) + except Exception as e: # nosec + # We don't raise out here, we raise out after enforcement, this + # ensures we do not leak role existence. Do nothing yet, process + # enforcement before raising out an error. + err = e + finally: + if err is not None or not self._is_domain_role(role): + ENFORCER.enforce_call(action='identity:update_role') + if err: + raise err + else: + ENFORCER.enforce_call(action='identity:update_domain_role', + member_target_type='role', + member_target=role) + request_body_role = self.request_body_json.get('role', {}) + validation.lazy_validate(schema.role_update, request_body_role) + self._require_matching_id(request_body_role) + ref = PROVIDERS.role_api.update_role( + role_id, request_body_role, initiator=self.audit_initiator) + return self.wrap_member(ref) + + def delete(self, role_id): + """Delete role. + + DELETE /v3/roles/{role_id} + """ + err = None + role = {} + try: + role = PROVIDERS.role_api.get_role(role_id) + except Exception as e: # nosec + # We don't raise out here, we raise out after enforcement, this + # ensures we do not leak role existence. Do nothing yet, process + # enforcement before raising out an error. + err = e + finally: + if err is not None or not self._is_domain_role(role): + ENFORCER.enforce_call(action='identity:delete_role') + if err: + raise err + else: + ENFORCER.enforce_call(action='identity:delete_domain_role', + member_target_type='role', + member_target=role) + PROVIDERS.role_api.delete_role(role_id, initiator=self.audit_initiator) + return None, http_client.NO_CONTENT + + +def _build_enforcement_target_ref(): + ref = {} + if flask.request.view_args: + ref['prior_role'] = PROVIDERS.role_api.get_role( + flask.request.view_args.get('prior_role_id')) + if flask.request.view_args.get('implied_role_id'): + ref['implied_role'] = PROVIDERS.role_api.get_role( + flask.request.view_args['implied_role_id']) + return ref + + +def _build_prior_role_response_data(prior_role_id, prior_role_name): + return { + 'id': prior_role_id, + 'links': { + 'self': ks_flask.base_url(path='/roles/%s' % prior_role_id) + }, + 'name': prior_role_name} + + +def _build_implied_role_response_data(implied_role): + return { + 'id': implied_role['id'], + 'links': { + 'self': ks_flask.base_url( + path='/roles/%s' % implied_role['id']) + }, + 'name': implied_role['name']} + + +def _role_inference_response(prior_role_id): + prior_role = PROVIDERS.role_api.get_role(prior_role_id) + response = { + 'role_inference': { + 'prior_role': _build_prior_role_response_data( + prior_role_id, prior_role['name'])}} + return response + + +class RoleImplicationListResource(flask_restful.Resource): + def get(self, prior_role_id): + """List Implied Roles. + + GET/HEAD /v3/roles/{prior_role_id}/implies + """ + ENFORCER.enforce_call(action='identity:list_implied_roles', + target_attr=_build_enforcement_target_ref()) + ref = PROVIDERS.role_api.list_implied_roles(prior_role_id) + implied_ids = [r['implied_role_id'] for r in ref] + response_json = _role_inference_response(prior_role_id) + response_json['role_inference']['implies'] = [] + for implied_id in implied_ids: + implied_role = PROVIDERS.role_api.get_role(implied_id) + response_json['role_inference']['implies'].append( + _build_implied_role_response_data(implied_role)) + response_json['links'] = { + 'self': ks_flask.base_url( + path='/roles/%s/implies' % prior_role_id)} + return response_json + + +class RoleImplicationResource(flask_restful.Resource): + + def head(self, prior_role_id, implied_role_id=None): + # TODO(morgan): deprecate "check_implied_role" policy, as a user must + # have both check_implied_role and get_implied_role to use the head + # action. This enforcement of HEAD is historical for + # consistent policy enforcement behavior even if it is superfluous. + # Alternatively we can keep check_implied_role and reference + # ._get_implied_role instead. + ENFORCER.enforce_call(action='identity:check_implied_role', + target_attr=_build_enforcement_target_ref()) + self.get(prior_role_id, implied_role_id) + # NOTE(morgan): Our API here breaks HTTP Spec. This should be evaluated + # for a future fix. This should just return the above "get" however, + # we document and implment this as a NO_CONTENT response. NO_CONTENT + # here is incorrect. It is maintained as is for API contract reasons. + return None, http_client.NO_CONTENT + + def get(self, prior_role_id, implied_role_id): + """Get implied role. + + GET/HEAD /v3/roles/{prior_role_id}/implies/{implied_role_id} + """ + ENFORCER.enforce_call( + action='identity:get_implied_role', + target_attr=_build_enforcement_target_ref()) + return self._get_implied_role(prior_role_id, implied_role_id) + + def _get_implied_role(self, prior_role_id, implied_role_id): + # Isolate this logic so it can be re-used without added enforcement + PROVIDERS.role_api.get_implied_role( + prior_role_id, implied_role_id) + implied_role_ref = PROVIDERS.role_api.get_role(implied_role_id) + response_json = _role_inference_response(prior_role_id) + response_json['role_inference'][ + 'implies'] = _build_implied_role_response_data( + implied_role_ref) + response_json['links'] = { + 'self': ks_flask.base_url( + path='/roles/%(prior)s/implies/%(implies)s' % { + 'prior': prior_role_id, 'implies': implied_role_id})} + return response_json + + def put(self, prior_role_id, implied_role_id): + """Create implied role. + + PUT /v3/roles/{prior_role_id}/implies/{implied_role_id} + """ + ENFORCER.enforce_call(action='identity:create_implied_role', + target_attr=_build_enforcement_target_ref()) + PROVIDERS.role_api.create_implied_role(prior_role_id, implied_role_id) + response_json = self._get_implied_role(prior_role_id, implied_role_id) + return response_json, http_client.CREATED + + def delete(self, prior_role_id, implied_role_id): + """Delete implied role. + + DELETE /v3/roles/{prior_role_id}/implies/{implied_role_id} + """ + ENFORCER.enforce_call(action='identity:delete_implied_role', + target_attr=_build_enforcement_target_ref()) + PROVIDERS.role_api.delete_implied_role(prior_role_id, implied_role_id) + return None, http_client.NO_CONTENT + + +class RoleAPI(ks_flask.APIBase): + _name = 'roles' + _import_name = __name__ + resources = [RoleResource] + resource_mapping = [ + ks_flask.construct_resource_map( + resource=RoleImplicationListResource, + url='/roles//implies', + resource_kwargs={}, + rel='implied_roles', + path_vars={'prior_role_id': json_home.Parameters.ROLE_ID}), + ks_flask.construct_resource_map( + resource=RoleImplicationResource, + resource_kwargs={}, + url=('/roles//' + 'implies/'), + rel='implied_role', + path_vars={ + 'prior_role_id': json_home.Parameters.ROLE_ID, + 'implied_role_id': json_home.Parameters.ROLE_ID}) + ] + + +APIs = (RoleAPI,) diff --git a/keystone/assignment/controllers.py b/keystone/assignment/controllers.py index dd717f4546..5b13e28f27 100644 --- a/keystone/assignment/controllers.py +++ b/keystone/assignment/controllers.py @@ -19,11 +19,8 @@ import functools from oslo_log import log -from keystone.assignment import schema from keystone.common import controller from keystone.common import provider_api -from keystone.common import validation -from keystone.common import wsgi import keystone.conf from keystone import exception from keystone.i18n import _ @@ -53,175 +50,6 @@ class ProjectAssignmentV3(controller.V3Controller): hints=hints) -class RoleV3(controller.V3Controller): - """The V3 Role CRUD APIs. - - To ease complexity (and hence risk) in writing the policy rules for the - role APIs, we create separate policy actions for roles that are domain - specific, as opposed to those that are global. In order to achieve this - each of the role API methods has a wrapper method that checks to see if the - role is global or domain specific. - - NOTE (henry-nash): If this separate global vs scoped policy action pattern - becomes repeated for other entities, we should consider encapsulating this - into a specialized router class. - - """ - - collection_name = 'roles' - member_name = 'role' - - def __init__(self): - super(RoleV3, self).__init__() - self.get_member_from_driver = PROVIDERS.role_api.get_role - - def _is_domain_role(self, role): - return role.get('domain_id') is not None - - def _is_domain_role_target(self, role_id): - try: - role = PROVIDERS.role_api.get_role(role_id) - except exception.RoleNotFound: - # We hide this error since we have not yet carried out a policy - # check - and it maybe that the caller isn't authorized to make - # this call. If so, we want that error to be raised instead. - return None, False - return role, self._is_domain_role(role) - - def create_role_wrapper(self, request, role): - if self._is_domain_role(role): - return self.create_domain_role(request, role=role) - else: - return self.create_role(request, role=role) - - @controller.protected() - def create_role(self, request, role): - validation.lazy_validate(schema.role_create, role) - return self._create_role(request, role) - - @controller.protected() - def create_domain_role(self, request, role): - validation.lazy_validate(schema.role_create, role) - return self._create_role(request, role) - - def list_roles_wrapper(self, request): - if request.params.get('domain_id'): - return self.list_domain_roles(request) - else: - return self.list_roles(request) - - @controller.filterprotected('name', 'domain_id') - def list_roles(self, request, filters): - return self._list_roles(request, filters) - - @controller.filterprotected('name', 'domain_id') - def list_domain_roles(self, request, filters): - return self._list_roles(request, filters) - - def get_role_wrapper(self, request, role_id): - role, is_domain_role = self._is_domain_role_target(role_id) - if is_domain_role: - return self.get_domain_role(request, role_id=role_id, role=role) - else: - return self.get_role(request, role_id=role_id, role=role) - - @controller.protected() - def get_role(self, request, role_id, role): - if not role: - raise exception.RoleNotFound(role_id=role_id) - return RoleV3.wrap_member(request.context_dict, role) - - @controller.protected() - def get_domain_role(self, request, role_id, role): - if not role: - raise exception.RoleNotFound(role_id=role_id) - return RoleV3.wrap_member(request.context_dict, role) - - def update_role_wrapper(self, request, role_id, role): - # Since we don't allow you change whether a role is global or domain - # specific, we can ignore the new update attributes and just look at - # the existing role. - _, is_domain_role = self._is_domain_role_target(role_id) - if is_domain_role: - return self.update_domain_role( - request, role_id=role_id, role=role) - else: - return self.update_role(request, role_id=role_id, role=role) - - @controller.protected() - def update_role(self, request, role_id, role): - validation.lazy_validate(schema.role_update, role) - return self._update_role(request, role_id, role) - - @controller.protected() - def update_domain_role(self, request, role_id, role): - validation.lazy_validate(schema.role_update, role) - return self._update_role(request, role_id, role) - - def delete_role_wrapper(self, request, role_id): - _, is_domain_role = self._is_domain_role_target(role_id) - if is_domain_role: - return self.delete_domain_role(request, role_id=role_id) - else: - return self.delete_role(request, role_id=role_id) - - @controller.protected() - def delete_role(self, request, role_id): - return self._delete_role(request, role_id) - - @controller.protected() - def delete_domain_role(self, request, role_id): - return self._delete_role(request, role_id) - - def _create_role(self, request, role): - if role['name'] == CONF.member_role_name: - # Use the configured member role ID when creating the configured - # member role name. This avoids the potential of creating a - # "member" role with an unexpected ID. - role['id'] = CONF.member_role_id - else: - role = self._assign_unique_id(role) - - ref = self._normalize_dict(role) - ref = PROVIDERS.role_api.create_role( - ref['id'], - ref, - initiator=request.audit_initiator) - return RoleV3.wrap_member(request.context_dict, ref) - - def _list_roles(self, request, filters): - hints = RoleV3.build_driver_hints(request, filters) - refs = PROVIDERS.role_api.list_roles(hints=hints) - return RoleV3.wrap_collection(request.context_dict, refs, hints=hints) - - def _update_role(self, request, role_id, role): - self._require_matching_id(role_id, role) - ref = PROVIDERS.role_api.update_role( - role_id, role, initiator=request.audit_initiator - ) - return RoleV3.wrap_member(request.context_dict, ref) - - def _delete_role(self, request, role_id): - PROVIDERS.role_api.delete_role(role_id, - initiator=request.audit_initiator) - - @classmethod - def build_driver_hints(cls, request, supported_filters): - # NOTE(jamielennox): To handle the default case of no domain_id defined - # the role_assignment backend does some hackery to distinguish between - # global and domain scoped roles. This backend behaviour relies upon a - # value of domain_id being set (not just defaulting to None). Manually - # set the empty filter if its not provided. - - hints = super(RoleV3, cls).build_driver_hints(request, - supported_filters) - - if not request.params.get('domain_id'): - hints.add_filter('domain_id', None) - - return hints - - class ImpliedRolesV3(controller.V3Controller): """The V3 ImpliedRoles CRD APIs. There is no Update.""" @@ -254,83 +82,6 @@ class ImpliedRolesV3(controller.V3Controller): } return implied_response - def _populate_prior_role_response(self, endpoint, prior_id): - prior_role = PROVIDERS.role_api.get_role(prior_id) - response = { - "role_inference": { - "prior_role": self._prior_role_stanza( - endpoint, prior_id, prior_role['name']) - } - } - return response - - def _populate_implied_roles_response(self, endpoint, - prior_id, implied_ids): - response = self._populate_prior_role_response(endpoint, prior_id) - response["role_inference"]['implies'] = [] - for implied_id in implied_ids: - implied_role = PROVIDERS.role_api.get_role(implied_id) - implied_response = self._implied_role_stanza( - endpoint, implied_role) - response["role_inference"]['implies'].append(implied_response) - response["links"] = { - "self": endpoint + "/v3/roles/" + prior_id + "/implies" - } - return response - - def _populate_implied_role_response(self, endpoint, prior_id, implied_id): - response = self._populate_prior_role_response(endpoint, prior_id) - implied_role = PROVIDERS.role_api.get_role(implied_id) - stanza = self._implied_role_stanza(endpoint, implied_role) - response["role_inference"]['implies'] = stanza - response["links"] = { - "self": (endpoint + "/v3/roles/" + prior_id - + "/implies/" + implied_id) - } - return response - - @controller.protected(callback=_check_implies_role) - def get_implied_role(self, request, prior_role_id, implied_role_id): - ref = PROVIDERS.role_api.get_implied_role(prior_role_id, - implied_role_id) - - prior_id = ref['prior_role_id'] - implied_id = ref['implied_role_id'] - endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( - request.context_dict, 'public') - response = self._populate_implied_role_response( - endpoint, prior_id, implied_id) - return response - - @controller.protected(callback=_check_implies_role) - def check_implied_role(self, request, prior_role_id, implied_role_id): - PROVIDERS.role_api.get_implied_role(prior_role_id, implied_role_id) - - @controller.protected(callback=_check_implies_role) - def create_implied_role(self, request, prior_role_id, implied_role_id): - PROVIDERS.role_api.create_implied_role(prior_role_id, implied_role_id) - return wsgi.render_response( - self.get_implied_role(request, - prior_role_id, - implied_role_id), - status=(201, 'Created')) - - @controller.protected(callback=_check_implies_role) - def delete_implied_role(self, request, prior_role_id, implied_role_id): - PROVIDERS.role_api.delete_implied_role(prior_role_id, implied_role_id) - - @controller.protected(callback=_check_implies_role) - def list_implied_roles(self, request, prior_role_id): - ref = PROVIDERS.role_api.list_implied_roles(prior_role_id) - implied_ids = [r['implied_role_id'] for r in ref] - endpoint = super(controller.V3Controller, ImpliedRolesV3).base_url( - request.context_dict, 'public') - - results = self._populate_implied_roles_response( - endpoint, prior_role_id, implied_ids) - - return results - @controller.protected() def list_role_inference_rules(self, request): refs = PROVIDERS.role_api.list_role_inference_rules() diff --git a/keystone/assignment/routers.py b/keystone/assignment/routers.py index 7c1e0b917a..36f49df32f 100644 --- a/keystone/assignment/routers.py +++ b/keystone/assignment/routers.py @@ -19,7 +19,6 @@ import functools from keystone.assignment import controllers from keystone.common import json_home -from keystone.common import router from keystone.common import wsgi @@ -39,7 +38,7 @@ class Public(wsgi.ComposableRouter): class Routers(wsgi.RoutersBase): - _path_prefixes = ('users', 'roles', 'role_inferences', 'projects', + _path_prefixes = ('users', 'role_inferences', 'projects', 'domains', 'system', 'role_assignments', 'OS-INHERIT') def append_v3_routers(self, mapper, routers): @@ -54,35 +53,7 @@ class Routers(wsgi.RoutersBase): 'user_id': json_home.Parameters.USER_ID, }) - routers.append( - router.Router(controllers.RoleV3(), 'roles', 'role', - resource_descriptions=self.v3_resources, - method_template='%s_wrapper')) - implied_roles_controller = controllers.ImpliedRolesV3() - self._add_resource( - mapper, implied_roles_controller, - path='/roles/{prior_role_id}/implies', - rel=json_home.build_v3_resource_relation('implied_roles'), - get_head_action='list_implied_roles', - path_vars={ - 'prior_role_id': json_home.Parameters.ROLE_ID, - } - ) - - self._add_resource( - mapper, implied_roles_controller, - path='/roles/{prior_role_id}/implies/{implied_role_id}', - put_action='create_implied_role', - delete_action='delete_implied_role', - head_action='check_implied_role', - get_action='get_implied_role', - rel=json_home.build_v3_resource_relation('implied_role'), - path_vars={ - 'prior_role_id': json_home.Parameters.ROLE_ID, - 'implied_role_id': json_home.Parameters.ROLE_ID - } - ) self._add_resource( mapper, implied_roles_controller, path='/role_inferences', diff --git a/keystone/server/flask/application.py b/keystone/server/flask/application.py index 4707b720a4..39a80722b4 100644 --- a/keystone/server/flask/application.py +++ b/keystone/server/flask/application.py @@ -50,6 +50,7 @@ _MOVED_API_PREFIXES = frozenset( 'limits', 'regions', 'registered_limits', + 'roles', 'services', ] )