Merge "Convert groups API to flask native dispatching"

This commit is contained in:
Zuul 2018-09-14 20:15:56 +00:00 committed by Gerrit Code Review
commit 77e050d509
6 changed files with 259 additions and 98 deletions

View File

@ -13,6 +13,7 @@
from keystone.api import credentials
from keystone.api import discovery
from keystone.api import endpoints
from keystone.api import groups
from keystone.api import limits
from keystone.api import os_ep_filter
from keystone.api import os_federation
@ -33,6 +34,7 @@ __all__ = (
'discovery',
'credentials',
'endpoints',
'groups',
'limits',
'os_ep_filter',
'os_federation',
@ -54,6 +56,7 @@ __apis__ = (
discovery,
credentials,
endpoints,
groups,
limits,
os_ep_filter,
os_federation,

201
keystone/api/groups.py Normal file
View File

@ -0,0 +1,201 @@
# 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/groups
import flask_restful
from six.moves import http_client
from keystone.common import json_home
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import validation
from keystone import exception
from keystone.identity import schema
from keystone.server import flask as ks_flask
ENFORCER = rbac_enforcer.RBACEnforcer
PROVIDERS = provider_api.ProviderAPIs
class GroupsResource(ks_flask.ResourceBase):
collection_key = 'groups'
member_key = 'group'
get_member_from_driver = PROVIDERS.deferred_provider_lookup(
api='identity_api', method='get_group')
def get(self, group_id=None):
if group_id is not None:
return self._get_group(group_id)
return self._list_groups()
def _get_group(self, group_id):
"""Get a group reference.
GET/HEAD /groups/{group_id}
"""
ENFORCER.enforce_call(action='identity:get_group')
return self.wrap_member(PROVIDERS.identity_api.get_group(group_id))
def _list_groups(self):
"""List groups.
GET/HEAD /groups
"""
filters = ['domain_id', 'name']
ENFORCER.enforce_call(action='identity:list_groups', filters=filters)
hints = self.build_driver_hints(filters)
domain = self._get_domain_id_for_list_request()
refs = PROVIDERS.identity_api.list_groups(domain_scope=domain,
hints=hints)
return self.wrap_collection(refs, hints=hints)
def post(self):
"""Create group.
POST /groups
"""
ENFORCER.enforce_call(action='identity:create_group')
group = self.request_body_json.get('group', {})
validation.lazy_validate(schema.group_create, group)
group = self._normalize_dict(group)
group = self._normalize_domain_id(group)
ref = PROVIDERS.identity_api.create_group(
group, initiator=self.audit_initiator)
return self.wrap_member(ref), http_client.CREATED
def patch(self, group_id):
"""Update group.
PATCH /groups/{group_id}
"""
ENFORCER.enforce_call(action='identity:update_group')
group = self.request_body_json.get('group', {})
validation.lazy_validate(schema.group_update, group)
self._require_matching_id(group)
ref = PROVIDERS.identity_api.update_group(
group_id, group, initiator=self.audit_initiator)
return self.wrap_member(ref)
def delete(self, group_id):
"""Delete group.
DELETE /groups/{group_id}
"""
ENFORCER.enforce_call(action='identity:delete_group')
PROVIDERS.identity_api.delete_group(
group_id, initiator=self.audit_initiator)
return None, http_client.NO_CONTENT
class GroupUsersResource(flask_restful.Resource):
def get(self, group_id):
"""Get list of users in group.
GET/HEAD /groups/{group_id}/users
"""
filters = ['domain_id', 'enabled', 'name', 'password_expires_at']
target = {}
try:
target['group'] = PROVIDERS.identity_api.get_group(group_id)
except exception.GroupNotFound:
# NOTE(morgan): If we have an issue populating the group
# data, leage target empty. This is the safest route and does not
# leak data before enforcement happens.
pass
ENFORCER.enforce_call(action='identity:list_users_in_group',
target_attr=target, filters=filters)
hints = ks_flask.ResourceBase.build_driver_hints(filters)
refs = PROVIDERS.identity_api.list_users_in_group(
group_id, hints=hints)
return ks_flask.ResourceBase.wrap_collection(
refs, collection_name='users')
class UserGroupCRUDResource(flask_restful.Resource):
@staticmethod
def _build_enforcement_target_attr(user_id, group_id):
target = {}
try:
target['group'] = PROVIDERS.identity_api.get_group(group_id)
except exception.GroupNotFound:
# Don't populate group data if group is not found.
pass
try:
target['user'] = PROVIDERS.identity_api.get_user(user_id)
except exception.UserNotFound:
# Don't populate user data if user is not found
pass
return target
def get(self, group_id, user_id):
"""Check if a user is in a group.
GET/HEAD /groups/{group_id}/users/{user_id}
"""
ENFORCER.enforce_call(
action='identity:check_user_in_group',
target_attr=self._build_enforcement_target_attr(user_id, group_id))
PROVIDERS.identity_api.check_user_in_group(user_id, group_id)
return None, http_client.NO_CONTENT
def put(self, group_id, user_id):
"""Add user to group.
PUT /groups/{group_id}/users/{user_id}
"""
ENFORCER.enforce_call(
action='identity:add_user_to_group',
target_attr=self._build_enforcement_target_attr(user_id, group_id))
PROVIDERS.identity_api.add_user_to_group(
user_id, group_id, initiator=ks_flask.build_audit_initiator())
return None, http_client.NO_CONTENT
def delete(self, group_id, user_id):
"""Remove user from group.
DELETE /groups/{group_id}/users/{user_id}
"""
ENFORCER.enforce_call(
action='identity:remove_user_from_group',
target_attr=self._build_enforcement_target_attr(user_id, group_id))
PROVIDERS.identity_api.remove_user_from_group(
user_id, group_id, initiator=ks_flask.build_audit_initiator())
return None, http_client.NO_CONTENT
class GroupAPI(ks_flask.APIBase):
_name = 'groups'
_import_name = __name__
resources = [GroupsResource]
resource_mapping = [
ks_flask.construct_resource_map(
resource=GroupUsersResource,
url='/groups/<string:group_id>/users',
resource_kwargs={},
rel='group_users',
path_vars={'group_id': json_home.Parameters.GROUP_ID}),
ks_flask.construct_resource_map(
resource=UserGroupCRUDResource,
url='/groups/<string:group_id>/users/<string:user_id>',
resource_kwargs={},
rel='group_user',
path_vars={
'group_id': json_home.Parameters.GROUP_ID,
'user_id': json_home.Parameters.USER_ID})
]
APIs = (GroupAPI,)

View File

@ -45,11 +45,6 @@ class UserV3(controller.V3Controller):
ref['group'] = PROVIDERS.identity_api.get_group(group_id)
self.check_protection(request, prep_info, ref)
def _check_group_protection(self, request, prep_info, group_id):
ref = {}
ref['group'] = PROVIDERS.identity_api.get_group(group_id)
self.check_protection(request, prep_info, ref)
@controller.protected()
def create_user(self, request, user):
validation.lazy_validate(schema.user_create, user)
@ -72,16 +67,6 @@ class UserV3(controller.V3Controller):
)
return UserV3.wrap_collection(request.context_dict, refs, hints=hints)
@controller.filterprotected('domain_id', 'enabled', 'name',
'password_expires_at',
callback=_check_group_protection)
def list_users_in_group(self, request, filters, group_id):
hints = UserV3.build_driver_hints(request, filters)
refs = PROVIDERS.identity_api.list_users_in_group(
group_id, hints=hints
)
return UserV3.wrap_collection(request.context_dict, refs, hints=hints)
@controller.protected()
def get_user(self, request, user_id):
ref = PROVIDERS.identity_api.get_user(user_id)
@ -99,22 +84,6 @@ class UserV3(controller.V3Controller):
validation.lazy_validate(schema.user_update, user)
return self._update_user(request, user_id, user)
@controller.protected(callback=_check_user_and_group_protection)
def add_user_to_group(self, request, user_id, group_id):
PROVIDERS.identity_api.add_user_to_group(
user_id, group_id, initiator=request.audit_initiator
)
@controller.protected(callback=_check_user_and_group_protection)
def check_user_in_group(self, request, user_id, group_id):
return PROVIDERS.identity_api.check_user_in_group(user_id, group_id)
@controller.protected(callback=_check_user_and_group_protection)
def remove_user_from_group(self, request, user_id, group_id):
PROVIDERS.identity_api.remove_user_from_group(
user_id, group_id, initiator=request.audit_initiator
)
@controller.protected()
def delete_user(self, request, user_id):
return PROVIDERS.identity_api.delete_user(
@ -156,26 +125,6 @@ class GroupV3(controller.V3Controller):
ref['user'] = PROVIDERS.identity_api.get_user(user_id)
self.check_protection(request, prep_info, ref)
@controller.protected()
def create_group(self, request, group):
validation.lazy_validate(schema.group_create, group)
# The manager layer will generate the unique ID for groups
ref = self._normalize_dict(group)
ref = self._normalize_domain_id(request, ref)
ref = PROVIDERS.identity_api.create_group(
ref, initiator=request.audit_initiator
)
return GroupV3.wrap_member(request.context_dict, ref)
@controller.filterprotected('domain_id', 'name')
def list_groups(self, request, filters):
hints = GroupV3.build_driver_hints(request, filters)
domain = self._get_domain_id_for_list_request(request)
refs = PROVIDERS.identity_api.list_groups(
domain_scope=domain, hints=hints
)
return GroupV3.wrap_collection(request.context_dict, refs, hints=hints)
@controller.filterprotected('name', callback=_check_user_protection)
def list_groups_for_user(self, request, filters, user_id):
hints = GroupV3.build_driver_hints(request, filters)
@ -183,23 +132,3 @@ class GroupV3(controller.V3Controller):
user_id, hints=hints
)
return GroupV3.wrap_collection(request.context_dict, refs, hints=hints)
@controller.protected()
def get_group(self, request, group_id):
ref = PROVIDERS.identity_api.get_group(group_id)
return GroupV3.wrap_member(request.context_dict, ref)
@controller.protected()
def update_group(self, request, group_id, group):
validation.lazy_validate(schema.group_update, group)
self._require_matching_id(group_id, group)
ref = PROVIDERS.identity_api.update_group(
group_id, group, initiator=request.audit_initiator
)
return GroupV3.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_group(self, request, group_id):
PROVIDERS.identity_api.delete_group(
group_id, initiator=request.audit_initiator
)

View File

@ -21,7 +21,7 @@ from keystone.identity import controllers
class Routers(wsgi.RoutersBase):
_path_prefixes = ('users', 'groups')
_path_prefixes = ('users', )
def append_v3_routers(self, mapper, routers):
user_controller = controllers.UserV3()
@ -39,32 +39,7 @@ class Routers(wsgi.RoutersBase):
'user_id': json_home.Parameters.USER_ID,
})
self._add_resource(
mapper, user_controller,
path='/groups/{group_id}/users',
get_head_action='list_users_in_group',
rel=json_home.build_v3_resource_relation('group_users'),
path_vars={
'group_id': json_home.Parameters.GROUP_ID,
})
self._add_resource(
mapper, user_controller,
path='/groups/{group_id}/users/{user_id}',
put_action='add_user_to_group',
get_head_action='check_user_in_group',
delete_action='remove_user_from_group',
rel=json_home.build_v3_resource_relation('group_user'),
path_vars={
'group_id': json_home.Parameters.GROUP_ID,
'user_id': json_home.Parameters.USER_ID,
})
group_controller = controllers.GroupV3()
routers.append(
router.Router(group_controller,
'groups', 'group',
resource_descriptions=self.v3_resources))
self._add_resource(
mapper, group_controller,

View File

@ -40,6 +40,7 @@ from keystone.resource import routers as resource_routers
_MOVED_API_PREFIXES = frozenset(
['credentials',
'endpoints',
'groups',
'OS-OAUTH1',
'OS-EP-FILTER',
'OS-FEDERATION',

View File

@ -804,7 +804,7 @@ class ResourceBase(flask_restful.Resource):
if not flask.request.args:
return hints
for key, value in flask.request.args.items():
for key, value in flask.request.args.items(multi=True):
# Check if this is an exact filter
if supported_filters is None or key in supported_filters:
hints.add_filter(key, value)
@ -886,6 +886,58 @@ class ResourceBase(flask_restful.Resource):
def _normalize_arg(arg):
return arg.replace(':', '_').replace('-', '_')
@classmethod
def _get_domain_id_for_list_request(cls):
"""Get the domain_id for a v3 list call.
If we running with multiple domain drivers, then the caller must
specify a domain_id either as a filter or as part of the token scope.
"""
if not CONF.identity.domain_specific_drivers_enabled:
# We don't need to specify a domain ID in this case
return
domain_id = flask.request.args.get('domain_id')
if domain_id:
return domain_id
token_ref = cls.get_token_ref()
if token_ref.domain_scoped:
return token_ref.domain_id
elif token_ref.project_scoped:
return token_ref.project_domain_id
else:
msg = _('No domain information specified as part of list request')
LOG.warning(msg)
raise exception.Unauthorized(msg)
@classmethod
def get_token_ref(cls):
"""Retrieve KeystoneToken object from the auth context and returns it.
:raises keystone.exception.Unauthorized: If auth context cannot be
found.
:returns: The KeystoneToken object.
"""
try:
# Retrieve the auth context that was prepared by
# AuthContextMiddleware.
auth_context = cls.auth_context
return auth_context['token']
except KeyError:
LOG.warning("Couldn't find the auth context.")
raise exception.Unauthorized()
@classmethod
def _normalize_domain_id(cls, ref):
"""Fill in domain_id if not specified in a v3 call."""
if not ref.get('domain_id'):
ref['domain_id'] = cls._get_domain_id_from_token()
return ref
def base_url(path=''):
url = CONF['public_endpoint']