add more middleware

This commit is contained in:
termie 2012-01-10 17:50:32 -08:00
parent 5c6f0a2ad9
commit 61ecf60491
3 changed files with 440 additions and 0 deletions

View File

@ -0,0 +1,92 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Starting point for routing EC2 requests.
"""
from urlparse import urlparse
from eventlet.green import httplib
import webob.dec
import webob.exc
from nova import flags
from nova import utils
from nova import wsgi
FLAGS = flags.FLAGS
flags.DEFINE_string('keystone_ec2_url',
'http://localhost:5000/v2.0/ec2tokens',
'URL to get token from ec2 request.')
class EC2Token(wsgi.Middleware):
"""Authenticate an EC2 request with keystone and convert to token."""
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
# Read request signature and access id.
try:
signature = req.params['Signature']
access = req.params['AWSAccessKeyId']
except KeyError:
raise webob.exc.HTTPBadRequest()
# Make a copy of args for authentication and signature verification.
auth_params = dict(req.params)
# Not part of authentication args
auth_params.pop('Signature')
# Authenticate the request.
creds = {'ec2Credentials': {'access': access,
'signature': signature,
'host': req.host,
'verb': req.method,
'path': req.path,
'params': auth_params,
}}
creds_json = utils.dumps(creds)
headers = {'Content-Type': 'application/json'}
# Disable "has no x member" pylint error
# for httplib and urlparse
# pylint: disable-msg=E1101
o = urlparse(FLAGS.keystone_ec2_url)
if o.scheme == "http":
conn = httplib.HTTPConnection(o.netloc)
else:
conn = httplib.HTTPSConnection(o.netloc)
conn.request('POST', o.path, body=creds_json, headers=headers)
response = conn.getresponse().read()
conn.close()
# NOTE(vish): We could save a call to keystone by
# having keystone return token, tenant,
# user, and roles from this call.
result = utils.loads(response)
try:
token_id = result['access']['token']['id']
except (AttributeError, KeyError):
raise webob.exc.HTTPBadRequest()
# Authenticated!
req.headers['X-Auth-Token'] = token_id
return self.application

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2010-2011 OpenStack, LLC.
#
# 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.
"""
NOVA LAZY PROVISIONING AUTH MIDDLEWARE
This WSGI component allows keystone act as an identity service for nova by
lazy provisioning nova projects/users as authenticated by auth_token.
Use by applying after auth_token in the nova paste config.
Example: docs/nova-api-paste.ini
"""
from nova import auth
from nova import context
from nova import flags
from nova import utils
from nova import wsgi
from nova import exception
import webob.dec
import webob.exc
FLAGS = flags.FLAGS
class KeystoneAuthShim(wsgi.Middleware):
"""Lazy provisioning nova project/users from keystone tenant/user"""
def __init__(self, application, db_driver=None):
if not db_driver:
db_driver = FLAGS.db_driver
self.db = utils.import_object(db_driver)
self.auth = auth.manager.AuthManager()
super(KeystoneAuthShim, self).__init__(application)
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
# find or create user
try:
user_id = req.headers['X_USER']
except:
return webob.exc.HTTPUnauthorized()
try:
user_ref = self.auth.get_user(user_id)
except:
user_ref = self.auth.create_user(user_id)
# get the roles
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
# set user admin-ness to keystone admin-ness
# FIXME: keystone-admin-role value from keystone.conf is not
# used neither here nor in glance_auth_token!
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
is_admin = 'Admin' in roles
if user_ref.is_admin() != is_admin:
self.auth.modify_user(user_ref, admin=is_admin)
# create a project for tenant
if 'X_TENANT_ID' in req.headers:
# This is the new header since Keystone went to ID/Name
project_id = req.headers['X_TENANT_ID']
else:
# This is for legacy compatibility
project_id = req.headers['X_TENANT']
if project_id:
try:
project_ref = self.auth.get_project(project_id)
except:
project_ref = self.auth.create_project(project_id, user_id)
# ensure user is a member of project
if not self.auth.is_project_member(user_id, project_id):
self.auth.add_to_project(user_id, project_id)
else:
project_ref = None
# Get the auth token
auth_token = req.headers.get('X_AUTH_TOKEN',
req.headers.get('X_STORAGE_TOKEN'))
# Build a context, including the auth_token...
ctx = context.RequestContext(user_id, project_id,
is_admin=('Admin' in roles),
auth_token=auth_token)
req.environ['nova.context'] = ctx
return self.application

243
keystone/middleware/swift_auth.py Executable file
View File

@ -0,0 +1,243 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2010-2011 OpenStack, LLC.
#
# 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.
"""
TOKEN-BASED AUTH MIDDLEWARE FOR SWIFT
Authentication on incoming request
* grab token from X-Auth-Token header
* TODO: grab the memcache servers from the request env
* TODOcheck for auth information in memcache
* check for auth information from keystone
* return if unauthorized
* decorate the request for authorization in swift
* forward to the swift proxy app
Authorization via callback
* check the path and extract the tenant
* get the auth information stored in keystone.identity during
authentication
* TODO: check if the user is an account admin or a reseller admin
* determine what object-type to authorize (account, container, object)
* use knowledge of tenant, admin status, and container acls to authorize
"""
import json
from urlparse import urlparse
from webob.exc import HTTPUnauthorized, HTTPNotFound, HTTPExpectationFailed
from keystone.bufferedhttp import http_connect_raw as http_connect
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.utils import get_logger, split_path
PROTOCOL_NAME = "Swift Token Authentication"
class AuthProtocol(object):
"""Handles authenticating and aurothrizing client calls.
Add to your pipeline in paste config like:
[pipeline:main]
pipeline = catch_errors healthcheck cache keystone proxy-server
[filter:keystone]
use = egg:keystone#swiftauth
keystone_url = http://127.0.0.1:8080
keystone_admin_token = 999888777666
"""
def __init__(self, app, conf):
"""Store valuable bits from the conf and set up logging."""
self.app = app
self.keystone_url = urlparse(conf.get('keystone_url'))
self.admin_token = conf.get('keystone_admin_token')
self.reseller_prefix = conf.get('reseller_prefix', 'AUTH')
self.log = get_logger(conf, log_route='keystone')
self.log.info('Keystone middleware started')
def __call__(self, env, start_response):
"""Authenticate the incoming request.
If authentication fails return an appropriate http status here,
otherwise forward through the rest of the app.
"""
self.log.debug('Keystone middleware called')
token = self._get_claims(env)
self.log.debug('token: %s', token)
if token:
identity = self._validate_claims(token)
if identity:
self.log.debug('request authenticated: %r', identity)
return self.perform_authenticated_request(identity, env,
start_response)
else:
self.log.debug('anonymous request')
return self.unauthorized_request(env, start_response)
self.log.debug('no auth token in request headers')
return self.perform_unidentified_request(env, start_response)
def unauthorized_request(self, env, start_response):
"""Clinet provided a token that wasn't acceptable, error out."""
return HTTPUnauthorized()(env, start_response)
def unauthorized(self, req):
"""Return unauthorized given a webob Request object.
This can be stuffed into the evironment for swift.authorize or
called from the authoriztion callback when authorization fails.
"""
return HTTPUnauthorized(request=req)
def perform_authenticated_request(self, identity, env, start_response):
"""Client provieded a valid identity, so use it for authorization."""
env['keystone.identity'] = identity
env['swift.authorize'] = self.authorize
env['swift.clean_acl'] = clean_acl
self.log.debug('calling app: %s // %r', start_response, env)
rv = self.app(env, start_response)
self.log.debug('return from app: %r', rv)
return rv
def perform_unidentified_request(self, env, start_response):
"""Withouth authentication data, use acls for access control."""
env['swift.authorize'] = self.authorize_via_acl
env['swift.clean_acl'] = self.authorize_via_acl
return self.app(env, start_response)
def authorize(self, req):
"""Used when we have a valid identity from keystone."""
self.log.debug('keystone middleware authorization begin')
env = req.environ
tenant = env.get('keystone.identity', {}).get('tenant')
if not tenant:
self.log.warn('identity info not present in authorize request')
return HTTPExpectationFailed('Unable to locate auth claim',
request=req)
# TODO(todd): everyone under a tenant can do anything to that tenant.
# more realistic would be role/group checking to do things
# like deleting the account or creating/deleting containers
# esp. when owned by other users in the same tenant.
if req.path.startswith('/v1/%s_%s' % (self.reseller_prefix, tenant)):
self.log.debug('AUTHORIZED OKAY')
return None
self.log.debug('tenant mismatch: %r', tenant)
return self.unauthorized(req)
def authorize_via_acl(self, req):
"""Anon request handling.
For now this only allows anon read of objects. Container and account
actions are prohibited.
"""
self.log.debug('authorizing anonymous request')
try:
version, account, container, obj = split_path(req.path, 1, 4, True)
except ValueError:
return HTTPNotFound(request=req)
if obj:
return self._authorize_anon_object(req, account, container, obj)
if container:
return self._authorize_anon_container(req, account, container)
if account:
return self._authorize_anon_account(req, account)
return self._authorize_anon_toplevel(req)
def _authorize_anon_object(self, req, account, container, obj):
referrers, groups = parse_acl(getattr(req, 'acl', None))
if referrer_allowed(req.referer, referrers):
self.log.debug('anonymous request AUTHORIZED OKAY')
return None
return self.unauthorized(req)
def _authorize_anon_container(self, req, account, container):
return self.unauthorized(req)
def _authorize_anon_account(self, req, account):
return self.unauthorized(req)
def _authorize_anon_toplevel(self, req):
return self.unauthorized(req)
def _get_claims(self, env):
claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
return claims
def _validate_claims(self, claims):
"""Ask keystone (as keystone admin) for information for this user."""
# TODO(todd): cache
self.log.debug('Asking keystone to validate token')
headers = {"Content-type": "application/json",
"Accept": "application/json",
"X-Auth-Token": self.admin_token}
self.log.debug('headers: %r', headers)
self.log.debug('url: %s', self.keystone_url)
conn = http_connect(self.keystone_url.hostname, self.keystone_url.port,
'GET', '/v2.0/tokens/%s' % claims, headers=headers)
resp = conn.getresponse()
data = resp.read()
conn.close()
# Check http status code for the "OK" family of responses
if not str(resp.status).startswith('20'):
return False
identity_info = json.loads(data)
roles = []
role_refs = identity_info["access"]["user"]["roles"]
if role_refs is not None:
for role_ref in role_refs:
roles.append(role_ref["id"])
try:
tenant = identity_info['access']['token']['tenantId']
except:
tenant = None
if not tenant:
tenant = identity_info['access']['user']['tenantId']
# TODO(Ziad): add groups back in
identity = {'user': identity_info['access']['user']['username'],
'tenant': tenant,
'roles': roles}
return identity
def filter_factory(global_conf, **local_conf):
"""Returns a WSGI filter app for use with paste.deploy."""
conf = global_conf.copy()
conf.update(local_conf)
def auth_filter(app):
return AuthProtocol(app, conf)
return auth_filter