keystone/keystone/common/controller.py

332 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 %(action)s(%(kwargs)s)') % {
'action': action,
'kwargs': ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])})
try:
token_ref = self.token_api.get_token(context['token_id'])
except exception.TokenNotFound:
LOG.warning(_('RBAC: Invalid token'))
raise exception.Unauthorized()
creds = {}
if 'token_data' in token_ref and 'token' in token_ref['token_data']:
#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(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, *args, **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(creds, action, flatten(kwargs))
LOG.debug(_('RBAC: Authorization granted'))
return f(self, context, *args, **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(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', 'credential_api',
'assignment_api')
class V2Controller(wsgi.Application):
"""Base controller class for Identity API v2."""
def _delete_tokens_for_trust(self, user_id, trust_id):
self.token_api.delete_tokens(user_id, trust_id=trust_id)
def _delete_tokens_for_user(self, user_id, project_id=None):
#First delete tokens that could get other tokens.
self.token_api.delete_tokens(user_id, tenant_id=project_id)
#delete tokens generated from trusts
for trust in self.trust_api.list_trusts_for_trustee(user_id):
self._delete_tokens_for_trust(user_id, trust['id'])
for trust in self.trust_api.list_trusts_for_trustor(user_id):
self._delete_tokens_for_trust(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, group_id):
user_refs = self.identity_api.list_users_in_group(group_id)
for user in user_refs:
self._delete_tokens_for_user(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(
flatten(r).get(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(
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