Merge "build auth context from middleware"

This commit is contained in:
Jenkins 2014-01-26 19:32:52 +00:00 committed by Gerrit Code Review
commit 49bba55d47
8 changed files with 264 additions and 66 deletions

View File

@ -3,6 +3,9 @@
[filter:debug]
paste.filter_factory = keystone.common.wsgi:Debug.factory
[filter:build_auth_context]
paste.filter_factory = keystone.middleware:AuthContextMiddleware.factory
[filter:token_auth]
paste.filter_factory = keystone.middleware:TokenAuthMiddleware.factory
@ -64,13 +67,13 @@ paste.app_factory = keystone.service:v3_app_factory
paste.app_factory = keystone.service:admin_app_factory
[pipeline:public_api]
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body ec2_extension user_crud_extension public_service
pipeline = access_log sizelimit url_normalize build_auth_context token_auth admin_token_auth xml_body json_body ec2_extension user_crud_extension public_service
[pipeline:admin_api]
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body ec2_extension s3_extension crud_extension admin_service
pipeline = access_log sizelimit url_normalize build_auth_context token_auth admin_token_auth xml_body json_body ec2_extension s3_extension crud_extension admin_service
[pipeline:api_v3]
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body ec2_extension s3_extension simple_cert_extension service_v3
pipeline = access_log sizelimit url_normalize build_auth_context token_auth admin_token_auth xml_body json_body ec2_extension s3_extension simple_cert_extension service_v3
[app:public_version_service]
paste.app_factory = keystone.service:public_version_app_factory

View File

@ -0,0 +1,111 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack Foundation
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright 2011 - 2012 Justin Santa Barbara
# All Rights Reserved.
#
# 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.
import collections
from keystone import exception
from keystone.openstack.common import log
AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT'
"""Environment variable used to convey the Keystone auth context.
Auth context is essentially the user credential used for policy enforcement.
It is a dictionary with the following attributes:
user_id: user ID of the principal
project_id (optional): project ID of the scoped project if auth is
project-scoped
domain_id (optional): domain ID of the scoped domain if auth is domain-scoped
roles (optional): list of role names for the given scope
"""
LOG = log.getLogger(__name__)
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 is_v3_token(token):
# V3 token data are encapsulated into "token" key while
# V2 token data are encapsulated into "access" key.
return 'token' in token
def v3_token_to_auth_context(token):
creds = {}
token_data = token['token']
try:
creds['user_id'] = token_data['user']['id']
except AttributeError:
LOG.warning(_('RBAC: Invalid user data in v3 token'))
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'])
return creds
def v2_token_to_auth_context(token):
creds = {}
token_data = token['access']
try:
creds['user_id'] = token_data['user']['id']
except AttributeError:
LOG.warning(_('RBAC: Invalid user data in v2 token'))
raise exception.Unauthorized()
if 'tenant' in token_data['token']:
creds['project_id'] = token_data['token']['tenant']['id']
else:
LOG.debug(_('RBAC: Proceeding without tenant'))
if 'roles' in token_data['user']:
creds['roles'] = [role['name'] for
role in token_data['user']['roles']]
return creds
def token_to_auth_context(token):
if is_v3_token(token):
creds = v3_token_to_auth_context(token)
else:
creds = v2_token_to_auth_context(token)
return creds

View File

