Merge "build auth context from middleware"
This commit is contained in:
commit
49bba55d47
@ -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
|
||||
|
111
keystone/common/authorization.py
Normal file
111
keystone/common/authorization.py
Normal 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
|
@ -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'))
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'],
|
||||
|
@ -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),
|
||||
{})
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user