Merge "Convert groups API to flask native dispatching"
This commit is contained in:
commit
77e050d509
|
@ -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,
|
||||
|
|
|
@ -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,)
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Reference in New Issue