
These fields are used for queries, and may need to be indexed Also moves the delete token for... functions into the base class for controllers. Removed the token API revoke token call as that needed access to other APIs. Logic was moved into the controller. Bug 1152801 Change-Id: I59c360fe5aef905dfa30cb55ee54ff1fbe64dc58
345 lines
12 KiB
Python
345 lines
12 KiB
Python
import collections
|
|
import functools
|
|
import uuid
|
|
|
|
from keystone.common import dependency
|
|
from keystone.common import logging
|
|
from keystone.common import wsgi
|
|
from keystone import config
|
|
from keystone import exception
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = config.CONF
|
|
DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id
|
|
|
|
|
|
def _build_policy_check_credentials(self, action, context, kwargs):
|
|
|
|
LOG.debug(_('RBAC: Authorizing %s(%s)') % (
|
|
action,
|
|
', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])))
|
|
|
|
try:
|
|
token_ref = self.token_api.get_token(
|
|
context=context, token_id=context['token_id'])
|
|
except exception.TokenNotFound:
|
|
LOG.warning(_('RBAC: Invalid token'))
|
|
raise exception.Unauthorized()
|
|
|
|
creds = {}
|
|
if 'token_data' in token_ref:
|
|
#V3 Tokens
|
|
token_data = token_ref['token_data']['token']
|
|
try:
|
|
creds['user_id'] = token_data['user']['id']
|
|
except AttributeError:
|
|
LOG.warning(_('RBAC: Invalid user'))
|
|
raise exception.Unauthorized()
|
|
|
|
if 'project' in token_data:
|
|
creds['project_id'] = token_data['project']['id']
|
|
else:
|
|
LOG.debug(_('RBAC: Proceeding without project'))
|
|
|
|
if 'domain' in token_data:
|
|
creds['domain_id'] = token_data['domain']['id']
|
|
|
|
if 'roles' in token_data:
|
|
creds['roles'] = []
|
|
for role in token_data['roles']:
|
|
creds['roles'].append(role['name'])
|
|
else:
|
|
#v2 Tokens
|
|
creds = token_ref.get('metadata', {}).copy()
|
|
try:
|
|
creds['user_id'] = token_ref['user'].get('id')
|
|
except AttributeError:
|
|
LOG.warning(_('RBAC: Invalid user'))
|
|
raise exception.Unauthorized()
|
|
try:
|
|
creds['project_id'] = token_ref['tenant'].get('id')
|
|
except AttributeError:
|
|
LOG.debug(_('RBAC: Proceeding without tenant'))
|
|
# NOTE(vish): this is pretty inefficient
|
|
creds['roles'] = [self.identity_api.get_role(context, role)['name']
|
|
for role in creds.get('roles', [])]
|
|
|
|
return creds
|
|
|
|
|
|
def flatten(d, parent_key=''):
|
|
"""Flatten a nested dictionary
|
|
|
|
Converts a dictionary with nested values to a single level flat
|
|
dictionary, with dotted notation for each key.
|
|
|
|
"""
|
|
items = []
|
|
for k, v in d.items():
|
|
new_key = parent_key + '.' + k if parent_key else k
|
|
if isinstance(v, collections.MutableMapping):
|
|
items.extend(flatten(v, new_key).items())
|
|
else:
|
|
items.append((new_key, v))
|
|
return dict(items)
|
|
|
|
|
|
def protected(f):
|
|
"""Wraps API calls with role based access controls (RBAC)."""
|
|
@functools.wraps(f)
|
|
def wrapper(self, context, **kwargs):
|
|
if 'is_admin' in context and context['is_admin']:
|
|
LOG.warning(_('RBAC: Bypassing authorization'))
|
|
else:
|
|
action = 'identity:%s' % f.__name__
|
|
creds = _build_policy_check_credentials(self, action,
|
|
context, kwargs)
|
|
# Simply use the passed kwargs as the target dict, which
|
|
# would typically include the prime key of a get/update/delete
|
|
# call.
|
|
self.policy_api.enforce(context, creds, action, flatten(kwargs))
|
|
LOG.debug(_('RBAC: Authorization granted'))
|
|
|
|
return f(self, context, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
def filterprotected(*filters):
|
|
"""Wraps filtered API calls with role based access controls (RBAC)."""
|
|
|
|
def _filterprotected(f):
|
|
@functools.wraps(f)
|
|
def wrapper(self, context, **kwargs):
|
|
if not context['is_admin']:
|
|
action = 'identity:%s' % f.__name__
|
|
creds = _build_policy_check_credentials(self, action,
|
|
context, kwargs)
|
|
# Now, build the target dict for policy check. We include:
|
|
#
|
|
# - Any query filter parameters
|
|
# - Data from the main url (which will be in the kwargs
|
|
# parameter) and would typically include the prime key
|
|
# of a get/update/delete call
|
|
#
|
|
# First any query filter parameters
|
|
target = dict()
|
|
if len(filters) > 0:
|
|
for filter in filters:
|
|
if filter in context['query_string']:
|
|
target[filter] = context['query_string'][filter]
|
|
|
|
LOG.debug(_('RBAC: Adding query filter params (%s)') % (
|
|
', '.join(['%s=%s' % (filter, target[filter])
|
|
for filter in target])))
|
|
|
|
# Now any formal url parameters
|
|
for key in kwargs:
|
|
target[key] = kwargs[key]
|
|
|
|
self.policy_api.enforce(context, creds, action,
|
|
flatten(target))
|
|
|
|
LOG.debug(_('RBAC: Authorization granted'))
|
|
else:
|
|
LOG.warning(_('RBAC: Bypassing authorization'))
|
|
return f(self, context, filters, **kwargs)
|
|
return wrapper
|
|
return _filterprotected
|
|
|
|
|
|
@dependency.requires('identity_api', 'policy_api', 'token_api',
|
|
'trust_api', 'catalog_api')
|
|
class V2Controller(wsgi.Application):
|
|
"""Base controller class for Identity API v2."""
|
|
|
|
def _delete_tokens_for_trust(self, context, user_id, trust_id):
|
|
try:
|
|
token_list = self.token_api.list_tokens(context, user_id,
|
|
trust_id=trust_id)
|
|
for token in token_list:
|
|
self.token_api.delete_token(context, token)
|
|
except exception.NotFound:
|
|
pass
|
|
|
|
def _delete_tokens_for_user(self, context, user_id, project_id=None):
|
|
#First delete tokens that could get other tokens.
|
|
for token_id in self.token_api.list_tokens(context,
|
|
user_id,
|
|
tenant_id=project_id):
|
|
try:
|
|
self.token_api.delete_token(context, token_id)
|
|
except exception.NotFound:
|
|
pass
|
|
#delete tokens generated from trusts
|
|
for trust in self.trust_api.list_trusts_for_trustee(context, user_id):
|
|
self._delete_tokens_for_trust(context, user_id, trust['id'])
|
|
for trust in self.trust_api.list_trusts_for_trustor(context, user_id):
|
|
self._delete_tokens_for_trust(context,
|
|
trust['trustee_user_id'],
|
|
trust['id'])
|
|
|
|
def _require_attribute(self, ref, attr):
|
|
"""Ensures the reference contains the specified attribute."""
|
|
if ref.get(attr) is None or ref.get(attr) == '':
|
|
msg = '%s field is required and cannot be empty' % attr
|
|
raise exception.ValidationError(message=msg)
|
|
|
|
def _normalize_domain_id(self, context, ref):
|
|
"""Fill in domain_id since v2 calls are not domain-aware.
|
|
|
|
This will overwrite any domain_id that was inadvertently
|
|
specified in the v2 call.
|
|
|
|
"""
|
|
ref['domain_id'] = DEFAULT_DOMAIN_ID
|
|
return ref
|
|
|
|
def _filter_domain_id(self, ref):
|
|
"""Remove domain_id since v2 calls are not domain-aware."""
|
|
ref.pop('domain_id', None)
|
|
return ref
|
|
|
|
|
|
class V3Controller(V2Controller):
|
|
"""Base controller class for Identity API v3.
|
|
|
|
Child classes should set the ``collection_name`` and ``member_name`` class
|
|
attributes, representing the collection of entities they are exposing to
|
|
the API. This is required for supporting self-referential links,
|
|
pagination, etc.
|
|
|
|
"""
|
|
|
|
collection_name = 'entities'
|
|
member_name = 'entity'
|
|
|
|
def _delete_tokens_for_group(self, context, group_id):
|
|
user_refs = self.identity_api.list_users_in_group(context, group_id)
|
|
for user in user_refs:
|
|
self._delete_tokens_for_user(context, user['id'])
|
|
|
|
@classmethod
|
|
def base_url(cls, path=None):
|
|
endpoint = CONF.public_endpoint % CONF
|
|
|
|
# allow a missing trailing slash in the config
|
|
if endpoint[-1] != '/':
|
|
endpoint += '/'
|
|
|
|
url = endpoint + 'v3'
|
|
|
|
if path:
|
|
return url + path
|
|
else:
|
|
return url + '/' + cls.collection_name
|
|
|
|
@classmethod
|
|
def _add_self_referential_link(cls, ref):
|
|
ref.setdefault('links', {})
|
|
ref['links']['self'] = cls.base_url() + '/' + ref['id']
|
|
|
|
@classmethod
|
|
def wrap_member(cls, context, ref):
|
|
cls._add_self_referential_link(ref)
|
|
return {cls.member_name: ref}
|
|
|
|
@classmethod
|
|
def wrap_collection(cls, context, refs, filters=[]):
|
|
for f in filters:
|
|
refs = cls.filter_by_attribute(context, refs, f)
|
|
|
|
refs = cls.paginate(context, refs)
|
|
|
|
for ref in refs:
|
|
cls.wrap_member(context, ref)
|
|
|
|
container = {cls.collection_name: refs}
|
|
container['links'] = {
|
|
'next': None,
|
|
'self': cls.base_url(path=context['path']),
|
|
'previous': None}
|
|
return container
|
|
|
|
@classmethod
|
|
def paginate(cls, context, refs):
|
|
"""Paginates a list of references by page & per_page query strings."""
|
|
# FIXME(dolph): client needs to support pagination first
|
|
return refs
|
|
|
|
page = context['query_string'].get('page', 1)
|
|
per_page = context['query_string'].get('per_page', 30)
|
|
return refs[per_page * (page - 1):per_page * page]
|
|
|
|
@classmethod
|
|
def filter_by_attribute(cls, context, refs, attr):
|
|
"""Filters a list of references by query string value."""
|
|
|
|
def _attr_match(ref_attr, val_attr):
|
|
"""Matches attributes allowing for booleans as strings.
|
|
|
|
We test explicitly for a value that defines it as 'False',
|
|
which also means that the existence of the attribute with
|
|
no value implies 'True'
|
|
|
|
"""
|
|
if type(ref_attr) is bool:
|
|
if (isinstance(val_attr, basestring) and
|
|
val_attr == '0'):
|
|
val = False
|
|
else:
|
|
val = True
|
|
return (ref_attr == val)
|
|
else:
|
|
return (ref_attr == val_attr)
|
|
|
|
if attr in context['query_string']:
|
|
value = context['query_string'][attr]
|
|
return [r for r in refs if _attr_match(r[attr], value)]
|
|
return refs
|
|
|
|
def _require_matching_id(self, value, ref):
|
|
"""Ensures the value matches the reference's ID, if any."""
|
|
if 'id' in ref and ref['id'] != value:
|
|
raise exception.ValidationError('Cannot change ID')
|
|
|
|
def _assign_unique_id(self, ref):
|
|
"""Generates and assigns a unique identifer to a reference."""
|
|
ref = ref.copy()
|
|
ref['id'] = uuid.uuid4().hex
|
|
return ref
|
|
|
|
def _normalize_domain_id(self, context, ref):
|
|
"""Fill in domain_id if not specified in a v3 call."""
|
|
|
|
if 'domain_id' not in ref:
|
|
if context['is_admin']:
|
|
ref['domain_id'] = DEFAULT_DOMAIN_ID
|
|
else:
|
|
# Fish the domain_id out of the token
|
|
#
|
|
# We could make this more efficient by loading the domain_id
|
|
# into the context in the wrapper function above (since
|
|
# this version of normalize_domain will only be called inside
|
|
# a v3 protected call). However, given that we only use this
|
|
# for creating entities, this optimization is probably not
|
|
# worth the duplication of state
|
|
try:
|
|
token_ref = self.token_api.get_token(
|
|
context=context, token_id=context['token_id'])
|
|
except exception.TokenNotFound:
|
|
LOG.warning(_('Invalid token in normalize_domain_id'))
|
|
raise exception.Unauthorized()
|
|
|
|
if 'domain' in token_ref:
|
|
ref['domain_id'] = token_ref['domain']['id']
|
|
else:
|
|
# FIXME(henry-nash) Revisit this once v3 token scoping
|
|
# across domains has been hashed out
|
|
ref['domain_id'] = DEFAULT_DOMAIN_ID
|
|
return ref
|
|
|
|
def _filter_domain_id(self, ref):
|
|
"""Override v2 filter to let domain_id out for v3 calls."""
|
|
return ref
|