@ -14,10 +14,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import functools
import uuid
from keystone.common import authorization
from keystone.common import dependency
from keystone.common import driver_hints
from keystone.common import utils
@ -41,7 +41,16 @@ def _build_policy_check_credentials(self, action, context, kwargs):
'action': action,
'kwargs': ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])})
# see if auth context has already been created. If so use it.
if ('environment' in context and
authorization.AUTH_CONTEXT_ENV in context['environment']):
LOG.debug(_('RBAC: using auth context from the request environment'))
return context['environment'].get(authorization.AUTH_CONTEXT_ENV)
# now build the auth context from the incoming auth token
try:
LOG.debug(_('RBAC: building auth context from the incoming '
'auth token'))
token_ref = self.token_api.get_token(context['token_id'])
except exception.TokenNotFound:
LOG.warning(_('RBAC: Invalid token'))
@ -51,62 +60,9 @@ def _build_policy_check_credentials(self, action, context, kwargs):
# it would otherwise need to reload the token_ref from backing store.
wsgi.validate_token_bind(context, token_ref)
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()
auth_context = authorization.token_to_auth_context(token_ref['token_data'])
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.assignment_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)
return auth_context
def protected(callback=None):
@ -170,7 +126,9 @@ def protected(callback=None):
# Add in the kwargs, which means that any entity provided as a
# parameter for calls like create and update will be included.
policy_dict.update(kwargs)
self.policy_api.enforce(creds, action, flatten(policy_dict))
self.policy_api.enforce(creds,
action,
authorization.flatten(policy_dict))
LOG.debug(_('RBAC: Authorization granted'))
return f(self, context, *args, **kwargs)
return inner
@ -209,7 +167,9 @@ def filterprotected(*filters):
for key in kwargs:
target[key] = kwargs[key]
self.policy_api.enforce(creds, action, flatten(target))
self.policy_api.enforce(creds,
action,
authorization.flatten(target))
LOG.debug(_('RBAC: Authorization granted'))
else:
@ -404,7 +364,7 @@ class V3Controller(wsgi.Application):
attr = filter['name']
value = filter['value']
refs = [r for r in refs if _attr_match(
flatten(r).get(attr), value)]
authorization.flatten(r).get(attr), value)]
else:
# It might be an inexact filter
refs = [r for r in refs if _inexact_attr_match(
@ -540,5 +500,7 @@ class V3Controller(wsgi.Application):
if target_attr:
policy_dict = {'target': target_attr}
policy_dict.update(prep_info['input_attr'])
self.policy_api.enforce(creds, action, flatten(policy_dict))
self.policy_api.enforce(creds,
action,
authorization.flatten(policy_dict))
LOG.debug(_('RBAC: Authorization granted'))

View File

@ -345,6 +345,7 @@ class Middleware(Application):
return _factory
def __init__(self, application):
super(Middleware, self).__init__()
self.application = application
def process_request(self, request):

View File

@ -17,6 +17,7 @@
import six
import webob.dec
from keystone.common import authorization
from keystone.common import config
from keystone.common import serializer
from keystone.common import utils
@ -202,3 +203,47 @@ class RequestBodySizeLimiter(wsgi.Middleware):
CONF.max_request_body_size)
req.body_file = limiter
return self.application
class AuthContextMiddleware(wsgi.Middleware):
"""Build the authentication context from the request auth token."""
def _build_auth_context(self, request):
token_id = request.headers.get(AUTH_TOKEN_HEADER)
if token_id == CONF.admin_token:
# NOTE(gyee): no need to proceed any further as the special admin
# token is being handled by AdminTokenAuthMiddleware. This code
# will not be impacted even if AdminTokenAuthMiddleware is removed
# from the pipeline as "is_admin" is default to "False". This code
# is independent of AdminTokenAuthMiddleware.
return {}
context = {'token_id': token_id}
context['environment'] = request.environ
try:
token_ref = self.token_api.get_token(token_id)
# TODO(gyee): validate_token_bind should really be its own
# middleware
wsgi.validate_token_bind(context, token_ref)
return authorization.token_to_auth_context(
token_ref['token_data'])
except exception.TokenNotFound:
LOG.warning(_('RBAC: Invalid token'))
raise exception.Unauthorized()
def process_request(self, request):
if AUTH_TOKEN_HEADER not in request.headers:
LOG.debug(_('Auth token not in the request header. '
'Will not build auth context.'))
return
if authorization.AUTH_CONTEXT_ENV in request.environ:
msg = _('Auth context already exists in the request environment')
LOG.warning(msg)
return
auth_context = self._build_auth_context(request)
LOG.debug(_('RBAC: auth_context: %s'), auth_context)
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context

View File

@ -18,6 +18,7 @@ import uuid
from keystone import assignment
from keystone import auth
from keystone.common import authorization
from keystone.common import environment
from keystone import config
from keystone import exception
@ -663,12 +664,20 @@ class AuthWithTrust(AuthTest):
fmt=TIME_FORMAT)
self.create_trust(expires_at=expires_at)
def _create_auth_context(self, token_id):
token_ref = self.token_api.get_token(token_id)
auth_context = authorization.token_to_auth_context(
token_ref['token_data'])
return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context},
'token_id': token_id}
def create_trust(self, expires_at=None, impersonation=True):
username = self.trustor['name']
password = 'foo2'
body_dict = _build_user_auth(username=username, password=password)
self.unscoped_token = self.controller.authenticate({}, body_dict)
context = {'token_id': self.unscoped_token['access']['token']['id']}
context = self._create_auth_context(
self.unscoped_token['access']['token']['id'])
trust_data = copy.deepcopy(self.sample_data)
trust_data['expires_at'] = expires_at
trust_data['impersonation'] = impersonation
@ -686,7 +695,8 @@ class AuthWithTrust(AuthTest):
return request_body
def test_create_trust_bad_data_fails(self):
context = {'token_id': self.unscoped_token['access']['token']['id']}
context = self._create_auth_context(
self.unscoped_token['access']['token']['id'])
bad_sample_data = {'trustor_user_id': self.trustor['id']}
self.assertRaises(exception.ValidationError,
@ -856,7 +866,8 @@ class AuthWithTrust(AuthTest):
self.controller.authenticate, {}, request_body)
def test_delete_trust_revokes_token(self):
context = {'token_id': self.unscoped_token['access']['token']['id']}
context = self._create_auth_context(
self.unscoped_token['access']['token']['id'])
self.fetch_v2_token_from_trust()
trust_id = self.new_trust['id']
tokens = self.token_api._list_tokens(self.trustor['id'],

View File

@ -22,9 +22,11 @@ from lxml import etree
import six
from keystone import auth
from keystone.common import authorization
from keystone.common import cache
from keystone.common import serializer
from keystone import config
from keystone import middleware
from keystone.openstack.common import timeutils
from keystone.policy.backends import rules
from keystone import tests
@ -1116,3 +1118,47 @@ class RestfulTestCase(rest.RestfulTestCase):
class VersionTestCase(RestfulTestCase):
def test_get_version(self):
pass
#NOTE(gyee): test AuthContextMiddleware here instead of test_middleware.py
# because we need the token
class AuthContextMiddlewareTestCase(RestfulTestCase):
def _mock_request_object(self, token_id):
class fake_req:
headers = {middleware.AUTH_TOKEN_HEADER: token_id}
environ = {}
return fake_req()
def test_auth_context_build_by_middleware(self):
# test to make sure AuthContextMiddleware successful build the auth
# context from the incoming auth token
admin_token = self.get_scoped_token()
req = self._mock_request_object(admin_token)
application = None
middleware.AuthContextMiddleware(application).process_request(req)
self.assertEqual(
req.environ.get(authorization.AUTH_CONTEXT_ENV)['user_id'],
self.user['id'])
def test_auth_context_override(self):
overridden_context = 'OVERRIDDEN_CONTEXT'
# this token should not be used
token = uuid.uuid4().hex
req = self._mock_request_object(token)
req.environ[authorization.AUTH_CONTEXT_ENV] = overridden_context
application = None
middleware.AuthContextMiddleware(application).process_request(req)
# make sure overridden context take precedence
self.assertEqual(req.environ.get(authorization.AUTH_CONTEXT_ENV),
overridden_context)
def test_admin_token_auth_context(self):
# test to make sure AuthContextMiddleware does not attempt to build
# auth context if the incoming auth token is the special admin token
req = self._mock_request_object(CONF.admin_token)
application = None
middleware.AuthContextMiddleware(application).process_request(req)
self.assertDictEqual(req.environ.get(authorization.AUTH_CONTEXT_ENV),
{})

View File

@ -2485,3 +2485,22 @@ class TestTrustAuth(TestAuthInfo):
auth=auth_data,
expected_status=200)
self.assertValidRoleResponse(r, self.role)
class TestAPIProtectionWithoutAuthContextMiddleware(test_v3.RestfulTestCase):
def test_api_protection_with_no_auth_context_in_env(self):
auth_data = self.build_authentication_request(
user_id=self.default_domain_user['id'],
password=self.default_domain_user['password'],
project_id=self.project['id'])
resp = self.post('/auth/tokens', body=auth_data)
token = resp.headers.get('X-Subject-Token')
auth_controller = auth.controllers.Auth()
# all we care is that auth context is not in the environment and
# 'token_id' is used to build the auth context instead
context = {'subject_token_id': token,
'token_id': token,
'query_string': {},
'environment': {}}
r = auth_controller.validate_token(context)
self.assertEqual(r.status_code, 200)