From 3a3553aa39bc65b9de15dc7278c7b8d655965f97 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 10 Jan 2012 17:21:28 -0800 Subject: [PATCH 001/121] add legacy middleware --- __init__.py | 1 + auth_token.py | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++ internal.py | 98 +++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 __init__.py create mode 100755 auth_token.py create mode 100644 internal.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..1593e6e2 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from keystone.middleware.internal import * diff --git a/auth_token.py b/auth_token.py new file mode 100755 index 00000000..c4e28589 --- /dev/null +++ b/auth_token.py @@ -0,0 +1,397 @@ +#!/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 + +This WSGI component performs multiple jobs: + +* it verifies that incoming client requests have valid tokens by verifying + tokens with the auth service. +* it will reject unauthenticated requests UNLESS it is in 'delay_auth_decision' + mode, which means the final decision is delegated to the downstream WSGI + component (usually the OpenStack service) +* it will collect and forward identity information from a valid token + such as user name etc... + +Refer to: http://wiki.openstack.org/openstack-authn + + +HEADERS +------- + +* Headers starting with HTTP\_ is a standard http header +* Headers starting with HTTP_X is an extended http header + +Coming in from initial call from client or customer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_AUTH_TOKEN + the client token being passed in + +HTTP_X_STORAGE_TOKEN + the client token being passed in (legacy Rackspace use) to support + cloud files + +Used for communication between components +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +www-authenticate + only used if this component is being used remotely + +HTTP_AUTHORIZATION + basic auth password used to validate the connection + +What we add to the request for use by the OpenStack service +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +HTTP_X_AUTHORIZATION + the client identity being passed in + +""" +import httplib +import json +import os + +import eventlet +from eventlet import wsgi +from paste import deploy +from urlparse import urlparse +import webob +import webob.exc +from webob.exc import HTTPUnauthorized + +from keystone.bufferedhttp import http_connect_raw as http_connect + +PROTOCOL_NAME = "Token Authentication" + + +class AuthProtocol(object): + """Auth Middleware that handles authenticating client calls""" + + def _init_protocol_common(self, app, conf): + """ Common initialization code""" + print "Starting the %s component" % PROTOCOL_NAME + + self.conf = conf + self.app = app + #if app is set, then we are in a WSGI pipeline and requests get passed + # on to app. If it is not set, this component should forward requests + + # where to find the OpenStack service (if not in local WSGI chain) + # these settings are only used if this component is acting as a proxy + # and the OpenSTack service is running remotely + self.service_protocol = conf.get('service_protocol', 'https') + self.service_host = conf.get('service_host') + self.service_port = int(conf.get('service_port')) + self.service_url = '%s://%s:%s' % (self.service_protocol, + self.service_host, + self.service_port) + # used to verify this component with the OpenStack service or PAPIAuth + self.service_pass = conf.get('service_pass') + + # delay_auth_decision means we still allow unauthenticated requests + # through and we let the downstream service make the final decision + self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) + + def _init_protocol(self, conf): + """ Protocol specific initialization """ + + # where to find the auth service (we use this to validate tokens) + self.auth_host = conf.get('auth_host') + self.auth_port = int(conf.get('auth_port')) + self.auth_protocol = conf.get('auth_protocol', 'https') + + # where to tell clients to find the auth service (default to url + # constructed based on endpoint we have for the service to use) + self.auth_location = conf.get('auth_uri', + "%s://%s:%s" % (self.auth_protocol, + self.auth_host, + self.auth_port)) + + # Credentials used to verify this component with the Auth service since + # validating tokens is a privileged call + self.admin_token = conf.get('admin_token') + + def __init__(self, app, conf): + """ Common initialization code """ + + #TODO(ziad): maybe we refactor this into a superclass + self._init_protocol_common(app, conf) # Applies to all protocols + self._init_protocol(conf) # Specific to this protocol + + def __call__(self, env, start_response): + """ Handle incoming request. Authenticate. And send downstream. """ + + #Prep headers to forward request to local or remote downstream service + proxy_headers = env.copy() + for header in proxy_headers.iterkeys(): + if header[0:5] == 'HTTP_': + proxy_headers[header[5:]] = proxy_headers[header] + del proxy_headers[header] + + #Look for authentication claims + claims = self._get_claims(env) + if not claims: + #No claim(s) provided + if self.delay_auth_decision: + #Configured to allow downstream service to make final decision. + #So mark status as Invalid and forward the request downstream + self._decorate_request("X_IDENTITY_STATUS", + "Invalid", env, proxy_headers) + else: + #Respond to client as appropriate for this auth protocol + return self._reject_request(env, start_response) + else: + # this request is presenting claims. Let's validate them + valid = self._validate_claims(claims) + if not valid: + # Keystone rejected claim + if self.delay_auth_decision: + # Downstream service will receive call still and decide + self._decorate_request("X_IDENTITY_STATUS", + "Invalid", env, proxy_headers) + else: + #Respond to client as appropriate for this auth protocol + return self._reject_claims(env, start_response) + else: + self._decorate_request("X_IDENTITY_STATUS", + "Confirmed", env, proxy_headers) + + #Collect information about valid claims + if valid: + claims = self._expound_claims(claims) + + # Store authentication data + if claims: + self._decorate_request('X_AUTHORIZATION', "Proxy %s" % + claims['user'], env, proxy_headers) + + # For legacy compatibility before we had ID and Name + self._decorate_request('X_TENANT', + claims['tenant'], env, proxy_headers) + + # Services should use these + self._decorate_request('X_TENANT_NAME', + claims.get('tenant_name', claims['tenant']), + env, proxy_headers) + self._decorate_request('X_TENANT_ID', + claims['tenant'], env, proxy_headers) + + self._decorate_request('X_USER', + claims['user'], env, proxy_headers) + if 'roles' in claims and len(claims['roles']) > 0: + if claims['roles'] != None: + roles = '' + for role in claims['roles']: + if len(roles) > 0: + roles += ',' + roles += role + self._decorate_request('X_ROLE', + roles, env, proxy_headers) + + # NOTE(todd): unused + self.expanded = True + + #Send request downstream + return self._forward_request(env, start_response, proxy_headers) + + # NOTE(todd): unused + def get_admin_auth_token(self, username, password): + """ + This function gets an admin auth token to be used by this service to + validate a user's token. Validate_token is a priviledged call so + it needs to be authenticated by a service that is calling it + """ + headers = {"Content-type": "application/json", + "Accept": "application/json"} + params = {"passwordCredentials": {"username": username, + "password": password, + "tenantId": "1"}} + conn = httplib.HTTPConnection("%s:%s" \ + % (self.auth_host, self.auth_port)) + conn.request("POST", "/v2.0/tokens", json.dumps(params), \ + headers=headers) + response = conn.getresponse() + data = response.read() + return data + + def _get_claims(self, env): + """Get claims from request""" + claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) + return claims + + def _reject_request(self, env, start_response): + """Redirect client to auth server""" + return webob.exc.HTTPUnauthorized("Authentication required", + [("WWW-Authenticate", + "Keystone uri='%s'" % self.auth_location)])(env, + start_response) + + def _reject_claims(self, env, start_response): + """Client sent bad claims""" + return webob.exc.HTTPUnauthorized()(env, + start_response) + + def _validate_claims(self, claims): + """Validate claims, and provide identity information isf applicable """ + + # Step 1: We need to auth with the keystone service, so get an + # admin token + #TODO(ziad): Need to properly implement this, where to store creds + # for now using token from ini + #auth = self.get_admin_auth_token("admin", "secrete", "1") + #admin_token = json.loads(auth)["auth"]["token"]["id"] + + # Step 2: validate the user's token with the auth service + # since this is a priviledged op,m we need to auth ourselves + # by using an admin token + headers = {"Content-type": "application/json", + "Accept": "application/json", + "X-Auth-Token": self.admin_token} + ##TODO(ziad):we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # "X-Auth-Token": admin_token} + # we're using a test token from the ini file for now + conn = http_connect(self.auth_host, self.auth_port, 'GET', + '/v2.0/tokens/%s' % claims, headers=headers) + resp = conn.getresponse() + # data = resp.read() + conn.close() + + if not str(resp.status).startswith('20'): + # Keystone rejected claim + return False + else: + #TODO(Ziad): there is an optimization we can do here. We have just + #received data from Keystone that we can use instead of making + #another call in _expound_claims + return True + + def _expound_claims(self, claims): + # Valid token. Get user data and put it in to the call + # so the downstream service can use it + headers = {"Content-type": "application/json", + "Accept": "application/json", + "X-Auth-Token": self.admin_token} + ##TODO(ziad):we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # "X-Auth-Token": admin_token} + # we're using a test token from the ini file for now + conn = http_connect(self.auth_host, self.auth_port, 'GET', + '/v2.0/tokens/%s' % claims, headers=headers) + resp = conn.getresponse() + data = resp.read() + conn.close() + + if not str(resp.status).startswith('20'): + raise LookupError('Unable to locate claims: %s' % resp.status) + + token_info = json.loads(data) + roles = [] + role_refs = token_info["access"]["user"]["roles"] + if role_refs != None: + for role_ref in role_refs: + # Nova looks for the non case-sensitive role 'Admin' + # to determine admin-ness + roles.append(role_ref["name"]) + + try: + tenant = token_info['access']['token']['tenant']['id'] + tenant_name = token_info['access']['token']['tenant']['name'] + except: + tenant = None + tenant_name = None + if not tenant: + tenant = token_info['access']['user'].get('tenantId') + tenant_name = token_info['access']['user'].get('tenantName') + verified_claims = {'user': token_info['access']['user']['username'], + 'tenant': tenant, + 'roles': roles} + if tenant_name: + verified_claims['tenantName'] = tenant_name + return verified_claims + + def _decorate_request(self, index, value, env, proxy_headers): + """Add headers to request""" + proxy_headers[index] = value + env["HTTP_%s" % index] = value + + def _forward_request(self, env, start_response, proxy_headers): + """Token/Auth processed & claims added to headers""" + self._decorate_request('AUTHORIZATION', + "Basic %s" % self.service_pass, env, proxy_headers) + #now decide how to pass on the call + if self.app: + # Pass to downstream WSGI component + return self.app(env, start_response) + #.custom_start_response) + else: + # We are forwarding to a remote service (no downstream WSGI app) + req = webob.Request(proxy_headers) + parsed = urlparse(req.url) + + conn = http_connect(self.service_host, + self.service_port, + req.method, + parsed.path, + proxy_headers, + ssl=(self.service_protocol == 'https')) + resp = conn.getresponse() + data = resp.read() + + #TODO(ziad): use a more sophisticated proxy + # we are rewriting the headers now + + if resp.status == 401 or resp.status == 305: + # Add our own headers to the list + headers = [("WWW_AUTHENTICATE", + "Keystone uri='%s'" % self.auth_location)] + return webob.Response(status=resp.status, + body=data, + headerlist=headers)(env, start_response) + else: + return webob.Response(status=resp.status, + body=data)(env, start_response) + + +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 + + +def app_factory(global_conf, **local_conf): + conf = global_conf.copy() + conf.update(local_conf) + return AuthProtocol(None, conf) + +if __name__ == "__main__": + app = deploy.loadapp("config:" + \ + os.path.join(os.path.abspath(os.path.dirname(__file__)), + os.pardir, + os.pardir, + "examples/paste/auth_token.ini"), + global_conf={"log_name": "auth_token.log"}) + wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/internal.py b/internal.py new file mode 100644 index 00000000..5bac8fb2 --- /dev/null +++ b/internal.py @@ -0,0 +1,98 @@ +import json + +from keystone import config +from keystone import wsgi + + +CONF = config.CONF + + +# Header used to transmit the auth token +AUTH_TOKEN_HEADER = 'X-Auth-Token' + + +# Environment variable used to pass the request context +CONTEXT_ENV = 'openstack.context' + + +# Environment variable used to pass the request params +PARAMS_ENV = 'openstack.params' + + +class TokenAuthMiddleware(wsgi.Middleware): + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['token_id'] = token + request.environ[CONTEXT_ENV] = context + + +class AdminTokenAuthMiddleware(wsgi.Middleware): + """A trivial filter that checks for a pre-defined admin token. + + Sets 'is_admin' to true in the context, expected to be checked by + methods that are admin-only. + + """ + + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['is_admin'] = (token == CONF.admin_token) + request.environ[CONTEXT_ENV] = context + + +class PostParamsMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as POST parameters. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + + def process_request(self, request): + params_parsed = request.params + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params + + +class JsonBodyMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as serialized JSON. + + Accepting arguments as JSON is useful for accepting data that may be more + complex than simple primitives. + + In this case we accept it as urlencoded data under the key 'json' as in + json= but this could be extended to accept raw JSON + in the POST body. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + + def process_request(self, request): + #if 'json' not in request.params: + # return + + params_json = request.body + if not params_json: + return + + params_parsed = json.loads(params_json) + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params From 1ef9bc88cb8c9fab67c2aa80a68dbd9984d26d32 Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 10 Jan 2012 17:29:37 -0800 Subject: [PATCH 002/121] woops --- glance_auth_token.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 glance_auth_token.py diff --git a/glance_auth_token.py b/glance_auth_token.py new file mode 100644 index 00000000..6bef1390 --- /dev/null +++ b/glance_auth_token.py @@ -0,0 +1,78 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# 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. + +""" +Glance Keystone Integration Middleware + +This WSGI component allows keystone to act as an identity service for +glance. Glance now supports the concept of images owned by a tenant, +and this middleware takes the authentication information provided by +auth_token and builds a glance-compatible context object. + +Use by applying after auth_token in the glance-api.ini and +glance-registry.ini configurations, replacing the existing context +middleware. + +Example: examples/paste/glance-api.conf, + examples/paste/glance-registry.conf +""" + +from glance.common import context + + +class KeystoneContextMiddleware(context.ContextMiddleware): + """Glance keystone integration middleware.""" + + def process_request(self, req): + """ + Extract keystone-provided authentication information from the + request and construct an appropriate context from it. + """ + # Only accept the authentication information if the identity + # has been confirmed--presumably by upstream + if req.headers.get('X_IDENTITY_STATUS', 'Invalid') != 'Confirmed': + # Use the default empty context + req.context = self.make_context(read_only=True) + return + + # OK, let's extract the information we need + auth_tok = req.headers.get('X_AUTH_TOKEN', + req.headers.get('X_STORAGE_TOKEN')) + user = req.headers.get('X_USER') + tenant = req.headers.get('X_TENANT') + roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] + is_admin = 'Admin' in roles + + # Construct the context + req.context = self.make_context(auth_tok=auth_tok, + user=user, + tenant=tenant, + roles=roles, + is_admin=is_admin) + + +def filter_factory(global_conf, **local_conf): + """ + Factory method for paste.deploy + """ + conf = global_conf.copy() + conf.update(local_conf) + + def filter(app): + return KeystoneContextMiddleware(app, conf) + + return filter From 80ee5a14a3c2e5becbc205fb019cf98bd0caa47d Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 10 Jan 2012 17:50:32 -0800 Subject: [PATCH 003/121] add more middleware --- ec2_token.py | 92 +++++++++++++++++ nova_auth_token.py | 105 ++++++++++++++++++++ swift_auth.py | 243 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 440 insertions(+) create mode 100644 ec2_token.py create mode 100644 nova_auth_token.py create mode 100755 swift_auth.py diff --git a/ec2_token.py b/ec2_token.py new file mode 100644 index 00000000..af141627 --- /dev/null +++ b/ec2_token.py @@ -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 diff --git a/nova_auth_token.py b/nova_auth_token.py new file mode 100644 index 00000000..68ad1d9d --- /dev/null +++ b/nova_auth_token.py @@ -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 diff --git a/swift_auth.py b/swift_auth.py new file mode 100755 index 00000000..75120c34 --- /dev/null +++ b/swift_auth.py @@ -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 From b422378cddbb9d9d6189f0900d2aec80ea00ff40 Mon Sep 17 00:00:00 2001 From: termie Date: Wed, 11 Jan 2012 12:57:20 -0800 Subject: [PATCH 004/121] check for membership --- nova_keystone_context.py | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 nova_keystone_context.py diff --git a/nova_keystone_context.py b/nova_keystone_context.py new file mode 100644 index 00000000..5c41bc87 --- /dev/null +++ b/nova_keystone_context.py @@ -0,0 +1,69 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 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 Auth Middleware. + +""" + +import webob.dec +import webob.exc + +from nova import context +from nova import flags +from nova import wsgi + + +FLAGS = flags.FLAGS +flags.DECLARE('use_forwarded_for', 'nova.api.auth') + + +class NovaKeystoneContext(wsgi.Middleware): + """Make a request context from keystone headers""" + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + try: + user_id = req.headers['X_USER'] + except KeyError: + return webob.exc.HTTPUnauthorized() + # get the roles + roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] + + 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'] + + # 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... + remote_address = getattr(req, 'remote_address', '127.0.0.1') + remote_address = req.remote_addr + if FLAGS.use_forwarded_for: + remote_address = req.headers.get('X-Forwarded-For', remote_address) + ctx = context.RequestContext(user_id, + project_id, + roles=roles, + auth_token=auth_token, + strategy='keystone', + remote_address=remote_address) + + req.environ['nova.context'] = ctx + return self.application From ebbe1dc0574d5556318b6807342395230578bae7 Mon Sep 17 00:00:00 2001 From: termie Date: Thu, 12 Jan 2012 16:07:23 -0800 Subject: [PATCH 005/121] re-indent --- internal.py | 106 ++++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/internal.py b/internal.py index 5bac8fb2..ccf68b82 100644 --- a/internal.py +++ b/internal.py @@ -1,3 +1,5 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + import json from keystone import config @@ -20,79 +22,79 @@ PARAMS_ENV = 'openstack.params' class TokenAuthMiddleware(wsgi.Middleware): - def process_request(self, request): - token = request.headers.get(AUTH_TOKEN_HEADER) - context = request.environ.get(CONTEXT_ENV, {}) - context['token_id'] = token - request.environ[CONTEXT_ENV] = context + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['token_id'] = token + request.environ[CONTEXT_ENV] = context class AdminTokenAuthMiddleware(wsgi.Middleware): - """A trivial filter that checks for a pre-defined admin token. + """A trivial filter that checks for a pre-defined admin token. - Sets 'is_admin' to true in the context, expected to be checked by - methods that are admin-only. + Sets 'is_admin' to true in the context, expected to be checked by + methods that are admin-only. - """ + """ - def process_request(self, request): - token = request.headers.get(AUTH_TOKEN_HEADER) - context = request.environ.get(CONTEXT_ENV, {}) - context['is_admin'] = (token == CONF.admin_token) - request.environ[CONTEXT_ENV] = context + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['is_admin'] = (token == CONF.admin_token) + request.environ[CONTEXT_ENV] = context class PostParamsMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as POST parameters. + """Middleware to allow method arguments to be passed as POST parameters. - Filters out the parameters `self`, `context` and anything beginning with - an underscore. + Filters out the parameters `self`, `context` and anything beginning with + an underscore. - """ + """ - def process_request(self, request): - params_parsed = request.params - params = {} - for k, v in params_parsed.iteritems(): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v + def process_request(self, request): + params_parsed = request.params + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v - request.environ[PARAMS_ENV] = params + request.environ[PARAMS_ENV] = params class JsonBodyMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as serialized JSON. + """Middleware to allow method arguments to be passed as serialized JSON. - Accepting arguments as JSON is useful for accepting data that may be more - complex than simple primitives. + Accepting arguments as JSON is useful for accepting data that may be more + complex than simple primitives. - In this case we accept it as urlencoded data under the key 'json' as in - json= but this could be extended to accept raw JSON - in the POST body. + In this case we accept it as urlencoded data under the key 'json' as in + json= but this could be extended to accept raw JSON + in the POST body. - Filters out the parameters `self`, `context` and anything beginning with - an underscore. + Filters out the parameters `self`, `context` and anything beginning with + an underscore. - """ + """ - def process_request(self, request): - #if 'json' not in request.params: - # return + def process_request(self, request): + #if 'json' not in request.params: + # return - params_json = request.body - if not params_json: - return + params_json = request.body + if not params_json: + return - params_parsed = json.loads(params_json) - params = {} - for k, v in params_parsed.iteritems(): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v + params_parsed = json.loads(params_json) + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v - request.environ[PARAMS_ENV] = params + request.environ[PARAMS_ENV] = params From 7abc4183d2b40710cfa1a8faac52420bd4927a8e Mon Sep 17 00:00:00 2001 From: termie Date: Wed, 18 Jan 2012 21:10:08 -0800 Subject: [PATCH 006/121] fix some imports --- internal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal.py b/internal.py index ccf68b82..389c9bf0 100644 --- a/internal.py +++ b/internal.py @@ -3,7 +3,7 @@ import json from keystone import config -from keystone import wsgi +from keystone.common import wsgi CONF = config.CONF From d57b88f66bbe2920323fc8cb0f8f4847104af46d Mon Sep 17 00:00:00 2001 From: termie Date: Wed, 18 Jan 2012 21:15:14 -0800 Subject: [PATCH 007/121] update some names --- __init__.py | 2 +- internal.py => core.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename internal.py => core.py (100%) diff --git a/__init__.py b/__init__.py index 1593e6e2..e2e9a993 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -from keystone.middleware.internal import * +from keystone.middleware.core import * diff --git a/internal.py b/core.py similarity index 100% rename from internal.py rename to core.py From 0a7d43e07be336ccc4aef969294a8c8bca968e4a Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 24 Jan 2012 15:55:44 -0800 Subject: [PATCH 008/121] fix middleware --- auth_token.py | 2 +- swift_auth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index c4e28589..d806dd01 100755 --- a/auth_token.py +++ b/auth_token.py @@ -76,7 +76,7 @@ import webob import webob.exc from webob.exc import HTTPUnauthorized -from keystone.bufferedhttp import http_connect_raw as http_connect +from keystone.common.bufferedhttp import http_connect_raw as http_connect PROTOCOL_NAME = "Token Authentication" diff --git a/swift_auth.py b/swift_auth.py index 75120c34..35bab6a3 100755 --- a/swift_auth.py +++ b/swift_auth.py @@ -43,7 +43,7 @@ import json from urlparse import urlparse from webob.exc import HTTPUnauthorized, HTTPNotFound, HTTPExpectationFailed -from keystone.bufferedhttp import http_connect_raw as http_connect +from keystone.common.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 From 85c7a7c62e8ac629af7da99c7e62e2a10cf65040 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Sun, 29 Jan 2012 10:57:02 -0800 Subject: [PATCH 009/121] doc updates --- core.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core.py b/core.py index 389c9bf0..305a7d44 100644 --- a/core.py +++ b/core.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import json +import webob from keystone import config from keystone.common import wsgi @@ -98,3 +99,40 @@ class JsonBodyMiddleware(wsgi.Middleware): params[k] = v request.environ[PARAMS_ENV] = params + + +class Debug(wsgi.Middleware): + """ + Middleware that produces stream debugging traces to the console (stdout) + for HTTP requests and responses flowing through it. + """ + + @webob.dec.wsgify + def __call__(self, req): + print ("*" * 40) + " REQUEST ENVIRON" + for key, value in req.environ.items(): + print key, "=", value + print + resp = req.get_response(self.application) + + print ("*" * 40) + " RESPONSE HEADERS" + for (key, value) in resp.headers.iteritems(): + print key, "=", value + print + + resp.app_iter = self.print_generator(resp.app_iter) + + return resp + + @staticmethod + def print_generator(app_iter): + """ + Iterator that prints the contents of a wrapper string iterator + when iterated. + """ + print ("*" * 40) + " BODY" + for part in app_iter: + sys.stdout.write(part) + sys.stdout.flush() + yield part + print From e4ba623eef7c008fbcd232b3cecbfc8f2d7be1c3 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Mon, 30 Jan 2012 19:59:58 +0000 Subject: [PATCH 010/121] pep8 cleanup --- core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core.py b/core.py index 305a7d44..73dcc003 100644 --- a/core.py +++ b/core.py @@ -103,8 +103,8 @@ class JsonBodyMiddleware(wsgi.Middleware): class Debug(wsgi.Middleware): """ - Middleware that produces stream debugging traces to the console (stdout) - for HTTP requests and responses flowing through it. + Middleware that produces stream debugging traces to the console (stdout) + for HTTP requests and responses flowing through it. """ @webob.dec.wsgify From 99065fba398c92eb2ff99b3e8fa43f9519cdf8cb Mon Sep 17 00:00:00 2001 From: termie Date: Tue, 31 Jan 2012 21:31:36 -0800 Subject: [PATCH 011/121] fix keystoneclient tests --- core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core.py b/core.py index 73dcc003..21e0f624 100644 --- a/core.py +++ b/core.py @@ -90,6 +90,8 @@ class JsonBodyMiddleware(wsgi.Middleware): return params_parsed = json.loads(params_json) + if not params_parsed: + params_parsed = {} params = {} for k, v in params_parsed.iteritems(): if k in ('self', 'context'): From 9771a6dc60c8e053d7a3486a4cc8bda60faa7f65 Mon Sep 17 00:00:00 2001 From: termie Date: Wed, 1 Feb 2012 16:51:46 -0800 Subject: [PATCH 012/121] be more safe with getting json aprams --- core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core.py b/core.py index 21e0f624..46d38673 100644 --- a/core.py +++ b/core.py @@ -89,9 +89,13 @@ class JsonBodyMiddleware(wsgi.Middleware): if not params_json: return - params_parsed = json.loads(params_json) - if not params_parsed: - params_parsed = {} + params_parsed = {} + try: + params_parsed = json.loads(params_json) + finally: + if not params_parsed: + params_parsed = {} + params = {} for k, v in params_parsed.iteritems(): if k in ('self', 'context'): From 3a0548d225098d9df7bf4dbd2238f7e54937cae2 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 8 Feb 2012 14:01:03 -0600 Subject: [PATCH 013/121] termie all the things Change-Id: Ib7b5fab2a09de8a9dcad8d8b0cf71c529e944f8c --- auth_token.py | 76 +++++++++++++++++++++++++-------------------------- core.py | 10 +++---- ec2_token.py | 4 +-- swift_auth.py | 14 +++++----- 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/auth_token.py b/auth_token.py index d806dd01..0dc723b9 100755 --- a/auth_token.py +++ b/auth_token.py @@ -78,7 +78,7 @@ from webob.exc import HTTPUnauthorized from keystone.common.bufferedhttp import http_connect_raw as http_connect -PROTOCOL_NAME = "Token Authentication" +PROTOCOL_NAME = 'Token Authentication' class AuthProtocol(object): @@ -86,7 +86,7 @@ class AuthProtocol(object): def _init_protocol_common(self, app, conf): """ Common initialization code""" - print "Starting the %s component" % PROTOCOL_NAME + print 'Starting the %s component' % PROTOCOL_NAME self.conf = conf self.app = app @@ -120,7 +120,7 @@ class AuthProtocol(object): # where to tell clients to find the auth service (default to url # constructed based on endpoint we have for the service to use) self.auth_location = conf.get('auth_uri', - "%s://%s:%s" % (self.auth_protocol, + '%s://%s:%s' % (self.auth_protocol, self.auth_host, self.auth_port)) @@ -152,8 +152,8 @@ class AuthProtocol(object): if self.delay_auth_decision: #Configured to allow downstream service to make final decision. #So mark status as Invalid and forward the request downstream - self._decorate_request("X_IDENTITY_STATUS", - "Invalid", env, proxy_headers) + self._decorate_request('X_IDENTITY_STATUS', + 'Invalid', env, proxy_headers) else: #Respond to client as appropriate for this auth protocol return self._reject_request(env, start_response) @@ -164,14 +164,14 @@ class AuthProtocol(object): # Keystone rejected claim if self.delay_auth_decision: # Downstream service will receive call still and decide - self._decorate_request("X_IDENTITY_STATUS", - "Invalid", env, proxy_headers) + self._decorate_request('X_IDENTITY_STATUS', + 'Invalid', env, proxy_headers) else: #Respond to client as appropriate for this auth protocol return self._reject_claims(env, start_response) else: - self._decorate_request("X_IDENTITY_STATUS", - "Confirmed", env, proxy_headers) + self._decorate_request('X_IDENTITY_STATUS', + 'Confirmed', env, proxy_headers) #Collect information about valid claims if valid: @@ -179,7 +179,7 @@ class AuthProtocol(object): # Store authentication data if claims: - self._decorate_request('X_AUTHORIZATION', "Proxy %s" % + self._decorate_request('X_AUTHORIZATION', 'Proxy %s' % claims['user'], env, proxy_headers) # For legacy compatibility before we had ID and Name @@ -218,14 +218,14 @@ class AuthProtocol(object): validate a user's token. Validate_token is a priviledged call so it needs to be authenticated by a service that is calling it """ - headers = {"Content-type": "application/json", - "Accept": "application/json"} - params = {"passwordCredentials": {"username": username, - "password": password, - "tenantId": "1"}} - conn = httplib.HTTPConnection("%s:%s" \ + headers = {'Content-type': 'application/json', + 'Accept': 'application/json'} + params = {'passwordCredentials': {'username': username, + 'password': password, + 'tenantId': '1'}} + conn = httplib.HTTPConnection('%s:%s' \ % (self.auth_host, self.auth_port)) - conn.request("POST", "/v2.0/tokens", json.dumps(params), \ + conn.request('POST', '/v2.0/tokens', json.dumps(params), \ headers=headers) response = conn.getresponse() data = response.read() @@ -238,8 +238,8 @@ class AuthProtocol(object): def _reject_request(self, env, start_response): """Redirect client to auth server""" - return webob.exc.HTTPUnauthorized("Authentication required", - [("WWW-Authenticate", + return webob.exc.HTTPUnauthorized('Authentication required', + [('WWW-Authenticate', "Keystone uri='%s'" % self.auth_location)])(env, start_response) @@ -255,19 +255,19 @@ class AuthProtocol(object): # admin token #TODO(ziad): Need to properly implement this, where to store creds # for now using token from ini - #auth = self.get_admin_auth_token("admin", "secrete", "1") - #admin_token = json.loads(auth)["auth"]["token"]["id"] + #auth = self.get_admin_auth_token('admin', 'secrete', '1') + #admin_token = json.loads(auth)['auth']['token']['id'] # Step 2: validate the user's token with the auth service # since this is a priviledged op,m we need to auth ourselves # by using an admin token - headers = {"Content-type": "application/json", - "Accept": "application/json", - "X-Auth-Token": self.admin_token} + headers = {'Content-type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': self.admin_token} ##TODO(ziad):we need to figure out how to auth to keystone #since validate_token is a priviledged call #Khaled's version uses creds to get a token - # "X-Auth-Token": admin_token} + # 'X-Auth-Token': admin_token} # we're using a test token from the ini file for now conn = http_connect(self.auth_host, self.auth_port, 'GET', '/v2.0/tokens/%s' % claims, headers=headers) @@ -287,13 +287,13 @@ class AuthProtocol(object): def _expound_claims(self, claims): # Valid token. Get user data and put it in to the call # so the downstream service can use it - headers = {"Content-type": "application/json", - "Accept": "application/json", - "X-Auth-Token": self.admin_token} + headers = {'Content-type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': self.admin_token} ##TODO(ziad):we need to figure out how to auth to keystone #since validate_token is a priviledged call #Khaled's version uses creds to get a token - # "X-Auth-Token": admin_token} + # 'X-Auth-Token': admin_token} # we're using a test token from the ini file for now conn = http_connect(self.auth_host, self.auth_port, 'GET', '/v2.0/tokens/%s' % claims, headers=headers) @@ -306,12 +306,12 @@ class AuthProtocol(object): token_info = json.loads(data) roles = [] - role_refs = token_info["access"]["user"]["roles"] + role_refs = token_info['access']['user']['roles'] if role_refs != None: for role_ref in role_refs: # Nova looks for the non case-sensitive role 'Admin' # to determine admin-ness - roles.append(role_ref["name"]) + roles.append(role_ref['name']) try: tenant = token_info['access']['token']['tenant']['id'] @@ -332,12 +332,12 @@ class AuthProtocol(object): def _decorate_request(self, index, value, env, proxy_headers): """Add headers to request""" proxy_headers[index] = value - env["HTTP_%s" % index] = value + env['HTTP_%s' % index] = value def _forward_request(self, env, start_response, proxy_headers): """Token/Auth processed & claims added to headers""" self._decorate_request('AUTHORIZATION', - "Basic %s" % self.service_pass, env, proxy_headers) + 'Basic %s' % self.service_pass, env, proxy_headers) #now decide how to pass on the call if self.app: # Pass to downstream WSGI component @@ -362,7 +362,7 @@ class AuthProtocol(object): if resp.status == 401 or resp.status == 305: # Add our own headers to the list - headers = [("WWW_AUTHENTICATE", + headers = [('WWW_AUTHENTICATE', "Keystone uri='%s'" % self.auth_location)] return webob.Response(status=resp.status, body=data, @@ -387,11 +387,11 @@ def app_factory(global_conf, **local_conf): conf.update(local_conf) return AuthProtocol(None, conf) -if __name__ == "__main__": - app = deploy.loadapp("config:" + \ +if __name__ == '__main__': + app = deploy.loadapp('config:' + \ os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir, os.pardir, - "examples/paste/auth_token.ini"), - global_conf={"log_name": "auth_token.log"}) + 'examples/paste/auth_token.ini'), + global_conf={'log_name': 'auth_token.log'}) wsgi.server(eventlet.listen(('', 8090)), app) diff --git a/core.py b/core.py index 46d38673..8e206dde 100644 --- a/core.py +++ b/core.py @@ -115,15 +115,15 @@ class Debug(wsgi.Middleware): @webob.dec.wsgify def __call__(self, req): - print ("*" * 40) + " REQUEST ENVIRON" + print ('*' * 40) + ' REQUEST ENVIRON' for key, value in req.environ.items(): - print key, "=", value + print key, '=', value print resp = req.get_response(self.application) - print ("*" * 40) + " RESPONSE HEADERS" + print ('*' * 40) + ' RESPONSE HEADERS' for (key, value) in resp.headers.iteritems(): - print key, "=", value + print key, '=', value print resp.app_iter = self.print_generator(resp.app_iter) @@ -136,7 +136,7 @@ class Debug(wsgi.Middleware): Iterator that prints the contents of a wrapper string iterator when iterated. """ - print ("*" * 40) + " BODY" + print ('*' * 40) + ' BODY' for part in app_iter: sys.stdout.write(part) sys.stdout.flush() diff --git a/ec2_token.py b/ec2_token.py index af141627..cc3094a3 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -65,11 +65,11 @@ class EC2Token(wsgi.Middleware): creds_json = utils.dumps(creds) headers = {'Content-Type': 'application/json'} - # Disable "has no x member" pylint error + # 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": + if o.scheme == 'http': conn = httplib.HTTPConnection(o.netloc) else: conn = httplib.HTTPSConnection(o.netloc) diff --git a/swift_auth.py b/swift_auth.py index 35bab6a3..e83c1366 100755 --- a/swift_auth.py +++ b/swift_auth.py @@ -49,7 +49,7 @@ 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" +PROTOCOL_NAME = 'Swift Token Authentication' class AuthProtocol(object): @@ -195,9 +195,9 @@ class AuthProtocol(object): # 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} + 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, @@ -206,17 +206,17 @@ class AuthProtocol(object): data = resp.read() conn.close() - # Check http status code for the "OK" family of responses + # 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"] + role_refs = identity_info['access']['user']['roles'] if role_refs is not None: for role_ref in role_refs: - roles.append(role_ref["id"]) + roles.append(role_ref['id']) try: tenant = identity_info['access']['token']['tenantId'] From 0dfcb5b28696bee5be3cdc417eaa0b4b0f33adff Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 8 Feb 2012 14:31:41 -0800 Subject: [PATCH 014/121] Add tests for core middleware * Partially fixes bug 928039 Change-Id: I3807bcc77ab424c73069889b65b1a5598c17011c --- core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core.py b/core.py index 8e206dde..c6324f3b 100644 --- a/core.py +++ b/core.py @@ -80,10 +80,11 @@ class JsonBodyMiddleware(wsgi.Middleware): an underscore. """ - def process_request(self, request): - #if 'json' not in request.params: - # return + # Ignore unrecognized content types. Empty string indicates + # the client did not explicitly set the header + if not request.content_type in ('application/json', ''): + return params_json = request.body if not params_json: @@ -92,6 +93,9 @@ class JsonBodyMiddleware(wsgi.Middleware): params_parsed = {} try: params_parsed = json.loads(params_json) + except ValueError: + msg = "Malformed json in request body" + raise webob.exc.HTTPBadRequest(explanation=msg) finally: if not params_parsed: params_parsed = {} From 4e3873945f11edecf43e10c9c741fcf35779bd77 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 8 Feb 2012 23:37:31 +0000 Subject: [PATCH 015/121] Fixes role checking for admin check Change-Id: I6afe52033996b56aa38033017e0ce2f37c471592 --- core.py | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/core.py b/core.py index c6324f3b..09b86bbf 100644 --- a/core.py +++ b/core.py @@ -1,7 +1,8 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 import json -import webob + +import webob.exc from keystone import config from keystone.common import wsgi @@ -109,40 +110,3 @@ class JsonBodyMiddleware(wsgi.Middleware): params[k] = v request.environ[PARAMS_ENV] = params - - -class Debug(wsgi.Middleware): - """ - Middleware that produces stream debugging traces to the console (stdout) - for HTTP requests and responses flowing through it. - """ - - @webob.dec.wsgify - def __call__(self, req): - print ('*' * 40) + ' REQUEST ENVIRON' - for key, value in req.environ.items(): - print key, '=', value - print - resp = req.get_response(self.application) - - print ('*' * 40) + ' RESPONSE HEADERS' - for (key, value) in resp.headers.iteritems(): - print key, '=', value - print - - resp.app_iter = self.print_generator(resp.app_iter) - - return resp - - @staticmethod - def print_generator(app_iter): - """ - Iterator that prints the contents of a wrapper string iterator - when iterated. - """ - print ('*' * 40) + ' BODY' - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print From b551c0565fa533e0c209a5200ca76741b3888f96 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Thu, 2 Feb 2012 14:23:09 +0100 Subject: [PATCH 016/121] Add s3_token. Add s3_token middleware originally written by Akira YOSHIYAMA. Make it works with new swift_auth. Make not necessary modification to swift3 middleware. Handle errors when connecting to keystone. Address termie's comment in reviews. Change-Id: Icb2ae3fe79296ee6415dd55d146339ab72dd1d46 --- s3_token.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 s3_token.py diff --git a/s3_token.py b/s3_token.py new file mode 100644 index 00000000..202e2c27 --- /dev/null +++ b/s3_token.py @@ -0,0 +1,136 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011,2012 Akira YOSHIYAMA +# 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. + +# This source code is based ./auth_token.py and ./ec2_token.py. +# See them for their copyright. + +"""Starting point for routing S3 requests.""" + +import httplib +import json + +import webob + +from swift.common import utils as swift_utils + + +PROTOCOL_NAME = "S3 Token Authentication" + + +class S3Token(object): + """Auth Middleware that handles S3 authenticating client calls.""" + + def __init__(self, app, conf): + """Common initialization code.""" + self.app = app + self.logger = swift_utils.get_logger(conf, log_route='s3_token') + self.logger.debug('Starting the %s component' % PROTOCOL_NAME) + + # where to find the auth service (we use this to validate tokens) + self.auth_host = conf.get('auth_host') + self.auth_port = int(conf.get('auth_port')) + self.auth_protocol = conf.get('auth_protocol', 'https') + + # where to tell clients to find the auth service (default to url + # constructed based on endpoint we have for the service to use) + self.auth_location = conf.get('auth_uri', + '%s://%s:%s' % (self.auth_protocol, + self.auth_host, + self.auth_port)) + + # Credentials used to verify this component with the Auth service since + # validating tokens is a privileged call + self.admin_token = conf.get('admin_token') + + def __call__(self, environ, start_response): + """Handle incoming request. authenticate and send downstream.""" + req = webob.Request(environ) + parts = swift_utils.split_path(req.path, 1, 4, True) + version, account, container, obj = parts + + # Read request signature and access id. + if not 'Authorization' in req.headers: + return self.app(environ, start_response) + token = req.headers.get('X-Auth-Token', + req.headers.get('X-Storage-Token')) + + auth_header = req.headers['Authorization'] + access, signature = auth_header.split(' ')[-1].rsplit(':', 1) + + # Authenticate the request. + creds = {'credentials': {'access': access, + 'token': token, + 'signature': signature, + 'host': req.host, + 'verb': req.method, + 'path': req.path, + 'expire': req.headers['Date'], + }} + + creds_json = json.dumps(creds) + headers = {'Content-Type': 'application/json'} + if self.auth_protocol == 'http': + conn = httplib.HTTPConnection(self.auth_host, self.auth_port) + else: + conn = httplib.HTTPSConnection(self.auth_host, self.auth_port) + + conn.request('POST', '/v2.0/s3tokens', + body=creds_json, + headers=headers) + resp = conn.getresponse() + if resp.status < 200 or resp.status >= 300: + raise Exception('Keystone reply error: status=%s reason=%s' % ( + resp.status, + resp.reason)) + + # NOTE(vish): We could save a call to keystone by having + # keystone return token, tenant, user, and roles + # from this call. + # + # NOTE(chmou): We still have the same problem we would need to + # change token_auth to detect if we already + # identified and not doing a second query and just + # pass it through to swiftauth in this case. + # identity_info = json.loads(response) + output = resp.read() + conn.close() + identity_info = json.loads(output) + try: + token_id = str(identity_info['access']['token']['id']) + tenant = (identity_info['access']['token']['tenant']['id'], + identity_info['access']['token']['tenant']['name']) + except (KeyError, IndexError): + self.logger.debug('Error getting keystone reply: %s' % + (str(output))) + raise + + req.headers['X-Auth-Token'] = token_id + environ['PATH_INFO'] = environ['PATH_INFO'].replace( + account, 'AUTH_%s' % tenant[0]) + return self.app(environ, start_response) + + +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 S3Token(app, conf) + return auth_filter From 3e7cac0d64c36f5cb682bae03b59a01a3d4f3f7a Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Thu, 2 Feb 2012 13:15:26 +0000 Subject: [PATCH 017/121] Update swift token middleware. - Update swift middleware which add features: - Leaves authententication to token-auth. - ACL. - Container-Sync - Referer. - S3. - address termie comments on reviews. Change-Id: Iefaa228015d6286e4fb05be2cc24aec01589ef58 --- auth_token.py | 3 +- nova_auth_token.py | 3 +- swift_auth.py | 333 ++++++++++++++++++++------------------------- 3 files changed, 149 insertions(+), 190 deletions(-) mode change 100755 => 100644 swift_auth.py diff --git a/auth_token.py b/auth_token.py index 0dc723b9..4a0d501a 100755 --- a/auth_token.py +++ b/auth_token.py @@ -1,6 +1,5 @@ -#!/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"); diff --git a/nova_auth_token.py b/nova_auth_token.py index 68ad1d9d..ad6bcbb8 100644 --- a/nova_auth_token.py +++ b/nova_auth_token.py @@ -1,6 +1,5 @@ -#!/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"); diff --git a/swift_auth.py b/swift_auth.py old mode 100755 new mode 100644 index e83c1366..d12ac162 --- a/swift_auth.py +++ b/swift_auth.py @@ -1,7 +1,6 @@ -#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# -# Copyright (c) 2010-2011 OpenStack, LLC. + +# Copyright (c) 2012 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,220 +15,183 @@ # See the License for the specific language governing permissions and # limitations under the License. +import webob -""" -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.common.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 +from swift.common import utils as swift_utils +from swift.common.middleware import acl as swift_acl -PROTOCOL_NAME = 'Swift Token Authentication' +class SwiftAuth(object): + """Swift middleware to Keystone authorization system. - -class AuthProtocol(object): - """Handles authenticating and aurothrizing client calls. - - Add to your pipeline in paste config like: + In Swift's proxy-server.conf add the middleware to your pipeline:: [pipeline:main] - pipeline = catch_errors healthcheck cache keystone proxy-server + pipeline = catch_errors cache tokenauth swiftauth proxy-server - [filter:keystone] + Set account auto creation to true:: + + [app:proxy-server] + account_autocreate = true + + And add a swift authorization filter section, such as:: + + [filter:swiftauth] use = egg:keystone#swiftauth - keystone_url = http://127.0.0.1:8080 - keystone_admin_token = 999888777666 + operator_roles = admin, SwiftOperator + is_admin = true + + If Swift memcache is to be used for caching tokens, add the additional + property in the tokenauth filter: + + [filter:tokenauth] + paste.filter_factory = keystone.middleware.auth_token:filter_factory + ... + cache = swift.cache + + This maps tenants to account in Swift. + + The user whose able to give ACL / create Containers permissions + will be the one that are inside the operator_roles + setting which by default includes the Admin and the SwiftOperator + roles. + + The option is_admin if set to true will allow the + username that has the same name as the account name to be the owner. + + Example: If we have the account called hellocorp with a user + hellocorp that user will be admin on that account and can give ACL + to all other users for hellocorp. + + :param app: The next WSGI app in the pipeline + :param conf: The dict of configuration values """ - 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') + self.conf = conf + self.logger = swift_utils.get_logger(conf, log_route='keystoneauth') + self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() + self.operator_roles = conf.get('operator_roles', + 'admin, SwiftOperator') + config_is_admin = conf.get('is_admin', "false").lower() + self.is_admin = config_is_admin in ('true', 't', '1', 'on', 'yes', 'y') + cfg_synchosts = conf.get('allowed_sync_hosts', '127.0.0.1') + self.allowed_sync_hosts = [h.strip() for h in cfg_synchosts.split(',') + if h.strip()] - def __call__(self, env, start_response): - """Authenticate the incoming request. + def __call__(self, environ, start_response): + identity = self._keystone_identity(environ) - If authentication fails return an appropriate http status here, - otherwise forward through the rest of the app. - """ + if not identity: + environ['swift.authorize'] = self.denied_response + return self.app(environ, start_response) - 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) + self.logger.debug("Using identity: %r" % (identity)) + environ['keystone.identity'] = identity + environ['REMOTE_USER'] = identity.get('tenant') + environ['swift.authorize'] = self.authorize + environ['swift.clean_acl'] = swift_acl.clean_acl + return self.app(environ, 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 _keystone_identity(self, environ): + """Extract the identity from the Keystone auth component.""" + if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed': + return + roles = [] + if 'HTTP_X_ROLE' in environ: + roles = environ['HTTP_X_ROLE'].split(',') + identity = {'user': environ.get('HTTP_X_USER'), + 'tenant': (environ.get('HTTP_X_TENANT_ID'), + environ.get('HTTP_X_TENANT_NAME')), + 'roles': roles} + return identity - 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 _reseller_check(self, account, tenant_id): + """Check reseller prefix.""" + return account == '%s_%s' % (self.reseller_prefix, tenant_id) 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 + env_identity = env.get('keystone.identity', {}) + tenant = env_identity.get('tenant') - 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) + part = swift_utils.split_path(req.path, 1, 4, True) + version, account, container, obj = part except ValueError: - return HTTPNotFound(request=req) + return webob.exc.HTTPNotFound(request=req) - if obj: - return self._authorize_anon_object(req, account, container, obj) + if not self._reseller_check(account, tenant[0]): + log_msg = 'tenant mismatch: %s != %s' % (account, tenant[0]) + self.logger.debug(log_msg) + return self.denied_response(req) - if container: - return self._authorize_anon_container(req, account, container) + user_groups = env_identity.get('roles', []) - if account: - return self._authorize_anon_account(req, account) + # Check the groups the user is belonging to. If the user is + # part of the group defined in the config variable + # operator_roles (like Admin) then it will be + # promoted as an Admin of the account/tenant. + for group in self.operator_roles.split(','): + group = group.strip() + if group in user_groups: + log_msg = "allow user in group %s as account admin" % group + self.logger.debug(log_msg) + req.environ['swift_owner'] = True + return - return self._authorize_anon_toplevel(req) + # If user is of the same name of the tenant then make owner of it. + user = env_identity.get('user', '') + if self.is_admin and user == tenant[1]: + req.environ['swift_owner'] = True + return - 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) + # Allow container sync. + if (req.environ.get('swift_sync_key') + and req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None) + and 'x-timestamp' in req.headers + and (req.remote_addr in self.allowed_sync_hosts + or swift_utils.get_remote_client(req) + in self.allowed_sync_hosts)): + log_msg = 'allowing proxy %s for container-sync' % req.remote_addr + self.logger.debug(log_msg) + return - def _authorize_anon_container(self, req, account, container): - return self.unauthorized(req) + # Check if referrer is allowed. + referrers, groups = swift_acl.parse_acl(getattr(req, 'acl', None)) + if swift_acl.referrer_allowed(req.referer, referrers): + if obj or '.rlistings' in groups: + log_msg = 'authorizing %s via referer ACL' % req.referrer + self.logger.debug(log_msg) + return + return self.denied_response(req) - def _authorize_anon_account(self, req, account): - return self.unauthorized(req) + # Allow ACL at individual user level (tenant:user format) + if '%s:%s' % (tenant[0], user) in groups: + log_msg = 'user %s:%s allowed in ACL authorizing' + self.logger.debug(log_msg % (tenant[0], user)) + return - def _authorize_anon_toplevel(self, req): - return self.unauthorized(req) + # Check if we have the group in the usergroups and allow it + for user_group in user_groups: + if user_group in groups: + log_msg = 'user %s:%s allowed in ACL: %s authorizing' + self.logger.debug(log_msg % (tenant[0], user, user_group)) + return - def _get_claims(self, env): - claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - return claims + return self.denied_response(req) - def _validate_claims(self, claims): - """Ask keystone (as keystone admin) for information for this user.""" + def denied_response(self, req): + """Deny WSGI Response. - # 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 + Returns a standard WSGI response callable with the status of 403 or 401 + depending on whether the REMOTE_USER is set or not. + """ + if req.remote_user: + return webob.exc.HTTPForbidden(request=req) + else: + return webob.exc.HTTPUnauthorized(request=req) def filter_factory(global_conf, **local_conf): @@ -238,6 +200,5 @@ def filter_factory(global_conf, **local_conf): conf.update(local_conf) def auth_filter(app): - return AuthProtocol(app, conf) - + return SwiftAuth(app, conf) return auth_filter From af08e9c88c0ba9aaf25cc4bd7a30d1c661b21155 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Wed, 15 Feb 2012 09:26:54 -0800 Subject: [PATCH 018/121] Added Apache 2.0 License information. Fixes bug 932819 Change-Id: I58e0c2ad704e2e8ff1924a01791694a5e02a154b --- auth_token.py | 14 ++++++++++++++ core.py | 14 ++++++++++++++ ec2_token.py | 14 ++++++++++++++ glance_auth_token.py | 14 ++++++++++++++ nova_auth_token.py | 14 ++++++++++++++ nova_keystone_context.py | 14 ++++++++++++++ s3_token.py | 14 ++++++++++++++ swift_auth.py | 14 ++++++++++++++ 8 files changed, 112 insertions(+) mode change 100755 => 100644 auth_token.py diff --git a/auth_token.py b/auth_token.py old mode 100755 new mode 100644 index 4a0d501a..7313b1ee --- a/auth_token.py +++ b/auth_token.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/core.py b/core.py index 09b86bbf..f5ef7794 100644 --- a/core.py +++ b/core.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + import json import webob.exc diff --git a/ec2_token.py b/ec2_token.py index cc3094a3..9b402990 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. diff --git a/glance_auth_token.py b/glance_auth_token.py index 6bef1390..6403d8e3 100644 --- a/glance_auth_token.py +++ b/glance_auth_token.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright 2011 OpenStack LLC. # All Rights Reserved. # diff --git a/nova_auth_token.py b/nova_auth_token.py index ad6bcbb8..52e863c2 100644 --- a/nova_auth_token.py +++ b/nova_auth_token.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright (c) 2010-2011 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/nova_keystone_context.py b/nova_keystone_context.py index 5c41bc87..8ef459a8 100644 --- a/nova_keystone_context.py +++ b/nova_keystone_context.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright (c) 2011 OpenStack, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/s3_token.py b/s3_token.py index 202e2c27..0c5b103d 100644 --- a/s3_token.py +++ b/s3_token.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011,2012 Akira YOSHIYAMA diff --git a/swift_auth.py b/swift_auth.py index d12ac162..fcf2be09 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -1,5 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 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. + # Copyright (c) 2012 OpenStack, LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); From b9a7c4590e2ddb6cfb4f45558fea424d36ddc012 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Sun, 12 Feb 2012 21:17:57 +0100 Subject: [PATCH 019/121] Update docs for Swift and S3 middlewares. - Rename SwiftOperator variable to lowercase swiftoperator along the way. - Remove reference to swift.cache as this is not working in our tokenauth version. Change-Id: I5dfbc872f7d9d71417f45cdd0ac46c3efbe2f731 --- swift_auth.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index fcf2be09..816b32c4 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -52,22 +52,14 @@ class SwiftAuth(object): [filter:swiftauth] use = egg:keystone#swiftauth - operator_roles = admin, SwiftOperator - is_admin = true - - If Swift memcache is to be used for caching tokens, add the additional - property in the tokenauth filter: - - [filter:tokenauth] - paste.filter_factory = keystone.middleware.auth_token:filter_factory - ... - cache = swift.cache + operator_roles = admin, swiftoperator + is_admin = false This maps tenants to account in Swift. The user whose able to give ACL / create Containers permissions will be the one that are inside the operator_roles - setting which by default includes the Admin and the SwiftOperator + setting which by default includes the admin and the swiftoperator roles. The option is_admin if set to true will allow the @@ -144,8 +136,8 @@ class SwiftAuth(object): # Check the groups the user is belonging to. If the user is # part of the group defined in the config variable - # operator_roles (like Admin) then it will be - # promoted as an Admin of the account/tenant. + # operator_roles (like admin) then it will be + # promoted as an admin of the account/tenant. for group in self.operator_roles.split(','): group = group.strip() if group in user_groups: From 07726b6b17592f4002295f88dc0ce953e340365b Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 21 Feb 2012 12:19:46 -0500 Subject: [PATCH 020/121] Re-adds admin_pass/user to auth_tok middleware. Re-adds support for 'admin_user' and 'admin_password' options to the auth_token middleware. This was removed in KSL. Fixes LP bug #939015. Change-Id: Ia6eb8ccf65777175964c1c1d2e58b8de54062d67 --- auth_token.py | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/auth_token.py b/auth_token.py index 7313b1ee..0286d092 100644 --- a/auth_token.py +++ b/auth_token.py @@ -140,6 +140,8 @@ class AuthProtocol(object): # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') + self.admin_user = conf.get('admin_user') + self.admin_password = conf.get('admin_password') def __init__(self, app, conf): """ Common initialization code """ @@ -261,15 +263,42 @@ class AuthProtocol(object): return webob.exc.HTTPUnauthorized()(env, start_response) - def _validate_claims(self, claims): + def _get_admin_auth_token(self, username, password): + """ + This function gets an admin auth token to be used by this service to + validate a user's token. Validate_token is a priviledged call so + it needs to be authenticated by a service that is calling it + """ + headers = { + "Content-type": "application/json", + "Accept": "application/json"} + params = { + "auth": { + "passwordCredentials": { + "username": username, + "password": password, + } + } + } + if self.auth_protocol == "http": + conn = httplib.HTTPConnection(self.auth_host, self.auth_port) + else: + conn = httplib.HTTPSConnection(self.auth_host, self.auth_port, + cert_file=self.cert_file) + conn.request("POST", '/v2.0/tokens', json.dumps(params), + headers=headers) + response = conn.getresponse() + data = response.read() + return json.loads(data)["access"]["token"]["id"] + + def _validate_claims(self, claims, retry=True): """Validate claims, and provide identity information isf applicable """ # Step 1: We need to auth with the keystone service, so get an # admin token - #TODO(ziad): Need to properly implement this, where to store creds - # for now using token from ini - #auth = self.get_admin_auth_token('admin', 'secrete', '1') - #admin_token = json.loads(auth)['auth']['token']['id'] + if not self.admin_token: + self.admin_token = self._get_admin_auth_token(self.admin_user, + self.admin_password) # Step 2: validate the user's token with the auth service # since this is a priviledged op,m we need to auth ourselves @@ -289,8 +318,11 @@ class AuthProtocol(object): conn.close() if not str(resp.status).startswith('20'): - # Keystone rejected claim - return False + if retry: + self.admin_token = None + return self._validate_claims(env, claims, False) + else: + return False else: #TODO(Ziad): there is an optimization we can do here. We have just #received data from Keystone that we can use instead of making From 9673219dc0767e0fc88d9846df9e070350ba80f9 Mon Sep 17 00:00:00 2001 From: Shevek Date: Wed, 22 Feb 2012 16:30:32 -0800 Subject: [PATCH 021/121] Fix copyright dates and remove duplicate Apache licenses. Change-Id: I8c4b3bace0d22798d0889342662e99b24887ca85 --- auth_token.py | 16 +--------------- ec2_token.py | 18 +++--------------- glance_auth_token.py | 34 ++++++++++------------------------ nova_auth_token.py | 17 +---------------- nova_keystone_context.py | 32 ++++++++++---------------------- s3_token.py | 21 ++++----------------- 6 files changed, 29 insertions(+), 109 deletions(-) diff --git a/auth_token.py b/auth_token.py index 0286d092..b6759caa 100644 --- a/auth_token.py +++ b/auth_token.py @@ -1,20 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 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. - -# Copyright (c) 2010-2011 OpenStack, LLC. +# Copyright 2010-2012 OpenStack LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/ec2_token.py b/ec2_token.py index 9b402990..264f9f07 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -1,6 +1,9 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 OpenStack LLC +# 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 @@ -14,21 +17,6 @@ # License for the specific language governing permissions and limitations # under the License. -# 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. diff --git a/glance_auth_token.py b/glance_auth_token.py index 6403d8e3..911f5bb8 100644 --- a/glance_auth_token.py +++ b/glance_auth_token.py @@ -1,33 +1,19 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright 2011-2012 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 +# 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 +# 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. - -# Copyright 2011 OpenStack LLC. -# 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. +# 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. """ Glance Keystone Integration Middleware diff --git a/nova_auth_token.py b/nova_auth_token.py index 52e863c2..f79cae22 100644 --- a/nova_auth_token.py +++ b/nova_auth_token.py @@ -1,20 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 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. - -# Copyright (c) 2010-2011 OpenStack, LLC. +# Copyright 2010-2012 OpenStack LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,7 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - """ NOVA LAZY PROVISIONING AUTH MIDDLEWARE diff --git a/nova_keystone_context.py b/nova_keystone_context.py index 8ef459a8..2b1c82a9 100644 --- a/nova_keystone_context.py +++ b/nova_keystone_context.py @@ -1,32 +1,20 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright 2011-2012 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 +# 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 +# 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. +# 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. -# Copyright (c) 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 Auth Middleware. diff --git a/s3_token.py b/s3_token.py index 0c5b103d..8cf3e0a0 100644 --- a/s3_token.py +++ b/s3_token.py @@ -1,6 +1,10 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2012 OpenStack LLC +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011,2012 Akira YOSHIYAMA +# 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 @@ -14,23 +18,6 @@ # License for the specific language governing permissions and limitations # under the License. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011,2012 Akira YOSHIYAMA -# 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. - # This source code is based ./auth_token.py and ./ec2_token.py. # See them for their copyright. From 932e06ada773799f4826a25e8ea3c484a6f29a88 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Fri, 24 Feb 2012 14:47:44 +0000 Subject: [PATCH 022/121] Remove extraneous _validate_claims() arg. Extraneous env argument to: keystone.middleware.auth_token.AuthProtocol._validate_claims() causes glance failure when keystone auth strategy is enabled: File "/opt/stack/keystone/keystone/middleware/auth_token.py", line 309, in _validate_claims return self._validate_claims(env, claims, False) NameError: global name '\''env'\'' is not defined Seen when running devstack on Fedora16. Change-Id: Ia04c14a709e3b332002dc6cddcda56e247821fa5 --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index b6759caa..90e38d25 100644 --- a/auth_token.py +++ b/auth_token.py @@ -306,7 +306,7 @@ class AuthProtocol(object): if not str(resp.status).startswith('20'): if retry: self.admin_token = None - return self._validate_claims(env, claims, False) + return self._validate_claims(claims, False) else: return False else: From a9d25d1ce1a726b43d4cb95a06de25f6ebe4614e Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Fri, 24 Feb 2012 13:31:38 -0500 Subject: [PATCH 023/121] Fix case of admin role in middleware. Fixes LP Bug #940521. Change-Id: I1d31c805651cb633dee7efc708cd2c86bb32c3b2 --- auth_token.py | 2 +- glance_auth_token.py | 2 +- nova_auth_token.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth_token.py b/auth_token.py index 90e38d25..3ec6f7a9 100644 --- a/auth_token.py +++ b/auth_token.py @@ -340,7 +340,7 @@ class AuthProtocol(object): role_refs = token_info['access']['user']['roles'] if role_refs != None: for role_ref in role_refs: - # Nova looks for the non case-sensitive role 'Admin' + # Nova looks for the non case-sensitive role 'admin' # to determine admin-ness roles.append(role_ref['name']) diff --git a/glance_auth_token.py b/glance_auth_token.py index 911f5bb8..be69a208 100644 --- a/glance_auth_token.py +++ b/glance_auth_token.py @@ -55,7 +55,7 @@ class KeystoneContextMiddleware(context.ContextMiddleware): user = req.headers.get('X_USER') tenant = req.headers.get('X_TENANT') roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] - is_admin = 'Admin' in roles + is_admin = 'admin' in roles # Construct the context req.context = self.make_context(auth_tok=auth_tok, diff --git a/nova_auth_token.py b/nova_auth_token.py index f79cae22..d5b280c2 100644 --- a/nova_auth_token.py +++ b/nova_auth_token.py @@ -67,7 +67,7 @@ class KeystoneAuthShim(wsgi.Middleware): # 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 + is_admin = 'admin' in roles if user_ref.is_admin() != is_admin: self.auth.modify_user(user_ref, admin=is_admin) @@ -96,7 +96,7 @@ class KeystoneAuthShim(wsgi.Middleware): # Build a context, including the auth_token... ctx = context.RequestContext(user_id, project_id, - is_admin=('Admin' in roles), + is_admin=('admin' in roles), auth_token=auth_token) req.environ['nova.context'] = ctx From da4b404e6bddb6522f190aaf82a57da680f53545 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 25 Feb 2012 15:56:38 -0500 Subject: [PATCH 024/121] Update auth_token middleware so it sets X_USER_ID. Fixes LP Bug #941101. Change-Id: I67b68f6004456eb76003fdcd2ec3fb4c9b9f3bfb --- auth_token.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index 3ec6f7a9..44aa47c6 100644 --- a/auth_token.py +++ b/auth_token.py @@ -189,13 +189,16 @@ class AuthProtocol(object): # Services should use these self._decorate_request('X_TENANT_NAME', - claims.get('tenant_name', claims['tenant']), + claims.get('tenantName', claims['tenant']), env, proxy_headers) self._decorate_request('X_TENANT_ID', claims['tenant'], env, proxy_headers) self._decorate_request('X_USER', + claims['userName'], env, proxy_headers) + self._decorate_request('X_USER_ID', claims['user'], env, proxy_headers) + if 'roles' in claims and len(claims['roles']) > 0: if claims['roles'] != None: roles = '' @@ -353,7 +356,9 @@ class AuthProtocol(object): if not tenant: tenant = token_info['access']['user'].get('tenantId') tenant_name = token_info['access']['user'].get('tenantName') - verified_claims = {'user': token_info['access']['user']['username'], + verified_claims = { + 'user': token_info['access']['user']['id'], + 'userName': token_info['access']['user']['username'], 'tenant': tenant, 'roles': roles} if tenant_name: From 7af36ff5991640528c1fbe250c3d49db543e238c Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 10 Feb 2012 14:52:13 -0600 Subject: [PATCH 025/121] XML de/serialization (bug 928058) Middleware rewrites incoming XML requests as JSON, and outgoing JSON as XML, per Accept and Content-Type headers. Tests assert that core API methods support WADL/XSD specs, and cover JSON content as well. Change-Id: I6897971dd745766cbc472fd6e5346b1b34d933b0 --- core.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/core.py b/core.py index f5ef7794..19212e0c 100644 --- a/core.py +++ b/core.py @@ -19,6 +19,8 @@ import json import webob.exc from keystone import config +from keystone import exception +from keystone.common import serializer from keystone.common import wsgi @@ -109,7 +111,7 @@ class JsonBodyMiddleware(wsgi.Middleware): try: params_parsed = json.loads(params_json) except ValueError: - msg = "Malformed json in request body" + msg = 'Malformed json in request body' raise webob.exc.HTTPBadRequest(explanation=msg) finally: if not params_parsed: @@ -124,3 +126,30 @@ class JsonBodyMiddleware(wsgi.Middleware): params[k] = v request.environ[PARAMS_ENV] = params + + +class XmlBodyMiddleware(wsgi.Middleware): + """De/serializes XML to/from JSON.""" + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + self.process_request(request) + response = request.get_response(self.application) + self.process_response(request, response) + return response + + def process_request(self, request): + """Transform the request from XML to JSON.""" + incoming_xml = 'application/xml' in str(request.content_type) + if incoming_xml and request.body: + request.content_type = 'application/json' + request.body = json.dumps(serializer.from_xml(request.body)) + + def process_response(self, request, response): + """Transform the response from JSON to XML.""" + outgoing_xml = 'application/xml' in str(request.accept) + if outgoing_xml and response.body: + response.content_type = 'application/xml' + try: + response.body = serializer.to_xml(json.loads(response.body)) + except: + raise exception.Error(message=response.body) From ee0f3283e82addd8530c74fd360e5fd5479bdec1 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Wed, 22 Feb 2012 22:28:42 -0500 Subject: [PATCH 026/121] Set tenantName to 'admin' in get_admin_auth_token. Sets the tenantName to 'admin' in get_admin_auth_token. This is required because user-only roles are currently not supported. Give that wsgi is hard coded to check for 'role:admin' this seems to be a reasonable thing to do. In the future it would be nice to add a custom admin_role setting in the config file so the role wouldn't be hard coded to 'admin'. Also removes unused version of get_admin_auth_token. Fixes LP Bug #939015. Change-Id: I545b458e31c8a44a5a69cad1e875f0fe02956246 --- auth_token.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/auth_token.py b/auth_token.py index 44aa47c6..5b726bad 100644 --- a/auth_token.py +++ b/auth_token.py @@ -77,6 +77,7 @@ from webob.exc import HTTPUnauthorized from keystone.common.bufferedhttp import http_connect_raw as http_connect +ADMIN_TENANTNAME = 'admin' PROTOCOL_NAME = 'Token Authentication' @@ -215,26 +216,6 @@ class AuthProtocol(object): #Send request downstream return self._forward_request(env, start_response, proxy_headers) - # NOTE(todd): unused - def get_admin_auth_token(self, username, password): - """ - This function gets an admin auth token to be used by this service to - validate a user's token. Validate_token is a priviledged call so - it needs to be authenticated by a service that is calling it - """ - headers = {'Content-type': 'application/json', - 'Accept': 'application/json'} - params = {'passwordCredentials': {'username': username, - 'password': password, - 'tenantId': '1'}} - conn = httplib.HTTPConnection('%s:%s' \ - % (self.auth_host, self.auth_port)) - conn.request('POST', '/v2.0/tokens', json.dumps(params), \ - headers=headers) - response = conn.getresponse() - data = response.read() - return data - def _get_claims(self, env): """Get claims from request""" claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) @@ -266,7 +247,8 @@ class AuthProtocol(object): "passwordCredentials": { "username": username, "password": password, - } + }, + "tenantName": ADMIN_TENANTNAME, } } if self.auth_protocol == "http": From 7063ca4012f14c9bab85fff2ae54a1942a5f7f48 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 28 Feb 2012 00:08:07 -0600 Subject: [PATCH 027/121] Provide request to Middleware.process_response() It appears that no middleware has taken advantage of the builtin process_response(response) convention, because a reference to the original request is typically necessary to build an appropriate response. Change-Id: If032261974eb1d756abdbd5b18892091978e2a07 --- core.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core.py b/core.py index 19212e0c..7faa3f01 100644 --- a/core.py +++ b/core.py @@ -130,12 +130,6 @@ class JsonBodyMiddleware(wsgi.Middleware): class XmlBodyMiddleware(wsgi.Middleware): """De/serializes XML to/from JSON.""" - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, request): - self.process_request(request) - response = request.get_response(self.application) - self.process_response(request, response) - return response def process_request(self, request): """Transform the request from XML to JSON.""" @@ -153,3 +147,4 @@ class XmlBodyMiddleware(wsgi.Middleware): response.body = serializer.to_xml(json.loads(response.body)) except: raise exception.Error(message=response.body) + return response From 36bdaab59e5d85e7d1e5757b9957dc5506a54a69 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Tue, 28 Feb 2012 11:56:24 -0500 Subject: [PATCH 028/121] Handle KeyError in _get_admin_auth_token. Updates the _get_admin_auth_token so that it more gracefully handles KeyError's. Also adds a conn.close to the _get_admin_auth_token method. Fixes LP Bug #942247. Change-Id: I2826f1b02bc7fd46c02ab3d15d88f76b9a8457f0 --- auth_token.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 5b726bad..1dd437c8 100644 --- a/auth_token.py +++ b/auth_token.py @@ -260,7 +260,11 @@ class AuthProtocol(object): headers=headers) response = conn.getresponse() data = response.read() - return json.loads(data)["access"]["token"]["id"] + conn.close() + try: + return json.loads(data)["access"]["token"]["id"] + except KeyError: + return None def _validate_claims(self, claims, retry=True): """Validate claims, and provide identity information isf applicable """ From c1a5196052f059452516488c043b194b69667470 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Wed, 22 Feb 2012 13:41:13 +0800 Subject: [PATCH 029/121] Unpythonic code in redux in auth_token.py Fixed bug #932578 Fixes code which are not pythonic and not pep8 standard. Change-Id: Idd8ba4a75ad0a854a60238aec2d8d32ff2ee9c53 --- auth_token.py | 194 +++++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 82 deletions(-) diff --git a/auth_token.py b/auth_token.py index 1dd437c8..c323565e 100644 --- a/auth_token.py +++ b/auth_token.py @@ -120,9 +120,9 @@ class AuthProtocol(object): # where to tell clients to find the auth service (default to url # constructed based on endpoint we have for the service to use) self.auth_location = conf.get('auth_uri', - '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port)) + '%s://%s:%s' % (self.auth_protocol, + self.auth_host, + self.auth_port)) # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call @@ -143,7 +143,7 @@ class AuthProtocol(object): #Prep headers to forward request to local or remote downstream service proxy_headers = env.copy() for header in proxy_headers.iterkeys(): - if header[0:5] == 'HTTP_': + if header.startswith('HTTP_'): proxy_headers[header[5:]] = proxy_headers[header] del proxy_headers[header] @@ -155,7 +155,9 @@ class AuthProtocol(object): #Configured to allow downstream service to make final decision. #So mark status as Invalid and forward the request downstream self._decorate_request('X_IDENTITY_STATUS', - 'Invalid', env, proxy_headers) + 'Invalid', + env, + proxy_headers) else: #Respond to client as appropriate for this auth protocol return self._reject_request(env, start_response) @@ -167,13 +169,17 @@ class AuthProtocol(object): if self.delay_auth_decision: # Downstream service will receive call still and decide self._decorate_request('X_IDENTITY_STATUS', - 'Invalid', env, proxy_headers) + 'Invalid', + env, + proxy_headers) else: #Respond to client as appropriate for this auth protocol return self._reject_claims(env, start_response) else: self._decorate_request('X_IDENTITY_STATUS', - 'Confirmed', env, proxy_headers) + 'Confirmed', + env, + proxy_headers) #Collect information about valid claims if valid: @@ -181,34 +187,44 @@ class AuthProtocol(object): # Store authentication data if claims: - self._decorate_request('X_AUTHORIZATION', 'Proxy %s' % - claims['user'], env, proxy_headers) + self._decorate_request('X_AUTHORIZATION', + 'Proxy %s' % claims['user'], + env, + proxy_headers) # For legacy compatibility before we had ID and Name self._decorate_request('X_TENANT', - claims['tenant'], env, proxy_headers) + claims['tenant'], + env, + proxy_headers) # Services should use these self._decorate_request('X_TENANT_NAME', - claims.get('tenantName', claims['tenant']), - env, proxy_headers) + claims.get('tenantName', + claims['tenant']), + env, + proxy_headers) self._decorate_request('X_TENANT_ID', - claims['tenant'], env, proxy_headers) + claims['tenant'], + env, + proxy_headers) self._decorate_request('X_USER', - claims['userName'], env, proxy_headers) + claims['userName'], + env, + proxy_headers) self._decorate_request('X_USER_ID', - claims['user'], env, proxy_headers) + claims['user'], + env, + proxy_headers) - if 'roles' in claims and len(claims['roles']) > 0: - if claims['roles'] != None: - roles = '' - for role in claims['roles']: - if len(roles) > 0: - roles += ',' - roles += role - self._decorate_request('X_ROLE', - roles, env, proxy_headers) + # NOTE(lzyeval): claims has a key 'roles' which is + # guaranteed to be a list (see note below) + roles = ','.join(filter(lambda x: x, claims['roles'])) + self._decorate_request('X_ROLE', + roles, + env, + proxy_headers) # NOTE(todd): unused self.expanded = True @@ -223,15 +239,15 @@ class AuthProtocol(object): def _reject_request(self, env, start_response): """Redirect client to auth server""" - return webob.exc.HTTPUnauthorized('Authentication required', - [('WWW-Authenticate', - "Keystone uri='%s'" % self.auth_location)])(env, - start_response) + headers = [('WWW-Authenticate', + "Keystone uri='%s'" % self.auth_location)] + resp = webob.exc.HTTPUnauthorized('Authentication required', headers) + return resp(env, start_response) def _reject_claims(self, env, start_response): """Client sent bad claims""" - return webob.exc.HTTPUnauthorized()(env, - start_response) + resp = webob.exc.HTTPUnauthorized() + return resp(env, start_response) def _get_admin_auth_token(self, username, password): """ @@ -241,23 +257,27 @@ class AuthProtocol(object): """ headers = { "Content-type": "application/json", - "Accept": "application/json"} + "Accept": "application/json", + } params = { - "auth": { - "passwordCredentials": { + "auth": { + "passwordCredentials": { "username": username, "password": password, }, - "tenantName": ADMIN_TENANTNAME, - } - } + "tenantName": ADMIN_TENANTNAME, + } + } if self.auth_protocol == "http": conn = httplib.HTTPConnection(self.auth_host, self.auth_port) else: - conn = httplib.HTTPSConnection(self.auth_host, self.auth_port, - cert_file=self.cert_file) - conn.request("POST", '/v2.0/tokens', json.dumps(params), - headers=headers) + conn = httplib.HTTPSConnection(self.auth_host, + self.auth_port, + cert_file=self.cert_file) + conn.request("POST", + '/v2.0/tokens', + json.dumps(params), + headers=headers) response = conn.getresponse() data = response.read() conn.close() @@ -278,16 +298,21 @@ class AuthProtocol(object): # Step 2: validate the user's token with the auth service # since this is a priviledged op,m we need to auth ourselves # by using an admin token - headers = {'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Token': self.admin_token} - ##TODO(ziad):we need to figure out how to auth to keystone - #since validate_token is a priviledged call - #Khaled's version uses creds to get a token - # 'X-Auth-Token': admin_token} - # we're using a test token from the ini file for now - conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/v2.0/tokens/%s' % claims, headers=headers) + headers = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': self.admin_token, + } + ##TODO(ziad):we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # 'X-Auth-Token': admin_token} + # we're using a test token from the ini file for now + conn = http_connect(self.auth_host, + self.auth_port, + 'GET', + '/v2.0/tokens/%s' % claims, + headers=headers) resp = conn.getresponse() # data = resp.read() conn.close() @@ -307,16 +332,21 @@ class AuthProtocol(object): def _expound_claims(self, claims): # Valid token. Get user data and put it in to the call # so the downstream service can use it - headers = {'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Token': self.admin_token} - ##TODO(ziad):we need to figure out how to auth to keystone - #since validate_token is a priviledged call - #Khaled's version uses creds to get a token - # 'X-Auth-Token': admin_token} - # we're using a test token from the ini file for now - conn = http_connect(self.auth_host, self.auth_port, 'GET', - '/v2.0/tokens/%s' % claims, headers=headers) + headers = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + 'X-Auth-Token': self.admin_token, + } + ##TODO(ziad):we need to figure out how to auth to keystone + #since validate_token is a priviledged call + #Khaled's version uses creds to get a token + # 'X-Auth-Token': admin_token} + # we're using a test token from the ini file for now + conn = http_connect(self.auth_host, + self.auth_port, + 'GET', + '/v2.0/tokens/%s' % claims, + headers=headers) resp = conn.getresponse() data = resp.read() conn.close() @@ -325,28 +355,28 @@ class AuthProtocol(object): raise LookupError('Unable to locate claims: %s' % resp.status) token_info = json.loads(data) - roles = [] - role_refs = token_info['access']['user']['roles'] - if role_refs != None: - for role_ref in role_refs: - # Nova looks for the non case-sensitive role 'admin' - # to determine admin-ness - roles.append(role_ref['name']) + access_user = token_info['access']['user'] + access_token = token_info['access']['token'] + # Nova looks for the non case-sensitive role 'admin' + # to determine admin-ness + # NOTE(lzyeval): roles is always a list + roles = map(lambda y: y['name'], access_user.get('roles', [])) try: - tenant = token_info['access']['token']['tenant']['id'] - tenant_name = token_info['access']['token']['tenant']['name'] + tenant = access_token['tenant']['id'] + tenant_name = access_token['tenant']['name'] except: tenant = None tenant_name = None if not tenant: - tenant = token_info['access']['user'].get('tenantId') - tenant_name = token_info['access']['user'].get('tenantName') + tenant = access_user.get('tenantId') + tenant_name = access_user.get('tenantName') verified_claims = { - 'user': token_info['access']['user']['id'], - 'userName': token_info['access']['user']['username'], - 'tenant': tenant, - 'roles': roles} + 'user': access_user['id'], + 'userName': access_user['username'], + 'tenant': tenant, + 'roles': roles, + } if tenant_name: verified_claims['tenantName'] = tenant_name return verified_claims @@ -385,7 +415,7 @@ class AuthProtocol(object): if resp.status == 401 or resp.status == 305: # Add our own headers to the list headers = [('WWW_AUTHENTICATE', - "Keystone uri='%s'" % self.auth_location)] + "Keystone uri='%s'" % self.auth_location)] return webob.Response(status=resp.status, body=data, headerlist=headers)(env, start_response) @@ -410,10 +440,10 @@ def app_factory(global_conf, **local_conf): return AuthProtocol(None, conf) if __name__ == '__main__': - app = deploy.loadapp('config:' + \ - os.path.join(os.path.abspath(os.path.dirname(__file__)), - os.pardir, - os.pardir, - 'examples/paste/auth_token.ini'), - global_conf={'log_name': 'auth_token.log'}) + app_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), + os.pardir, + os.pardir, + 'examples/paste/auth_token.ini') + app = deploy.loadapp('config:%s' % app_path, + global_conf={'log_name': 'auth_token.log'}) wsgi.server(eventlet.listen(('', 8090)), app) From b13302745aa63ee782a1053b10da768979324592 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 28 Feb 2012 21:05:17 -0800 Subject: [PATCH 030/121] improve auth_token middleware * remove ability to run auth_token as stand-alone proxy service * only validate a token once * improved error handling & comments where further improvement needed * improved admin_token logic * resolved bug 942984 and bug 942985 Change-Id: I12ae25c9d8047862072b7ebea1a98722eae1f40d --- auth_token.py | 639 +++++++++++++++++++++++++------------------------- 1 file changed, 316 insertions(+), 323 deletions(-) diff --git a/auth_token.py b/auth_token.py index c323565e..a6673159 100644 --- a/auth_token.py +++ b/auth_token.py @@ -18,18 +18,17 @@ """ TOKEN-BASED AUTH MIDDLEWARE -This WSGI component performs multiple jobs: +This WSGI component: -* it verifies that incoming client requests have valid tokens by verifying +* Verifies that incoming client requests have valid tokens by validating tokens with the auth service. -* it will reject unauthenticated requests UNLESS it is in 'delay_auth_decision' +* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' mode, which means the final decision is delegated to the downstream WSGI component (usually the OpenStack service) -* it will collect and forward identity information from a valid token - such as user name etc... - -Refer to: http://wiki.openstack.org/openstack-authn +* Collects and forwards identity information based on a valid token + such as user name, tenant, etc +Refer to: http://keystone.openstack.org/middleware_architecture.html HEADERS ------- @@ -41,17 +40,18 @@ Coming in from initial call from client or customer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ HTTP_X_AUTH_TOKEN - the client token being passed in + The client token being passed in. HTTP_X_STORAGE_TOKEN - the client token being passed in (legacy Rackspace use) to support - cloud files + The client token being passed in (legacy Rackspace use) to support + swift/cloud files Used for communication between components ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -www-authenticate - only used if this component is being used remotely +WWW-Authenticate + HTTP header returned to a user indicating which endpoint to use + to retrieve a new token HTTP_AUTHORIZATION basic auth password used to validate the connection @@ -60,368 +60,370 @@ What we add to the request for use by the OpenStack service ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ HTTP_X_AUTHORIZATION - the client identity being passed in + The client identity being passed in + +HTTP_X_IDENTITY_STATUS + 'Confirmed' or 'Invalid' + The underlying service will only see a value of 'Invalid' if the Middleware + is configured to run in 'delay_auth_decision' mode + +HTTP_X_TENANT_ID + Identity service managed unique identifier, string + +HTTP_X_TENANT_NAME + Unique tenant identifier, string + +HTTP_X_USER_ID + Identity-service managed unique identifier, string + +HTTP_X_USER_NAME + Unique user identifier, string + +HTTP_X_ROLES + Comma delimited list of case-sensitive Roles + +HTTP_X_TENANT + *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME + Keystone-assigned unique identifier, deprecated + +HTTP_X_USER + *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME + Unique user name, string + +HTTP_X_ROLE + *Deprecated* in favor of HTTP_X_ROLES + This is being renamed, and the new header contains the same data. """ + import httplib import json -import os +import logging -import eventlet -from eventlet import wsgi -from paste import deploy -from urlparse import urlparse import webob import webob.exc -from webob.exc import HTTPUnauthorized -from keystone.common.bufferedhttp import http_connect_raw as http_connect -ADMIN_TENANTNAME = 'admin' -PROTOCOL_NAME = 'Token Authentication' +logger = logging.getLogger('keystone.middleware.auth_token') + + +class InvalidUserToken(Exception): + pass + + +class ServiceError(Exception): + pass class AuthProtocol(object): - """Auth Middleware that handles authenticating client calls""" - - def _init_protocol_common(self, app, conf): - """ Common initialization code""" - print 'Starting the %s component' % PROTOCOL_NAME + """Auth Middleware that handles authenticating client calls.""" + def __init__(self, app, conf): + logger.info('Starting keystone auth_token middleware') self.conf = conf self.app = app - #if app is set, then we are in a WSGI pipeline and requests get passed - # on to app. If it is not set, this component should forward requests - - # where to find the OpenStack service (if not in local WSGI chain) - # these settings are only used if this component is acting as a proxy - # and the OpenSTack service is running remotely - self.service_protocol = conf.get('service_protocol', 'https') - self.service_host = conf.get('service_host') - self.service_port = int(conf.get('service_port')) - self.service_url = '%s://%s:%s' % (self.service_protocol, - self.service_host, - self.service_port) - # used to verify this component with the OpenStack service or PAPIAuth - self.service_pass = conf.get('service_pass') # delay_auth_decision means we still allow unauthenticated requests # through and we let the downstream service make the final decision self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) - def _init_protocol(self, conf): - """ Protocol specific initialization """ - # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port')) - self.auth_protocol = conf.get('auth_protocol', 'https') - # where to tell clients to find the auth service (default to url - # constructed based on endpoint we have for the service to use) - self.auth_location = conf.get('auth_uri', - '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port)) + auth_protocol = conf.get('auth_protocol', 'https') + if auth_protocol == 'http': + self.http_client_class = httplib.HTTPConnection + else: + self.http_client_class = httplib.HTTPSConnection + + default_auth_uri = '%s://%s:%s' % (auth_protocol, + self.auth_host, + self.auth_port) + self.auth_uri = conf.get('auth_uri', default_auth_uri) # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') self.admin_user = conf.get('admin_user') self.admin_password = conf.get('admin_password') - - def __init__(self, app, conf): - """ Common initialization code """ - - #TODO(ziad): maybe we refactor this into a superclass - self._init_protocol_common(app, conf) # Applies to all protocols - self._init_protocol(conf) # Specific to this protocol + self.admin_tenant_name = conf.get('admin_tenant_name', 'admin') def __call__(self, env, start_response): - """ Handle incoming request. Authenticate. And send downstream. """ + """Handle incoming request. - #Prep headers to forward request to local or remote downstream service - proxy_headers = env.copy() - for header in proxy_headers.iterkeys(): - if header.startswith('HTTP_'): - proxy_headers[header[5:]] = proxy_headers[header] - del proxy_headers[header] + Authenticate send downstream on success. Reject request if + we can't authenticate. - #Look for authentication claims - claims = self._get_claims(env) - if not claims: - #No claim(s) provided + """ + logger.debug('Authenticating user token') + try: + self._remove_auth_headers(env) + user_token = self._get_user_token_from_header(env) + token_info = self._validate_user_token(user_token) + user_headers = self._build_user_headers(token_info) + self._add_headers(env, user_headers) + return self.app(env, start_response) + + except InvalidUserToken: if self.delay_auth_decision: - #Configured to allow downstream service to make final decision. - #So mark status as Invalid and forward the request downstream - self._decorate_request('X_IDENTITY_STATUS', - 'Invalid', - env, - proxy_headers) + logger.info('Invalid user token - deferring reject downstream') + self._add_headers(env, {'X-Identity-Status': 'Invalid'}) + return self.app(env, start_response) else: - #Respond to client as appropriate for this auth protocol + logger.info('Invalid user token - rejecting request') return self._reject_request(env, start_response) + + except ServiceError, e: + logger.critical('Unable to obtain admin token: %s' % e) + resp = webob.exc.HTTPServiceUnavailable() + return resp(env, start_response) + + def _remove_auth_headers(self, env): + """Remove headers so a user can't fake authentication. + + :param env: wsgi request environment + + """ + auth_headers = ( + 'X-Identity-Status', + 'X-Tenant-Id', + 'X-Tenant-Name', + 'X-User-Id', + 'X-User-Name', + 'X-Roles', + # Deprecated + 'X-User', + 'X-Tenant', + 'X-Role', + ) + logger.debug('Removing headers from request environment: %s' % + ','.join(auth_headers)) + self._remove_headers(env, auth_headers) + + def _get_user_token_from_header(self, env): + """Get token id from request. + + :param env: wsgi request environment + :return token id + :raises InvalidUserToken if no token is provided in request + + """ + token = self._get_header(env, 'X-Auth-Token', + self._get_header(env, 'X-Storage-Token')) + if token: + return token else: - # this request is presenting claims. Let's validate them - valid = self._validate_claims(claims) - if not valid: - # Keystone rejected claim - if self.delay_auth_decision: - # Downstream service will receive call still and decide - self._decorate_request('X_IDENTITY_STATUS', - 'Invalid', - env, - proxy_headers) - else: - #Respond to client as appropriate for this auth protocol - return self._reject_claims(env, start_response) - else: - self._decorate_request('X_IDENTITY_STATUS', - 'Confirmed', - env, - proxy_headers) - - #Collect information about valid claims - if valid: - claims = self._expound_claims(claims) - - # Store authentication data - if claims: - self._decorate_request('X_AUTHORIZATION', - 'Proxy %s' % claims['user'], - env, - proxy_headers) - - # For legacy compatibility before we had ID and Name - self._decorate_request('X_TENANT', - claims['tenant'], - env, - proxy_headers) - - # Services should use these - self._decorate_request('X_TENANT_NAME', - claims.get('tenantName', - claims['tenant']), - env, - proxy_headers) - self._decorate_request('X_TENANT_ID', - claims['tenant'], - env, - proxy_headers) - - self._decorate_request('X_USER', - claims['userName'], - env, - proxy_headers) - self._decorate_request('X_USER_ID', - claims['user'], - env, - proxy_headers) - - # NOTE(lzyeval): claims has a key 'roles' which is - # guaranteed to be a list (see note below) - roles = ','.join(filter(lambda x: x, claims['roles'])) - self._decorate_request('X_ROLE', - roles, - env, - proxy_headers) - - # NOTE(todd): unused - self.expanded = True - - #Send request downstream - return self._forward_request(env, start_response, proxy_headers) - - def _get_claims(self, env): - """Get claims from request""" - claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) - return claims + raise InvalidUserToken('Unable to find token in headers') def _reject_request(self, env, start_response): - """Redirect client to auth server""" - headers = [('WWW-Authenticate', - "Keystone uri='%s'" % self.auth_location)] + """Redirect client to auth server. + + :param env: wsgi request environment + :param start_response: wsgi response callback + :returns HTTPUnauthorized http response + + """ + headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] resp = webob.exc.HTTPUnauthorized('Authentication required', headers) return resp(env, start_response) - def _reject_claims(self, env, start_response): - """Client sent bad claims""" - resp = webob.exc.HTTPUnauthorized() - return resp(env, start_response) + def get_admin_token(self): + """Return admin token, possibly fetching a new one. + + :return admin token id + :raise ServiceError when unable to retrieve token from keystone - def _get_admin_auth_token(self, username, password): """ - This function gets an admin auth token to be used by this service to - validate a user's token. Validate_token is a priviledged call so - it needs to be authenticated by a service that is calling it - """ - headers = { - "Content-type": "application/json", - "Accept": "application/json", - } - params = { - "auth": { - "passwordCredentials": { - "username": username, - "password": password, - }, - "tenantName": ADMIN_TENANTNAME, - } - } - if self.auth_protocol == "http": - conn = httplib.HTTPConnection(self.auth_host, self.auth_port) - else: - conn = httplib.HTTPSConnection(self.auth_host, - self.auth_port, - cert_file=self.cert_file) - conn.request("POST", - '/v2.0/tokens', - json.dumps(params), - headers=headers) - response = conn.getresponse() - data = response.read() - conn.close() - try: - return json.loads(data)["access"]["token"]["id"] - except KeyError: - return None - - def _validate_claims(self, claims, retry=True): - """Validate claims, and provide identity information isf applicable """ - - # Step 1: We need to auth with the keystone service, so get an - # admin token if not self.admin_token: - self.admin_token = self._get_admin_auth_token(self.admin_user, - self.admin_password) + self.admin_token = self._request_admin_token() - # Step 2: validate the user's token with the auth service - # since this is a priviledged op,m we need to auth ourselves - # by using an admin token - headers = { - 'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Token': self.admin_token, - } - ##TODO(ziad):we need to figure out how to auth to keystone - #since validate_token is a priviledged call - #Khaled's version uses creds to get a token - # 'X-Auth-Token': admin_token} - # we're using a test token from the ini file for now - conn = http_connect(self.auth_host, - self.auth_port, - 'GET', - '/v2.0/tokens/%s' % claims, - headers=headers) - resp = conn.getresponse() - # data = resp.read() - conn.close() + return self.admin_token - if not str(resp.status).startswith('20'): - if retry: - self.admin_token = None - return self._validate_claims(claims, False) - else: - return False - else: - #TODO(Ziad): there is an optimization we can do here. We have just - #received data from Keystone that we can use instead of making - #another call in _expound_claims - return True + def _get_http_connection(self): + return self.http_client_class(self.auth_host, self.auth_port) - def _expound_claims(self, claims): - # Valid token. Get user data and put it in to the call - # so the downstream service can use it - headers = { - 'Content-type': 'application/json', - 'Accept': 'application/json', - 'X-Auth-Token': self.admin_token, - } - ##TODO(ziad):we need to figure out how to auth to keystone - #since validate_token is a priviledged call - #Khaled's version uses creds to get a token - # 'X-Auth-Token': admin_token} - # we're using a test token from the ini file for now - conn = http_connect(self.auth_host, - self.auth_port, - 'GET', - '/v2.0/tokens/%s' % claims, - headers=headers) - resp = conn.getresponse() - data = resp.read() - conn.close() + def _json_request(self, method, path, body=None, additional_headers=None): + """HTTP request helper used to make json requests. - if not str(resp.status).startswith('20'): - raise LookupError('Unable to locate claims: %s' % resp.status) + :param method: http method + :param path: relative request url + :param body: dict to encode to json as request body. Optional. + :param additional_headers: dict of additional headers to send with + http request. Optional. + :return (http response object, response body parsed as json) + :raise ServerError when unable to communicate with keystone - token_info = json.loads(data) - access_user = token_info['access']['user'] - access_token = token_info['access']['token'] - # Nova looks for the non case-sensitive role 'admin' - # to determine admin-ness - # NOTE(lzyeval): roles is always a list - roles = map(lambda y: y['name'], access_user.get('roles', [])) + """ + conn = self._get_http_connection() + + kwargs = { + 'headers': { + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + } + + if additional_headers: + kwargs['headers'].update(additional_headers) + + if body: + kwargs['body'] = json.dumps(body) try: - tenant = access_token['tenant']['id'] - tenant_name = access_token['tenant']['name'] - except: - tenant = None - tenant_name = None - if not tenant: - tenant = access_user.get('tenantId') - tenant_name = access_user.get('tenantName') - verified_claims = { - 'user': access_user['id'], - 'userName': access_user['username'], - 'tenant': tenant, - 'roles': roles, + conn.request(method, path, **kwargs) + response = conn.getresponse() + body = response.read() + data = json.loads(body) + except Exception, e: + logger.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + return response, data + + def _request_admin_token(self): + """Retrieve new token as admin user from keystone. + + :return token id upon success + :raises ServerError when unable to communicate with keystone + + """ + params = { + 'auth': { + 'passwordCredentials': { + 'username': self.admin_user, + 'password': self.admin_password, + }, + 'tenantName': self.admin_tenant_name, } - if tenant_name: - verified_claims['tenantName'] = tenant_name - return verified_claims + } - def _decorate_request(self, index, value, env, proxy_headers): - """Add headers to request""" - proxy_headers[index] = value - env['HTTP_%s' % index] = value + response, data = self._json_request('POST', + '/v2.0/tokens', + body=params) - def _forward_request(self, env, start_response, proxy_headers): - """Token/Auth processed & claims added to headers""" - self._decorate_request('AUTHORIZATION', - 'Basic %s' % self.service_pass, env, proxy_headers) - #now decide how to pass on the call - if self.app: - # Pass to downstream WSGI component - return self.app(env, start_response) - #.custom_start_response) + try: + token = data['access']['token']['id'] + assert token + return token + except (AssertionError, KeyError): + raise ServiceError('invalid json response') + + def _validate_user_token(self, user_token, retry=True): + """Authenticate user token with keystone. + + :param user_token: user's token id + :param retry: flag that forces the middleware to retry + user authentication when an indeterminate + response is received. Optional. + :return token object received from keystone on success + :raise InvalidUserToken if token is rejected + :raise ServiceError if unable to authenticate token + + """ + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', + '/v2.0/tokens/%s' % user_token, + additional_headers=headers) + + if response.status == 200: + return data + if response.status == 404: + # FIXME(ja): I'm assuming the 404 status means that user_token is + # invalid - not that the admin_token is invalid + raise InvalidUserToken('Token authorization failed') + if response.status == 401: + logger.info('Keystone rejected admin token, resetting') + self.admin_token = None else: - # We are forwarding to a remote service (no downstream WSGI app) - req = webob.Request(proxy_headers) - parsed = urlparse(req.url) + logger.error('Bad response code while validating token: %s' % + response.status) + if retry: + logger.info('Retrying validation') + return self._validate_user_token(user_token, False) + else: + raise InvalidUserToken() - conn = http_connect(self.service_host, - self.service_port, - req.method, - parsed.path, - proxy_headers, - ssl=(self.service_protocol == 'https')) - resp = conn.getresponse() - data = resp.read() + def _build_user_headers(self, token_info): + """Convert token object into headers. - #TODO(ziad): use a more sophisticated proxy - # we are rewriting the headers now + Build headers that represent authenticated user: + * X_IDENTITY_STATUS: Confirmed or Invalid + * X_TENANT_ID: id of tenant if tenant is present + * X_TENANT_NAME: name of tenant if tenant is present + * X_USER_ID: id of user + * X_USER_NAME: name of user + * X_ROLES: list of roles - if resp.status == 401 or resp.status == 305: - # Add our own headers to the list - headers = [('WWW_AUTHENTICATE', - "Keystone uri='%s'" % self.auth_location)] - return webob.Response(status=resp.status, - body=data, - headerlist=headers)(env, start_response) - else: - return webob.Response(status=resp.status, - body=data)(env, start_response) + Additional (deprecated) headers include: + * X_USER: name of user + * X_TENANT: For legacy compatibility before we had ID and Name + * X_ROLE: list of roles + + :param token_info: token object returned by keystone on authentication + :raise InvalidUserToken when unable to parse token object + + """ + user = token_info['access']['user'] + token = token_info['access']['token'] + roles = ','.join([role['name'] for role in user.get('roles', [])]) + + # FIXME(ja): I think we are checking in both places because: + # tenant might not be returned, and there was a pre-release + # that put tenant objects inside the user object? + try: + tenant_id = token['tenant']['id'] + tenant_name = token['tenant']['name'] + except: + tenant_id = user.get('tenantId') + tenant_name = user.get('tenantName') + + user_id = user['id'] + user_name = user['username'] + + return { + 'X-Identity-Status': 'Confirmed', + 'X-Tenant-Id': tenant_id, + 'X-Tenant-Name': tenant_name, + 'X-User-Id': user_id, + 'X-User-Name': user_name, + 'X-Roles': roles, + # Deprecated + 'X-User': user_name, + 'X-Tenant': tenant_name, + 'X-Role': roles, + } + + def _header_to_env_var(self, key): + """Convert header to wsgi env variable. + + :param key: http header name (ex. 'X-Auth-Token') + :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') + + """ + return 'HTTP_%s' % key.replace('-', '_').upper() + + def _add_headers(self, env, headers): + """Add http headers to environment.""" + for (k, v) in headers.iteritems(): + env_key = self._header_to_env_var(k) + env[env_key] = v + + def _remove_headers(self, env, keys): + """Remove http headers from environment.""" + for k in keys: + env_key = self._header_to_env_var(k) + try: + del env[env_key] + except KeyError: + pass + + def _get_header(self, env, key, default=None): + """Get http header from environment.""" + env_key = self._header_to_env_var(key) + return env.get(env_key, default) def filter_factory(global_conf, **local_conf): @@ -438,12 +440,3 @@ def app_factory(global_conf, **local_conf): conf = global_conf.copy() conf.update(local_conf) return AuthProtocol(None, conf) - -if __name__ == '__main__': - app_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), - os.pardir, - os.pardir, - 'examples/paste/auth_token.ini') - app = deploy.loadapp('config:%s' % app_path, - global_conf={'log_name': 'auth_token.log'}) - wsgi.server(eventlet.listen(('', 8090)), app) From 73c149875e49fad591bd8eee35fe019627cc35b1 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Mon, 13 Feb 2012 23:29:49 +0000 Subject: [PATCH 031/121] Add reseller admin capability. - A user with the reseller admin role will be able to access to every other accounts. - Rename name groups to roles. Change-Id: I8e86d8280a8fcdefbd4f9386bec11afdad797167 --- swift_auth.py | 45 +++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 816b32c4..9bb38597 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -78,7 +78,9 @@ class SwiftAuth(object): self.logger = swift_utils.get_logger(conf, log_route='keystoneauth') self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() self.operator_roles = conf.get('operator_roles', - 'admin, SwiftOperator') + 'admin, swiftoperator') + self.reseller_admin_role = conf.get('reseller_admin_role', + 'ResellerAdmin') config_is_admin = conf.get('is_admin', "false").lower() self.is_admin = config_is_admin in ('true', 't', '1', 'on', 'yes', 'y') cfg_synchosts = conf.get('allowed_sync_hosts', '127.0.0.1') @@ -127,21 +129,31 @@ class SwiftAuth(object): except ValueError: return webob.exc.HTTPNotFound(request=req) + user_roles = env_identity.get('roles', []) + + # Give unconditional access to a user with the reseller_admin + # role. + if self.reseller_admin_role in user_roles: + msg = 'User %s has reseller admin authorizing' + self.logger.debug(msg % tenant[0]) + req.environ['swift_owner'] = True + return + + # Check if a user tries to access an account that does not match their + # token if not self._reseller_check(account, tenant[0]): log_msg = 'tenant mismatch: %s != %s' % (account, tenant[0]) self.logger.debug(log_msg) return self.denied_response(req) - user_groups = env_identity.get('roles', []) - - # Check the groups the user is belonging to. If the user is - # part of the group defined in the config variable + # Check the roles the user is belonging to. If the user is + # part of the role defined in the config variable # operator_roles (like admin) then it will be # promoted as an admin of the account/tenant. - for group in self.operator_roles.split(','): - group = group.strip() - if group in user_groups: - log_msg = "allow user in group %s as account admin" % group + for role in self.operator_roles.split(','): + role = role.strip() + if role in user_roles: + log_msg = 'allow user with role %s as account admin' % (role) self.logger.debug(log_msg) req.environ['swift_owner'] = True return @@ -165,25 +177,26 @@ class SwiftAuth(object): return # Check if referrer is allowed. - referrers, groups = swift_acl.parse_acl(getattr(req, 'acl', None)) + referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) if swift_acl.referrer_allowed(req.referer, referrers): - if obj or '.rlistings' in groups: + #TODO(chmou): convert .rlistings to Keystone type role. + if obj or '.rlistings' in roles: log_msg = 'authorizing %s via referer ACL' % req.referrer self.logger.debug(log_msg) return return self.denied_response(req) # Allow ACL at individual user level (tenant:user format) - if '%s:%s' % (tenant[0], user) in groups: + if '%s:%s' % (tenant[0], user) in roles: log_msg = 'user %s:%s allowed in ACL authorizing' self.logger.debug(log_msg % (tenant[0], user)) return - # Check if we have the group in the usergroups and allow it - for user_group in user_groups: - if user_group in groups: + # Check if we have the role in the userroles and allow it + for user_role in user_roles: + if user_role in roles: log_msg = 'user %s:%s allowed in ACL: %s authorizing' - self.logger.debug(log_msg % (tenant[0], user, user_group)) + self.logger.debug(log_msg % (tenant[0], user, user_role)) return return self.denied_response(req) From 56b2f818ef5a3b7f3c32c093908a59c1e6df0e6e Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 7 Mar 2012 16:00:45 -0800 Subject: [PATCH 032/121] HTTP_AUTHORIZATION was used in proxy mode Change-Id: I72eae79bd1991321eac224777fb186c5022f2c12 --- auth_token.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/auth_token.py b/auth_token.py index a6673159..9f9421de 100644 --- a/auth_token.py +++ b/auth_token.py @@ -53,15 +53,9 @@ WWW-Authenticate HTTP header returned to a user indicating which endpoint to use to retrieve a new token -HTTP_AUTHORIZATION - basic auth password used to validate the connection - What we add to the request for use by the OpenStack service ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -HTTP_X_AUTHORIZATION - The client identity being passed in - HTTP_X_IDENTITY_STATUS 'Confirmed' or 'Invalid' The underlying service will only see a value of 'Invalid' if the Middleware From 21f4850dce566336491866af15c9c4820cff2e25 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Fri, 2 Mar 2012 11:34:16 +0000 Subject: [PATCH 033/121] Make sure we have a port number before int it. - Remove unused auth_location in s3_token along the way. - Fixes bug 944720. Change-Id: Ib6e48511d09798868c5ca3fa00472525bc9f8823 --- auth_token.py | 3 +-- s3_token.py | 9 +-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/auth_token.py b/auth_token.py index a6673159..a99f8c57 100644 --- a/auth_token.py +++ b/auth_token.py @@ -129,8 +129,7 @@ class AuthProtocol(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') - self.auth_port = int(conf.get('auth_port')) - + self.auth_port = int(conf.get('auth_port', 35357)) auth_protocol = conf.get('auth_protocol', 'https') if auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection diff --git a/s3_token.py b/s3_token.py index 8cf3e0a0..348fcc40 100644 --- a/s3_token.py +++ b/s3_token.py @@ -45,16 +45,9 @@ class S3Token(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') - self.auth_port = int(conf.get('auth_port')) + self.auth_port = int(conf.get('auth_port', 35357)) self.auth_protocol = conf.get('auth_protocol', 'https') - # where to tell clients to find the auth service (default to url - # constructed based on endpoint we have for the service to use) - self.auth_location = conf.get('auth_uri', - '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port)) - # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') From 9e9a5876cfe4a0b4b2e86f97f86f6856cebf3083 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Fri, 2 Mar 2012 13:38:39 -0600 Subject: [PATCH 034/121] Added license header (bug 929663) Change-Id: Ia36a22f2d6bba411e4fad81ea2d6fa1f0465a733 --- __init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/__init__.py b/__init__.py index e2e9a993..85ed395c 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 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. + from keystone.middleware.core import * From 675f7bbfbcae74036581503d23a3610a8a2cd915 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Fri, 2 Mar 2012 15:31:54 +0000 Subject: [PATCH 035/121] Add token caching via memcache. - Fixes bug 938253 - caching requires both python-memcache and iso8601 Change-Id: I23d5849aad4c6a2333b903eaca6d4f00be8615d3 --- auth_token.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/auth_token.py b/auth_token.py index 9f9421de..095b6964 100644 --- a/auth_token.py +++ b/auth_token.py @@ -90,9 +90,11 @@ HTTP_X_ROLE """ +import datetime import httplib import json import logging +import time import webob import webob.exc @@ -143,6 +145,20 @@ class AuthProtocol(object): self.admin_password = conf.get('admin_password') self.admin_tenant_name = conf.get('admin_tenant_name', 'admin') + # Token caching via memcache + self._cache = None + memcache_servers = conf.get('memcache_servers') + # By default the token will be cached for 5 minutes + self.token_cache_time = conf.get('token_cache_time', 300) + if memcache_servers: + try: + import memcache + import iso8601 + logger.info('Using memcache for caching token') + self._cache = memcache.Client(memcache_servers.split(',')) + except NameError as e: + logger.warn('disabled caching due to missing libraries %s', e) + def __call__(self, env, start_response): """Handle incoming request. @@ -317,16 +333,22 @@ class AuthProtocol(object): :raise ServiceError if unable to authenticate token """ + cached = self._cache_get(user_token) + if cached: + return cached + headers = {'X-Auth-Token': self.get_admin_token()} response, data = self._json_request('GET', '/v2.0/tokens/%s' % user_token, additional_headers=headers) if response.status == 200: + self._cache_put(user_token, data) return data if response.status == 404: # FIXME(ja): I'm assuming the 404 status means that user_token is # invalid - not that the admin_token is invalid + self._cache_store_invalid(user_token) raise InvalidUserToken('Token authorization failed') if response.status == 401: logger.info('Keystone rejected admin token, resetting') @@ -419,6 +441,54 @@ class AuthProtocol(object): env_key = self._header_to_env_var(key) return env.get(env_key, default) + def _cache_get(self, token): + """Return token information from cache. + + If token is invalid raise InvalidUserToken + return token only if fresh (not expired). + """ + if self._cache and token: + key = 'tokens/%s' % token + cached = self._cache.get(key) + if cached == 'invalid': + logger.debug('Cached Token %s is marked unauthorized', token) + raise InvalidUserToken('Token authorization failed') + if cached: + data, expires = cached + if time.time() < expires: + logger.debug('Returning cached token %s', token) + return data + else: + logger.debug('Cached Token %s seems expired', token) + + def _cache_put(self, token, data): + """Put token data into the cache. + + Stores the parsed expire date in cache allowing + quick check of token freshness on retrieval. + """ + if self._cache and data: + key = 'tokens/%s' % token + if 'token' in data.get('access', {}): + timestamp = data['access']['token']['expires'] + expires = iso8601.parse_date(timestamp) + else: + logger.error('invalid token format') + return + logger.debug('Storing %s token in memcache', token) + self._cache.set(key, + (data, expires), + time=self.token_cache_time) + + def _cache_store_invalid(self, token): + """Store invalid token in cache.""" + if self._cache: + key = 'tokens/%s' % token + logger.debug('Marking token %s as unauthorized in memcache', token) + self._cache.set(key, + 'invalid', + time=self.token_cache_time) + def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" From 61f63a9e986bdb60d0d86a242c646497bccd597f Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Sat, 10 Mar 2012 13:59:44 -0800 Subject: [PATCH 036/121] Add simple set of tests for auth_token middleware Change-Id: Ie959e91dc555e35b8e5ba4b01c68a3f232efc115 --- auth_token.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 9f056034..ee763e34 100644 --- a/auth_token.py +++ b/auth_token.py @@ -283,13 +283,18 @@ class AuthProtocol(object): conn.request(method, path, **kwargs) response = conn.getresponse() body = response.read() - data = json.loads(body) except Exception, e: logger.error('HTTP connection exception: %s' % e) raise ServiceError('Unable to communicate with keystone') finally: conn.close() + try: + data = json.loads(body) + except ValueError: + logger.debug('Keystone did not return json-encoded body') + data = {} + return response, data def _request_admin_token(self): From 2ed5d570d595096c0a862d27c26fd0887e5dde29 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Sat, 10 Mar 2012 17:22:06 +0100 Subject: [PATCH 037/121] Fix iso8601 import/use and date comparaison. - Store the unix time from iso8601.parse_date to compare against time.time. - on a WSGI environement the import don't get passed to the methods from __init__ use a self. variable. - Fixes bug 951603. - Add unit tests. - Add iso8601 to test-requires. Change-Id: Ia8af8b203d1310d5ae6868c3a14dfdf68d6e5331 --- auth_token.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index ee763e34..9874a1bf 100644 --- a/auth_token.py +++ b/auth_token.py @@ -146,6 +146,7 @@ class AuthProtocol(object): # Token caching via memcache self._cache = None + self._iso8601 = None memcache_servers = conf.get('memcache_servers') # By default the token will be cached for 5 minutes self.token_cache_time = conf.get('token_cache_time', 300) @@ -155,6 +156,7 @@ class AuthProtocol(object): import iso8601 logger.info('Using memcache for caching token') self._cache = memcache.Client(memcache_servers.split(',')) + self._iso8601 = iso8601 except NameError as e: logger.warn('disabled caching due to missing libraries %s', e) @@ -459,7 +461,7 @@ class AuthProtocol(object): raise InvalidUserToken('Token authorization failed') if cached: data, expires = cached - if time.time() < expires: + if time.time() < float(expires): logger.debug('Returning cached token %s', token) return data else: @@ -475,7 +477,7 @@ class AuthProtocol(object): key = 'tokens/%s' % token if 'token' in data.get('access', {}): timestamp = data['access']['token']['expires'] - expires = iso8601.parse_date(timestamp) + expires = self._iso8601.parse_date(timestamp).strftime('%s') else: logger.error('invalid token format') return From 6dc5c1c43b2be7dd1d4a52443e2a119845446a4c Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 13 Mar 2012 12:27:53 -0500 Subject: [PATCH 038/121] Improved legacy tenancy resolution (bug 951933) Change-Id: Ia6fd5eb57e8d7f90328117351f7b814b1b4495dc --- auth_token.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/auth_token.py b/auth_token.py index 9874a1bf..76b20b36 100644 --- a/auth_token.py +++ b/auth_token.py @@ -90,7 +90,6 @@ HTTP_X_ROLE """ -import datetime import httplib import json import logging @@ -392,15 +391,29 @@ class AuthProtocol(object): token = token_info['access']['token'] roles = ','.join([role['name'] for role in user.get('roles', [])]) - # FIXME(ja): I think we are checking in both places because: - # tenant might not be returned, and there was a pre-release - # that put tenant objects inside the user object? - try: - tenant_id = token['tenant']['id'] - tenant_name = token['tenant']['name'] - except: - tenant_id = user.get('tenantId') - tenant_name = user.get('tenantName') + def get_tenant_info(): + """Returns a (tenant_id, tenant_name) tuple from context.""" + def essex(): + """Essex puts the tenant ID and name on the token.""" + return (token['tenant']['id'], token['tenant']['name']) + + def pre_diablo(): + """Pre-diablo, Keystone only provided tenantId.""" + return (token['tenantId'], token['tenantId']) + + def default_tenant(): + """Assume the user's default tenant.""" + return (user['tenantId'], user['tenantName']) + + for method in [essex, pre_diablo, default_tenant]: + try: + return method() + except KeyError: + pass + + raise InvalidUserToken('Unable to determine tenancy.') + + tenant_id, tenant_name = get_tenant_info() user_id = user['id'] user_name = user['username'] From c95f0033b7dd68278e0d5b50a315c7eca754e738 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Wed, 14 Mar 2012 16:19:12 +0000 Subject: [PATCH 039/121] Allow connect to another tenant. - Works with nova s3_affix_tenant. - This would only be allowed for user who has reselleradmin rights. - Fixes bug 954505. Change-Id: Iea84f1c61f6c725982c8bee95889ce084d9ffd82 --- s3_token.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/s3_token.py b/s3_token.py index 348fcc40..603b760c 100644 --- a/s3_token.py +++ b/s3_token.py @@ -66,8 +66,22 @@ class S3Token(object): auth_header = req.headers['Authorization'] access, signature = auth_header.split(' ')[-1].rsplit(':', 1) + # NOTE(chmou): This is to handle the special case with nova + # when we have the option s3_affix_tenant. We will force it to + # connect to another account than the one + # authenticated. Before people start getting worried about + # security, I should point that we are connecting with + # username/token specified by the user but instead of + # connecting to its own account we will force it to go to an + # another account. In a normal scenario if that user don't + # have the reseller right it will just fail but since the + # reseller account can connect to every account it is allowed + # by the swift_auth middleware. + force_tenant = None + if ':' in access: + access, force_tenant = access.split(':') - # Authenticate the request. + # Authenticate request. creds = {'credentials': {'access': access, 'token': token, 'signature': signature, @@ -100,8 +114,7 @@ class S3Token(object): # NOTE(chmou): We still have the same problem we would need to # change token_auth to detect if we already # identified and not doing a second query and just - # pass it through to swiftauth in this case. - # identity_info = json.loads(response) + # pass it thru to swiftauth in this case. output = resp.read() conn.close() identity_info = json.loads(output) @@ -115,8 +128,11 @@ class S3Token(object): raise req.headers['X-Auth-Token'] = token_id + tenant_to_connect = force_tenant or tenant[0] + self.logger.debug('Connecting with tenant: %s' % + (tenant_to_connect)) environ['PATH_INFO'] = environ['PATH_INFO'].replace( - account, 'AUTH_%s' % tenant[0]) + account, 'AUTH_%s' % tenant_to_connect) return self.app(environ, start_response) From 2513c941880d7f6c39616c91d6ef3b689bc0a53f Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 14 Mar 2012 14:28:04 -0500 Subject: [PATCH 040/121] Refactor keystone.common.logging use (bug 948224) Change-Id: I01b2b5748a2524273bb8c2b734ab22415652f739 --- auth_token.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/auth_token.py b/auth_token.py index 76b20b36..9b9e90ad 100644 --- a/auth_token.py +++ b/auth_token.py @@ -99,7 +99,7 @@ import webob import webob.exc -logger = logging.getLogger('keystone.middleware.auth_token') +LOG = logging.getLogger(__name__) class InvalidUserToken(Exception): @@ -114,7 +114,7 @@ class AuthProtocol(object): """Auth Middleware that handles authenticating client calls.""" def __init__(self, app, conf): - logger.info('Starting keystone auth_token middleware') + LOG.info('Starting keystone auth_token middleware') self.conf = conf self.app = app @@ -153,11 +153,11 @@ class AuthProtocol(object): try: import memcache import iso8601 - logger.info('Using memcache for caching token') + LOG.info('Using memcache for caching token') self._cache = memcache.Client(memcache_servers.split(',')) self._iso8601 = iso8601 except NameError as e: - logger.warn('disabled caching due to missing libraries %s', e) + LOG.warn('disabled caching due to missing libraries %s', e) def __call__(self, env, start_response): """Handle incoming request. @@ -166,7 +166,7 @@ class AuthProtocol(object): we can't authenticate. """ - logger.debug('Authenticating user token') + LOG.debug('Authenticating user token') try: self._remove_auth_headers(env) user_token = self._get_user_token_from_header(env) @@ -177,15 +177,15 @@ class AuthProtocol(object): except InvalidUserToken: if self.delay_auth_decision: - logger.info('Invalid user token - deferring reject downstream') + LOG.info('Invalid user token - deferring reject downstream') self._add_headers(env, {'X-Identity-Status': 'Invalid'}) return self.app(env, start_response) else: - logger.info('Invalid user token - rejecting request') + LOG.info('Invalid user token - rejecting request') return self._reject_request(env, start_response) except ServiceError, e: - logger.critical('Unable to obtain admin token: %s' % e) + LOG.critical('Unable to obtain admin token: %s' % e) resp = webob.exc.HTTPServiceUnavailable() return resp(env, start_response) @@ -207,7 +207,7 @@ class AuthProtocol(object): 'X-Tenant', 'X-Role', ) - logger.debug('Removing headers from request environment: %s' % + LOG.debug('Removing headers from request environment: %s' % ','.join(auth_headers)) self._remove_headers(env, auth_headers) @@ -285,7 +285,7 @@ class AuthProtocol(object): response = conn.getresponse() body = response.read() except Exception, e: - logger.error('HTTP connection exception: %s' % e) + LOG.error('HTTP connection exception: %s' % e) raise ServiceError('Unable to communicate with keystone') finally: conn.close() @@ -293,7 +293,7 @@ class AuthProtocol(object): try: data = json.loads(body) except ValueError: - logger.debug('Keystone did not return json-encoded body') + LOG.debug('Keystone did not return json-encoded body') data = {} return response, data @@ -356,13 +356,13 @@ class AuthProtocol(object): self._cache_store_invalid(user_token) raise InvalidUserToken('Token authorization failed') if response.status == 401: - logger.info('Keystone rejected admin token, resetting') + LOG.info('Keystone rejected admin token, resetting') self.admin_token = None else: - logger.error('Bad response code while validating token: %s' % + LOG.error('Bad response code while validating token: %s' % response.status) if retry: - logger.info('Retrying validation') + LOG.info('Retrying validation') return self._validate_user_token(user_token, False) else: raise InvalidUserToken() @@ -470,15 +470,15 @@ class AuthProtocol(object): key = 'tokens/%s' % token cached = self._cache.get(key) if cached == 'invalid': - logger.debug('Cached Token %s is marked unauthorized', token) + LOG.debug('Cached Token %s is marked unauthorized', token) raise InvalidUserToken('Token authorization failed') if cached: data, expires = cached if time.time() < float(expires): - logger.debug('Returning cached token %s', token) + LOG.debug('Returning cached token %s', token) return data else: - logger.debug('Cached Token %s seems expired', token) + LOG.debug('Cached Token %s seems expired', token) def _cache_put(self, token, data): """Put token data into the cache. @@ -492,9 +492,9 @@ class AuthProtocol(object): timestamp = data['access']['token']['expires'] expires = self._iso8601.parse_date(timestamp).strftime('%s') else: - logger.error('invalid token format') + LOG.error('invalid token format') return - logger.debug('Storing %s token in memcache', token) + LOG.debug('Storing %s token in memcache', token) self._cache.set(key, (data, expires), time=self.token_cache_time) @@ -503,7 +503,7 @@ class AuthProtocol(object): """Store invalid token in cache.""" if self._cache: key = 'tokens/%s' % token - logger.debug('Marking token %s as unauthorized in memcache', token) + LOG.debug('Marking token %s as unauthorized in memcache', token) self._cache.set(key, 'invalid', time=self.token_cache_time) From 1cb8aa97bcfe68c24cdbadf1e8889d5d851b1950 Mon Sep 17 00:00:00 2001 From: Brian Lamar Date: Wed, 14 Mar 2012 18:30:13 -0400 Subject: [PATCH 041/121] Update username -> name in token response. Tokens validation responses contain user information. The API docs seem to indicate token["user"]["name"] contains the username but currently the auth_token.py middleware checks for token["user"]["username"]. This updates that check and the tests. Fixes bug 955563 Change-Id: Ib2fbf6fcea87f7066394cf14c18158f1e5eeaf06 --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 76b20b36..e89c1a76 100644 --- a/auth_token.py +++ b/auth_token.py @@ -416,7 +416,7 @@ class AuthProtocol(object): tenant_id, tenant_name = get_tenant_info() user_id = user['id'] - user_name = user['username'] + user_name = user['name'] return { 'X-Identity-Status': 'Confirmed', From 8e78148a109f49bbdae4da73d7dd85b0ecf3afc6 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Fri, 16 Mar 2012 15:55:22 -0700 Subject: [PATCH 042/121] Remove glance_auth_token middleware * Fixes bug 957501 Change-Id: I2ae6ec7b391dd41587f2246940a8d392c12c91fe --- glance_auth_token.py | 78 -------------------------------------------- 1 file changed, 78 deletions(-) delete mode 100644 glance_auth_token.py diff --git a/glance_auth_token.py b/glance_auth_token.py deleted file mode 100644 index be69a208..00000000 --- a/glance_auth_token.py +++ /dev/null @@ -1,78 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011-2012 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. - -""" -Glance Keystone Integration Middleware - -This WSGI component allows keystone to act as an identity service for -glance. Glance now supports the concept of images owned by a tenant, -and this middleware takes the authentication information provided by -auth_token and builds a glance-compatible context object. - -Use by applying after auth_token in the glance-api.ini and -glance-registry.ini configurations, replacing the existing context -middleware. - -Example: examples/paste/glance-api.conf, - examples/paste/glance-registry.conf -""" - -from glance.common import context - - -class KeystoneContextMiddleware(context.ContextMiddleware): - """Glance keystone integration middleware.""" - - def process_request(self, req): - """ - Extract keystone-provided authentication information from the - request and construct an appropriate context from it. - """ - # Only accept the authentication information if the identity - # has been confirmed--presumably by upstream - if req.headers.get('X_IDENTITY_STATUS', 'Invalid') != 'Confirmed': - # Use the default empty context - req.context = self.make_context(read_only=True) - return - - # OK, let's extract the information we need - auth_tok = req.headers.get('X_AUTH_TOKEN', - req.headers.get('X_STORAGE_TOKEN')) - user = req.headers.get('X_USER') - tenant = req.headers.get('X_TENANT') - roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] - is_admin = 'admin' in roles - - # Construct the context - req.context = self.make_context(auth_tok=auth_tok, - user=user, - tenant=tenant, - roles=roles, - is_admin=is_admin) - - -def filter_factory(global_conf, **local_conf): - """ - Factory method for paste.deploy - """ - conf = global_conf.copy() - conf.update(local_conf) - - def filter(app): - return KeystoneContextMiddleware(app, conf) - - return filter From 7fc0a5c8df5e99dc8ce992786aaa4a2d9bbd5957 Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Mon, 19 Mar 2012 08:31:26 -0700 Subject: [PATCH 043/121] Remove nova-specific middlewares * Nova now ships with nova.api.auth.NovaKeystoneContext * Nova does not depend on either of the middlewares being removed Change-Id: I9546e5c84ea1453f5dfd2dd7bf9924ccda57f87a --- nova_auth_token.py | 103 --------------------------------------- nova_keystone_context.py | 71 --------------------------- 2 files changed, 174 deletions(-) delete mode 100644 nova_auth_token.py delete mode 100644 nova_keystone_context.py diff --git a/nova_auth_token.py b/nova_auth_token.py deleted file mode 100644 index d5b280c2..00000000 --- a/nova_auth_token.py +++ /dev/null @@ -1,103 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2012 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 diff --git a/nova_keystone_context.py b/nova_keystone_context.py deleted file mode 100644 index 2b1c82a9..00000000 --- a/nova_keystone_context.py +++ /dev/null @@ -1,71 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011-2012 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 Auth Middleware. - -""" - -import webob.dec -import webob.exc - -from nova import context -from nova import flags -from nova import wsgi - - -FLAGS = flags.FLAGS -flags.DECLARE('use_forwarded_for', 'nova.api.auth') - - -class NovaKeystoneContext(wsgi.Middleware): - """Make a request context from keystone headers""" - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - try: - user_id = req.headers['X_USER'] - except KeyError: - return webob.exc.HTTPUnauthorized() - # get the roles - roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')] - - 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'] - - # 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... - remote_address = getattr(req, 'remote_address', '127.0.0.1') - remote_address = req.remote_addr - if FLAGS.use_forwarded_for: - remote_address = req.headers.get('X-Forwarded-For', remote_address) - ctx = context.RequestContext(user_id, - project_id, - roles=roles, - auth_token=auth_token, - strategy='keystone', - remote_address=remote_address) - - req.environ['nova.context'] = ctx - return self.application From 0f3adee483ad14f198892efb16da4e2b31b7d7ee Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Tue, 20 Mar 2012 17:08:46 +0000 Subject: [PATCH 044/121] Rename tokenauth to authtoken. - Avoid confusing by using the authtoken name for auth_token middleware. - Improve swift_auth middleware doc. Change-Id: I287860eba067b99a1d89f8f17200820340836ff9 --- swift_auth.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 9bb38597..23578723 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -38,10 +38,15 @@ from swift.common.middleware import acl as swift_acl class SwiftAuth(object): """Swift middleware to Keystone authorization system. - In Swift's proxy-server.conf add the middleware to your pipeline:: + In Swift's proxy-server.conf add this middleware to your pipeline:: [pipeline:main] - pipeline = catch_errors cache tokenauth swiftauth proxy-server + pipeline = catch_errors cache authtoken swiftauth proxy-server + + Make sure you have the authtoken middleware before the swiftauth + middleware. The authtoken will take care of validating the user + and swiftauth middleware will authorize it. See the documentation + about how to configure the authtoken middleware. Set account auto creation to true:: From 38b2a3e7f3ccac7586656c7e8a9549d1a2cb5b1c Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Wed, 14 Mar 2012 17:12:22 +0000 Subject: [PATCH 045/121] updating docs to include creating service accts and some general doc cleanup Change-Id: I745e636391f72dce9c142e399f46c0eb586aba8b --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 44cee377..6b8885b4 100644 --- a/auth_token.py +++ b/auth_token.py @@ -28,7 +28,7 @@ This WSGI component: * Collects and forwards identity information based on a valid token such as user name, tenant, etc -Refer to: http://keystone.openstack.org/middleware_architecture.html +Refer to: http://keystone.openstack.org/middlewarearchitecture.html HEADERS ------- From 4999d9e6f9fb88354405b6fd342a301964a9b299 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Wed, 21 Mar 2012 16:59:15 +0000 Subject: [PATCH 046/121] S3 tokens cleanups. - Cleanups. - Remove reference about config admin_username/password/token. - Return proper http error on errors. - Add unittests (skip them for now when swift is not installed). - Fixes bug 956983. Change-Id: I392fc274f3b01a5a0b5779dd13f9cd3b819ee65a --- s3_token.py | 124 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/s3_token.py b/s3_token.py index 603b760c..4ef9d814 100644 --- a/s3_token.py +++ b/s3_token.py @@ -21,7 +21,17 @@ # This source code is based ./auth_token.py and ./ec2_token.py. # See them for their copyright. -"""Starting point for routing S3 requests.""" +""" +S3 TOKEN MIDDLEWARE + +This WSGI component: + +* Get a request from the swift3 middleware with an S3 Authorization + access key. +* Validate s3 token in Keystone. +* Transform the account name to AUTH_%(tenant_name). + +""" import httplib import json @@ -34,6 +44,10 @@ from swift.common import utils as swift_utils PROTOCOL_NAME = "S3 Token Authentication" +class ServiceError(Exception): + pass + + class S3Token(object): """Auth Middleware that handles S3 authenticating client calls.""" @@ -43,29 +57,74 @@ class S3Token(object): self.logger = swift_utils.get_logger(conf, log_route='s3_token') self.logger.debug('Starting the %s component' % PROTOCOL_NAME) + # NOTE(chmou): We probably want to make sure that there is a _ + # at the end of our reseller_prefix. + self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_') # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port', 35357)) - self.auth_protocol = conf.get('auth_protocol', 'https') + auth_protocol = conf.get('auth_protocol', 'https') + if auth_protocol == 'http': + self.http_client_class = httplib.HTTPConnection + else: + self.http_client_class = httplib.HTTPSConnection - # Credentials used to verify this component with the Auth service since - # validating tokens is a privileged call - self.admin_token = conf.get('admin_token') + def _json_request(self, creds_json): + headers = {'Content-Type': 'application/json'} + + try: + conn = self.http_client_class(self.auth_host, self.auth_port) + conn.request('POST', '/v2.0/s3tokens', + body=creds_json, + headers=headers) + response = conn.getresponse() + output = response.read() + except Exception, e: + self.logger.info('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + if response.status < 200 or response.status >= 300: + raise ServiceError('Keystone reply error: status=%s reason=%s' % ( + response.status, + response.reason)) + + return (response, output) def __call__(self, environ, start_response): """Handle incoming request. authenticate and send downstream.""" req = webob.Request(environ) - parts = swift_utils.split_path(req.path, 1, 4, True) - version, account, container, obj = parts + + try: + parts = swift_utils.split_path(req.path, 1, 4, True) + version, account, container, obj = parts + except ValueError: + msg = 'Not a path query, skipping.' + self.logger.debug(msg) + return self.app(environ, start_response) # Read request signature and access id. if not 'Authorization' in req.headers: + msg = 'No Authorization header. skipping.' + self.logger.debug(msg) return self.app(environ, start_response) + token = req.headers.get('X-Auth-Token', req.headers.get('X-Storage-Token')) + if not token: + msg = 'You did not specify a auth or a storage token. skipping.' + self.logger.debug(msg) + return self.app(environ, start_response) auth_header = req.headers['Authorization'] - access, signature = auth_header.split(' ')[-1].rsplit(':', 1) + try: + access, signature = auth_header.split(' ')[-1].rsplit(':', 1) + except(ValueError): + msg = 'You have an invalid Authorization header: %s' + self.logger.debug(msg % (auth_header)) + return webob.exc.HTTPBadRequest()(environ, start_response) + # NOTE(chmou): This is to handle the special case with nova # when we have the option s3_affix_tenant. We will force it to # connect to another account than the one @@ -84,28 +143,8 @@ class S3Token(object): # Authenticate request. creds = {'credentials': {'access': access, 'token': token, - 'signature': signature, - 'host': req.host, - 'verb': req.method, - 'path': req.path, - 'expire': req.headers['Date'], - }} - + 'signature': signature}} creds_json = json.dumps(creds) - headers = {'Content-Type': 'application/json'} - if self.auth_protocol == 'http': - conn = httplib.HTTPConnection(self.auth_host, self.auth_port) - else: - conn = httplib.HTTPSConnection(self.auth_host, self.auth_port) - - conn.request('POST', '/v2.0/s3tokens', - body=creds_json, - headers=headers) - resp = conn.getresponse() - if resp.status < 200 or resp.status >= 300: - raise Exception('Keystone reply error: status=%s reason=%s' % ( - resp.status, - resp.reason)) # NOTE(vish): We could save a call to keystone by having # keystone return token, tenant, user, and roles @@ -115,24 +154,23 @@ class S3Token(object): # change token_auth to detect if we already # identified and not doing a second query and just # pass it thru to swiftauth in this case. - output = resp.read() - conn.close() - identity_info = json.loads(output) + (resp, output) = self._json_request(creds_json) + try: + identity_info = json.loads(output) token_id = str(identity_info['access']['token']['id']) - tenant = (identity_info['access']['token']['tenant']['id'], - identity_info['access']['token']['tenant']['name']) - except (KeyError, IndexError): - self.logger.debug('Error getting keystone reply: %s' % - (str(output))) - raise + tenant = identity_info['access']['token']['tenant'] + except (ValueError, KeyError): + error = 'Error on keystone reply: %d %s' + self.logger.debug(error % (resp.status, str(output))) + return webob.exc.HTTPBadRequest()(environ, start_response) req.headers['X-Auth-Token'] = token_id - tenant_to_connect = force_tenant or tenant[0] - self.logger.debug('Connecting with tenant: %s' % - (tenant_to_connect)) - environ['PATH_INFO'] = environ['PATH_INFO'].replace( - account, 'AUTH_%s' % tenant_to_connect) + tenant_to_connect = force_tenant or tenant['id'] + self.logger.debug('Connecting with tenant: %s' % (tenant_to_connect)) + new_tenant_name = '%s%s' % (self.reseller_prefix, tenant_to_connect) + environ['PATH_INFO'] = environ['PATH_INFO'].replace(account, + new_tenant_name) return self.app(environ, start_response) From ec48e26b8556a0a371487f972dae6d686d43fdb1 Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Tue, 20 Mar 2012 18:47:19 -0700 Subject: [PATCH 047/121] Improve swift_auth test coverage + Minor fixes * Isolates authorize() tests from wsgi tests * Adds coverage for authorize() * Adds support for a blank reseller_prefix * Adds swift_auth test dependencies to tools/test-requires * Cleans up authorize()'s use of tenant_id/tenant_name (addresses bug 963546) Change-Id: I603b89ab4fe8559b0f5d72528afd659ee0f0bce1 --- swift_auth.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 23578723..df62a834 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -121,12 +121,12 @@ class SwiftAuth(object): def _reseller_check(self, account, tenant_id): """Check reseller prefix.""" - return account == '%s_%s' % (self.reseller_prefix, tenant_id) + return account == '%s%s' % (self.reseller_prefix, tenant_id) def authorize(self, req): env = req.environ env_identity = env.get('keystone.identity', {}) - tenant = env_identity.get('tenant') + tenant_id, tenant_name = env_identity.get('tenant') try: part = swift_utils.split_path(req.path, 1, 4, True) @@ -140,14 +140,14 @@ class SwiftAuth(object): # role. if self.reseller_admin_role in user_roles: msg = 'User %s has reseller admin authorizing' - self.logger.debug(msg % tenant[0]) + self.logger.debug(msg % tenant_id) req.environ['swift_owner'] = True return # Check if a user tries to access an account that does not match their # token - if not self._reseller_check(account, tenant[0]): - log_msg = 'tenant mismatch: %s != %s' % (account, tenant[0]) + if not self._reseller_check(account, tenant_id): + log_msg = 'tenant mismatch: %s != %s' % (account, tenant_id) self.logger.debug(log_msg) return self.denied_response(req) @@ -165,7 +165,7 @@ class SwiftAuth(object): # If user is of the same name of the tenant then make owner of it. user = env_identity.get('user', '') - if self.is_admin and user == tenant[1]: + if self.is_admin and user == tenant_name: req.environ['swift_owner'] = True return @@ -192,16 +192,16 @@ class SwiftAuth(object): return self.denied_response(req) # Allow ACL at individual user level (tenant:user format) - if '%s:%s' % (tenant[0], user) in roles: + if '%s:%s' % (tenant_name, user) in roles: log_msg = 'user %s:%s allowed in ACL authorizing' - self.logger.debug(log_msg % (tenant[0], user)) + self.logger.debug(log_msg % (tenant_name, user)) return # Check if we have the role in the userroles and allow it for user_role in user_roles: if user_role in roles: log_msg = 'user %s:%s allowed in ACL: %s authorizing' - self.logger.debug(log_msg % (tenant[0], user, user_role)) + self.logger.debug(log_msg % (tenant_name, user, user_role)) return return self.denied_response(req) From f1baa7af4254124adbe48713f8da6e6286ed308c Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Tue, 20 Mar 2012 22:19:36 -0700 Subject: [PATCH 048/121] Add support to swift_auth for tokenless authz * Updates keystone.middleware.swift_auth to allow token-less (unauthenticated) access for container sync (bug 954030) and permitted referrers (bug 924578). Change-Id: Ieccf458c44dfe55f546dc15c79704800dad59ac0 --- swift_auth.py | 106 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index df62a834..b5f0a9b1 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -44,9 +44,12 @@ class SwiftAuth(object): pipeline = catch_errors cache authtoken swiftauth proxy-server Make sure you have the authtoken middleware before the swiftauth - middleware. The authtoken will take care of validating the user - and swiftauth middleware will authorize it. See the documentation - about how to configure the authtoken middleware. + middleware. authtoken will take care of validating the user and + swiftauth will authorize access. If support is required for + unvalidated users (as with anonymous access), authtoken will need + to be configured with delay_auth_decision set to true. See the + documentation for more detail on how to configure the authtoken + middleware. Set account auto creation to true:: @@ -95,15 +98,17 @@ class SwiftAuth(object): def __call__(self, environ, start_response): identity = self._keystone_identity(environ) - if not identity: - environ['swift.authorize'] = self.denied_response - return self.app(environ, start_response) + if identity: + self.logger.debug('Using identity: %r' % (identity)) + environ['keystone.identity'] = identity + environ['REMOTE_USER'] = identity.get('tenant') + environ['swift.authorize'] = self.authorize + else: + self.logger.debug('Authorizing as anonymous') + environ['swift.authorize'] = self.authorize_anonymous - self.logger.debug("Using identity: %r" % (identity)) - environ['keystone.identity'] = identity - environ['REMOTE_USER'] = identity.get('tenant') - environ['swift.authorize'] = self.authorize environ['swift.clean_acl'] = swift_acl.clean_acl + return self.app(environ, start_response) def _keystone_identity(self, environ): @@ -119,9 +124,12 @@ class SwiftAuth(object): 'roles': roles} return identity + def _get_account_for_tenant(self, tenant_id): + return '%s%s' % (self.reseller_prefix, tenant_id) + def _reseller_check(self, account, tenant_id): """Check reseller prefix.""" - return account == '%s%s' % (self.reseller_prefix, tenant_id) + return account == self._get_account_for_tenant(tenant_id) def authorize(self, req): env = req.environ @@ -169,26 +177,13 @@ class SwiftAuth(object): req.environ['swift_owner'] = True return - # Allow container sync. - if (req.environ.get('swift_sync_key') - and req.environ['swift_sync_key'] == - req.headers.get('x-container-sync-key', None) - and 'x-timestamp' in req.headers - and (req.remote_addr in self.allowed_sync_hosts - or swift_utils.get_remote_client(req) - in self.allowed_sync_hosts)): - log_msg = 'allowing proxy %s for container-sync' % req.remote_addr - self.logger.debug(log_msg) - return - - # Check if referrer is allowed. referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) - if swift_acl.referrer_allowed(req.referer, referrers): - #TODO(chmou): convert .rlistings to Keystone type role. - if obj or '.rlistings' in roles: - log_msg = 'authorizing %s via referer ACL' % req.referrer - self.logger.debug(log_msg) - return + + authorized = self._authorize_unconfirmed_identity(req, obj, referrers, + roles) + if authorized: + return + elif authorized is not None: return self.denied_response(req) # Allow ACL at individual user level (tenant:user format) @@ -206,6 +201,57 @@ class SwiftAuth(object): return self.denied_response(req) + def authorize_anonymous(self, req): + """ + Authorize an anonymous request. + + :returns: None if authorization is granted, an error page otherwise. + """ + try: + part = swift_utils.split_path(req.path, 1, 4, True) + version, account, container, obj = part + except ValueError: + return webob.exc.HTTPNotFound(request=req) + + is_authoritative_authz = (account and + account.startswith(self.reseller_prefix)) + if not is_authoritative_authz: + return self.denied_response(req) + + referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) + authorized = self._authorize_unconfirmed_identity(req, obj, referrers, + roles) + if not authorized: + return self.denied_response(req) + + def _authorize_unconfirmed_identity(self, req, obj, referrers, roles): + """" + Perform authorization for access that does not require a + confirmed identity. + + :returns: A boolean if authorization is granted or denied. None if + a determination could not be made. + """ + # Allow container sync. + if (req.environ.get('swift_sync_key') + and req.environ['swift_sync_key'] == + req.headers.get('x-container-sync-key', None) + and 'x-timestamp' in req.headers + and (req.remote_addr in self.allowed_sync_hosts + or swift_utils.get_remote_client(req) + in self.allowed_sync_hosts)): + log_msg = 'allowing proxy %s for container-sync' % req.remote_addr + self.logger.debug(log_msg) + return True + + # Check if referrer is allowed. + if swift_acl.referrer_allowed(req.referer, referrers): + if obj or '.rlistings' in roles: + log_msg = 'authorizing %s via referer ACL' % req.referrer + self.logger.debug(log_msg) + return True + return False + def denied_response(self, req): """Deny WSGI Response. From 63d904963bff64e05910823d2df58881e3aa93c6 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Fri, 30 Mar 2012 22:04:16 -0700 Subject: [PATCH 049/121] additional logging to support debugging auth issue fixes bug 969801 Change-Id: Iaf752e5f3692c91030cfd8575114f2c3293d1dba --- auth_token.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 6b8885b4..92c889d5 100644 --- a/auth_token.py +++ b/auth_token.py @@ -224,6 +224,7 @@ class AuthProtocol(object): if token: return token else: + LOG.warn("Unable to find authentication token in headers: %s", env) raise InvalidUserToken('Unable to find token in headers') def _reject_request(self, env, start_response): @@ -324,6 +325,7 @@ class AuthProtocol(object): assert token return token except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) raise ServiceError('invalid json response') def _validate_user_token(self, user_token, retry=True): @@ -354,9 +356,10 @@ class AuthProtocol(object): # FIXME(ja): I'm assuming the 404 status means that user_token is # invalid - not that the admin_token is invalid self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) raise InvalidUserToken('Token authorization failed') if response.status == 401: - LOG.info('Keystone rejected admin token, resetting') + LOG.info('Keystone rejected admin token %s, resetting', headers) self.admin_token = None else: LOG.error('Bad response code while validating token: %s' % @@ -365,6 +368,9 @@ class AuthProtocol(object): LOG.info('Retrying validation') return self._validate_user_token(user_token, False) else: + LOG.warn("Invalid user token: %s. Keystone response: %s.", + user_token, data) + raise InvalidUserToken() def _build_user_headers(self, token_info): From 8a465ae6b8d3fbca444aab791ec14f07d169f73f Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Mon, 2 Apr 2012 17:15:47 +0200 Subject: [PATCH 050/121] Add a _ at the end of reseller_prefix default. - Fixes bug 971592. Change-Id: Ic9edb2b8b0043413e4ec16de9c669646ae4230a6 --- swift_auth.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/swift_auth.py b/swift_auth.py index b5f0a9b1..19f8cab9 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -77,6 +77,15 @@ class SwiftAuth(object): hellocorp that user will be admin on that account and can give ACL to all other users for hellocorp. + If you need to have a different reseller_prefix to be able to + mix different auth servers you can configure the option + reseller_prefix in your swiftauth entry like this : + + reseller_prefix = NEWAUTH_ + + Make sure you have a underscore at the end of your new + reseller_prefix option. + :param app: The next WSGI app in the pipeline :param conf: The dict of configuration values """ @@ -84,7 +93,7 @@ class SwiftAuth(object): self.app = app self.conf = conf self.logger = swift_utils.get_logger(conf, log_route='keystoneauth') - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip() + self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_').strip() self.operator_roles = conf.get('operator_roles', 'admin, swiftoperator') self.reseller_admin_role = conf.get('reseller_admin_role', From ef33dcba38f1fd70e2afbe1fd3e87f41964d9f79 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Wed, 4 Apr 2012 17:30:51 +0200 Subject: [PATCH 051/121] Exit on error in a S3 way. - Fixes bug 973433. - Add more debug logging. - Test xml output of s3_token. Change-Id: Ibd0714bb6eeb75cfde614043fc7d062f584d0714 --- s3_token.py | 51 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/s3_token.py b/s3_token.py index 4ef9d814..d2226771 100644 --- a/s3_token.py +++ b/s3_token.py @@ -41,7 +41,7 @@ import webob from swift.common import utils as swift_utils -PROTOCOL_NAME = "S3 Token Authentication" +PROTOCOL_NAME = 'S3 Token Authentication' class ServiceError(Exception): @@ -54,11 +54,8 @@ class S3Token(object): def __init__(self, app, conf): """Common initialization code.""" self.app = app - self.logger = swift_utils.get_logger(conf, log_route='s3_token') + self.logger = swift_utils.get_logger(conf, log_route='s3token') self.logger.debug('Starting the %s component' % PROTOCOL_NAME) - - # NOTE(chmou): We probably want to make sure that there is a _ - # at the end of our reseller_prefix. self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_') # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') @@ -69,6 +66,21 @@ class S3Token(object): else: self.http_client_class = httplib.HTTPSConnection + def deny_request(self, code): + error_table = { + 'AccessDenied': + (401, 'Access denied'), + 'InvalidURI': + (400, 'Could not parse the specified URI'), + } + resp = webob.Response(content_type='text/xml') + resp.status = error_table[code][0] + resp.body = error_table[code][1] + resp.body = '\r\n\r\n ' \ + '%s\r\n %s\r\n\r\n' \ + % (code, error_table[code][1]) + return resp + def _json_request(self, creds_json): headers = {'Content-Type': 'application/json'} @@ -81,20 +93,23 @@ class S3Token(object): output = response.read() except Exception, e: self.logger.info('HTTP connection exception: %s' % e) - raise ServiceError('Unable to communicate with keystone') + resp = self.deny_request('InvalidURI') + raise ServiceError(resp) finally: conn.close() if response.status < 200 or response.status >= 300: - raise ServiceError('Keystone reply error: status=%s reason=%s' % ( - response.status, - response.reason)) + self.logger.debug('Keystone reply error: status=%s reason=%s' % + (response.status, response.reason)) + resp = self.deny_request('AccessDenied') + raise ServiceError(resp) return (response, output) def __call__(self, environ, start_response): """Handle incoming request. authenticate and send downstream.""" req = webob.Request(environ) + self.logger.debug('Calling S3Token middleware.') try: parts = swift_utils.split_path(req.path, 1, 4, True) @@ -123,7 +138,7 @@ class S3Token(object): except(ValueError): msg = 'You have an invalid Authorization header: %s' self.logger.debug(msg % (auth_header)) - return webob.exc.HTTPBadRequest()(environ, start_response) + return self.deny_request('InvalidURI')(environ, start_response) # NOTE(chmou): This is to handle the special case with nova # when we have the option s3_affix_tenant. We will force it to @@ -145,7 +160,8 @@ class S3Token(object): 'token': token, 'signature': signature}} creds_json = json.dumps(creds) - + self.logger.debug('Connecting to Keystone sending this JSON: %s' % + creds_json) # NOTE(vish): We could save a call to keystone by having # keystone return token, tenant, user, and roles # from this call. @@ -154,7 +170,16 @@ class S3Token(object): # change token_auth to detect if we already # identified and not doing a second query and just # pass it thru to swiftauth in this case. - (resp, output) = self._json_request(creds_json) + try: + resp, output = self._json_request(creds_json) + except ServiceError as e: + resp = e.args[0] + msg = 'Received error, exiting middleware with error: %s' + self.logger.debug(msg % (resp.status)) + return resp(environ, start_response) + + self.logger.debug('Keystone Reply: Status: %d, Output: %s' % ( + resp.status, output)) try: identity_info = json.loads(output) @@ -163,7 +188,7 @@ class S3Token(object): except (ValueError, KeyError): error = 'Error on keystone reply: %d %s' self.logger.debug(error % (resp.status, str(output))) - return webob.exc.HTTPBadRequest()(environ, start_response) + return self.deny_request('InvalidURI')(environ, start_response) req.headers['X-Auth-Token'] = token_id tenant_to_connect = force_tenant or tenant['id'] From 8e765731b90c9b2116313efb7bb38370d51d328f Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Mon, 14 May 2012 11:33:54 +0000 Subject: [PATCH 052/121] Make sure we parse delay_auth_decision as boolean. - Fixes bug 995222. - Documentation had already a false which is correct, updating the bug. Change-Id: I08625d8fa07c05b25c851c1df327cbdf660bd614 --- auth_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 92c889d5..b7a793c2 100644 --- a/auth_token.py +++ b/auth_token.py @@ -120,7 +120,8 @@ class AuthProtocol(object): # delay_auth_decision means we still allow unauthenticated requests # through and we let the downstream service make the final decision - self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) + self.delay_auth_decision = (conf.get('delay_auth_decision', False) + in ('true', 't', '1', 'on', 'yes', 'y')) # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') From 5789730c71d14c14cc02857e771030057e299e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Dur=C3=A1n=20Casta=C3=B1eda?= Date: Mon, 19 Mar 2012 01:44:31 +0100 Subject: [PATCH 053/121] Added 'NormalizingFilter' middleware. Fixes bug 956954. Change-Id: Ib5995a01439e564fcb27682976e8e27c8bb7d0d1 --- core.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core.py b/core.py index 7faa3f01..62af3353 100644 --- a/core.py +++ b/core.py @@ -148,3 +148,17 @@ class XmlBodyMiddleware(wsgi.Middleware): except: raise exception.Error(message=response.body) return response + + +class NormalizingFilter(wsgi.Middleware): + """Middleware filter to handle URL normalization.""" + + def process_request(self, request): + """Normalizes URLs.""" + # Removes a trailing slash from the given path, if any. + if len(request.environ['PATH_INFO']) > 1 and \ + request.environ['PATH_INFO'][-1] == '/': + request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1] + # Rewrites path to root if no path is given. + elif not request.environ['PATH_INFO']: + request.environ['PATH_INFO'] = '/' From f80d231a8eef09ef46c4e87edf59fee47d5ce187 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Thu, 17 May 2012 15:14:49 +0800 Subject: [PATCH 054/121] Backslash continuation removal (Keystone folsom-1) Fixes bug #1000608 Remove backslash continuations (except sqlalchemy and mox related code) Change-Id: I72eded8b49783937b7066f03fc61da6439edb82c --- s3_token.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/s3_token.py b/s3_token.py index d2226771..19953acd 100644 --- a/s3_token.py +++ b/s3_token.py @@ -76,9 +76,10 @@ class S3Token(object): resp = webob.Response(content_type='text/xml') resp.status = error_table[code][0] resp.body = error_table[code][1] - resp.body = '\r\n\r\n ' \ - '%s\r\n %s\r\n\r\n' \ - % (code, error_table[code][1]) + resp.body = ('\r\n' + '\r\n %s\r\n ' + '%s\r\n\r\n' % + (code, error_table[code][1])) return resp def _json_request(self, creds_json): From 7d553727100fe242e7dd428e6554846d552864c6 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Tue, 15 May 2012 17:24:03 +0200 Subject: [PATCH 055/121] Allow other middleware overriding authentication. - Implements blueprint swift-middleware-add-overrides-feature. - Let other middleware do authentication for certain request allow tempurl or formpost to temporary allow access for certain object and do the validation in there. Change-Id: I4f5bcb5832f96ced2c6c10d565737ae894771690 --- swift_auth.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 19f8cab9..d4be9f1f 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -46,10 +46,10 @@ class SwiftAuth(object): Make sure you have the authtoken middleware before the swiftauth middleware. authtoken will take care of validating the user and swiftauth will authorize access. If support is required for - unvalidated users (as with anonymous access), authtoken will need - to be configured with delay_auth_decision set to true. See the - documentation for more detail on how to configure the authtoken - middleware. + unvalidated users (as with anonymous access) or for + tempurl/formpost middleware, authtoken will need to be configured with + delay_auth_decision set to 1. See the documentation for more + detail on how to configure the authtoken middleware. Set account auto creation to true:: @@ -99,14 +99,25 @@ class SwiftAuth(object): self.reseller_admin_role = conf.get('reseller_admin_role', 'ResellerAdmin') config_is_admin = conf.get('is_admin', "false").lower() - self.is_admin = config_is_admin in ('true', 't', '1', 'on', 'yes', 'y') + self.is_admin = config_is_admin in swift_utils.TRUE_VALUES cfg_synchosts = conf.get('allowed_sync_hosts', '127.0.0.1') self.allowed_sync_hosts = [h.strip() for h in cfg_synchosts.split(',') if h.strip()] + config_overrides = conf.get('allow_overrides', 't').lower() + self.allow_overrides = config_overrides in swift_utils.TRUE_VALUES def __call__(self, environ, start_response): identity = self._keystone_identity(environ) + # Check if one of the middleware like tempurl or formpost have + # set the swift.authorize_override environ and want to control the + # authentication + if (self.allow_overrides and + environ.get('swift.authorize_override', False)): + msg = 'Authorizing from an overriding middleware (i.e: tempurl)' + self.logger.debug(msg) + return self.app(environ, start_response) + if identity: self.logger.debug('Using identity: %r' % (identity)) environ['keystone.identity'] = identity From 99279a17b8e411439bb2f766e109ff245e1b1c1e Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Tue, 22 May 2012 15:01:38 +0000 Subject: [PATCH 056/121] Use X_USER_NAME and X_ROLES headers. - Don't use deprecated headers X_USER and X_ROLE but the newest one X_USER_NAME and X_ROLES. - Fixes bug 999447. Change-Id: I12752c7668863cbb47ee4b6e484cc494133443e8 --- swift_auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 19f8cab9..08c00909 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -125,9 +125,9 @@ class SwiftAuth(object): if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed': return roles = [] - if 'HTTP_X_ROLE' in environ: - roles = environ['HTTP_X_ROLE'].split(',') - identity = {'user': environ.get('HTTP_X_USER'), + if 'HTTP_X_ROLES' in environ: + roles = environ['HTTP_X_ROLES'].split(',') + identity = {'user': environ.get('HTTP_X_USER_NAME'), 'tenant': (environ.get('HTTP_X_TENANT_ID'), environ.get('HTTP_X_TENANT_NAME')), 'roles': roles} From b687623c2b3816d7cafb2f011fe825c70f32b07e Mon Sep 17 00:00:00 2001 From: Lin Hua Cheng Date: Mon, 21 May 2012 22:46:38 -0700 Subject: [PATCH 057/121] Add ACL check using : format. Fixes bug 999998. Swift auth middleware uses a new format for expressing a container ACL for a user: :. This fix add supports for checking ACL using the old format of :. Change-Id: I44985b191afb174605c35041741056ae1e78fa77 --- swift_auth.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 19f8cab9..19ef6ab9 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -196,9 +196,11 @@ class SwiftAuth(object): return self.denied_response(req) # Allow ACL at individual user level (tenant:user format) - if '%s:%s' % (tenant_name, user) in roles: - log_msg = 'user %s:%s allowed in ACL authorizing' - self.logger.debug(log_msg % (tenant_name, user)) + # For backward compatibility, check for ACL in tenant_id:user format + if ('%s:%s' % (tenant_name, user) in roles + or '%s:%s' % (tenant_id, user) in roles): + log_msg = 'user %s:%s or %s:%s allowed in ACL authorizing' + self.logger.debug(log_msg % (tenant_name, user, tenant_id, user)) return # Check if we have the role in the userroles and allow it From f98fb256805c3fb6d7d391d4abf1573dc726413c Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Tue, 3 Apr 2012 21:06:02 +0200 Subject: [PATCH 058/121] Update swift_auth documentation. - Make it consistent between the source documentation and the rst documentation. - Note about the default being https. Change-Id: Ic78ef79198eee9b514bb52fce12d7224e9ab65ae --- swift_auth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/swift_auth.py b/swift_auth.py index 19f8cab9..be6d639e 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -41,7 +41,7 @@ class SwiftAuth(object): In Swift's proxy-server.conf add this middleware to your pipeline:: [pipeline:main] - pipeline = catch_errors cache authtoken swiftauth proxy-server + pipeline = catch_errors cache authtoken keystone proxy-server Make sure you have the authtoken middleware before the swiftauth middleware. authtoken will take care of validating the user and @@ -58,10 +58,9 @@ class SwiftAuth(object): And add a swift authorization filter section, such as:: - [filter:swiftauth] - use = egg:keystone#swiftauth + [filter:keystone] + paste.filter_factory = keystone.middleware.auth_token:filter_factory operator_roles = admin, swiftoperator - is_admin = false This maps tenants to account in Swift. From 3f2ee1592e5e85ea2cd1699ace4e1ddf54ff3ff0 Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Thu, 24 May 2012 16:14:35 +0200 Subject: [PATCH 059/121] Fixes some pep8 warning/errors. - Using flake8 so a bit more than that. Change-Id: I63fa21f7d3d02f96c0c56804fdd56da37c952d7d --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 92c889d5..869c147d 100644 --- a/auth_token.py +++ b/auth_token.py @@ -444,7 +444,7 @@ class AuthProtocol(object): :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') """ - return 'HTTP_%s' % key.replace('-', '_').upper() + return 'HTTP_%s' % key.replace('-', '_').upper() def _add_headers(self, env, headers): """Add http headers to environment.""" From a85f376b59b0cf7a4563cfb28fe12827b7f9dd93 Mon Sep 17 00:00:00 2001 From: Liem Nguyen Date: Wed, 23 May 2012 18:05:11 +0000 Subject: [PATCH 060/121] blueprint 2-way-ssl Implemented bp/2-way-ssl using eventlet-based SSL. Change-Id: I5aeb622aded13b406e01c78a2d8c245543306180 --- auth_token.py | 16 ++++++++++++---- s3_token.py | 13 ++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/auth_token.py b/auth_token.py index 92c889d5..dd91fa48 100644 --- a/auth_token.py +++ b/auth_token.py @@ -125,17 +125,21 @@ class AuthProtocol(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port', 35357)) - auth_protocol = conf.get('auth_protocol', 'https') - if auth_protocol == 'http': + self.auth_protocol = conf.get('auth_protocol', 'https') + if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection - default_auth_uri = '%s://%s:%s' % (auth_protocol, + default_auth_uri = '%s://%s:%s' % (self.auth_protocol, self.auth_host, self.auth_port) self.auth_uri = conf.get('auth_uri', default_auth_uri) + # SSL + self.cert_file = conf.get('certfile') + self.key_file = conf.get('keyfile') + # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') @@ -252,7 +256,11 @@ class AuthProtocol(object): return self.admin_token def _get_http_connection(self): - return self.http_client_class(self.auth_host, self.auth_port) + if self.auth_protocol == 'http': + return self.http_client_class(self.auth_host, self.auth_port) + else: + return self.http_client_class(self.auth_host, self.auth_port, + self.key_file, self.cert_file) def _json_request(self, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. diff --git a/s3_token.py b/s3_token.py index 19953acd..a4f1f09f 100644 --- a/s3_token.py +++ b/s3_token.py @@ -60,11 +60,14 @@ class S3Token(object): # where to find the auth service (we use this to validate tokens) self.auth_host = conf.get('auth_host') self.auth_port = int(conf.get('auth_port', 35357)) - auth_protocol = conf.get('auth_protocol', 'https') - if auth_protocol == 'http': + self.auth_protocol = conf.get('auth_protocol', 'https') + if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection + # SSL + self.cert_file = conf.get('certfile') + self.key_file = conf.get('keyfile') def deny_request(self, code): error_table = { @@ -86,7 +89,11 @@ class S3Token(object): headers = {'Content-Type': 'application/json'} try: - conn = self.http_client_class(self.auth_host, self.auth_port) + if self.auth_protocol == 'http': + conn = self.http_client_class(self.auth_host, self.auth_port) + else: + conn = self.http_client_class(self.auth_host, self.auth_port, + self.key_file, self.cert_file) conn.request('POST', '/v2.0/s3tokens', body=creds_json, headers=headers) From 75fe43a34e70153d593533bc8c7a77f1bdebced9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draig=20Brady?= Date: Wed, 23 May 2012 23:52:49 +0100 Subject: [PATCH 061/121] fix importing of optional modules in auth_token * keystone/middleware/auth_token.py: Catch the correct exception so that the memcache and iso8601 modules can be optional as intended. * tests/test_auth_token_middleware.py: Test the ImportError path * keystone/test.py: Add a new mixin class to support disabling importing of a module. Bug: 1003715 Change-Id: I87cc2f3bc79b17a52ea672bac7e0ebcf9e1fce57 --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index a6f2af66..1551883d 100644 --- a/auth_token.py +++ b/auth_token.py @@ -161,7 +161,7 @@ class AuthProtocol(object): LOG.info('Using memcache for caching token') self._cache = memcache.Client(memcache_servers.split(',')) self._iso8601 = iso8601 - except NameError as e: + except ImportError as e: LOG.warn('disabled caching due to missing libraries %s', e) def __call__(self, env, start_response): From f4ad57400bed682657d2529bece4c63de54d47a6 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Mon, 18 Jun 2012 14:16:34 -0500 Subject: [PATCH 062/121] PEP8 fixes Change-Id: I0989396691eb31d9008c016e64f2c197f8c7e48c --- auth_token.py | 10 ++++++---- core.py | 4 ++-- ec2_token.py | 17 ++++++++++------- s3_token.py | 16 ++++++++-------- swift_auth.py | 4 ++-- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/auth_token.py b/auth_token.py index 1551883d..419b757d 100644 --- a/auth_token.py +++ b/auth_token.py @@ -213,7 +213,7 @@ class AuthProtocol(object): 'X-Role', ) LOG.debug('Removing headers from request environment: %s' % - ','.join(auth_headers)) + ','.join(auth_headers)) self._remove_headers(env, auth_headers) def _get_user_token_from_header(self, env): @@ -260,8 +260,10 @@ class AuthProtocol(object): if self.auth_protocol == 'http': return self.http_client_class(self.auth_host, self.auth_port) else: - return self.http_client_class(self.auth_host, self.auth_port, - self.key_file, self.cert_file) + return self.http_client_class(self.auth_host, + self.auth_port, + self.key_file, + self.cert_file) def _json_request(self, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. @@ -372,7 +374,7 @@ class AuthProtocol(object): self.admin_token = None else: LOG.error('Bad response code while validating token: %s' % - response.status) + response.status) if retry: LOG.info('Retrying validation') return self._validate_user_token(user_token, False) diff --git a/core.py b/core.py index 62af3353..0f6c1e63 100644 --- a/core.py +++ b/core.py @@ -156,8 +156,8 @@ class NormalizingFilter(wsgi.Middleware): def process_request(self, request): """Normalizes URLs.""" # Removes a trailing slash from the given path, if any. - if len(request.environ['PATH_INFO']) > 1 and \ - request.environ['PATH_INFO'][-1] == '/': + if (len(request.environ['PATH_INFO']) > 1 and + request.environ['PATH_INFO'][-1] == '/'): request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1] # Rewrites path to root if no path is given. elif not request.environ['PATH_INFO']: diff --git a/ec2_token.py b/ec2_token.py index 264f9f07..daac10aa 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -57,13 +57,16 @@ class EC2Token(wsgi.Middleware): 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 = { + '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'} diff --git a/s3_token.py b/s3_token.py index a4f1f09f..bdb2bf78 100644 --- a/s3_token.py +++ b/s3_token.py @@ -71,11 +71,9 @@ class S3Token(object): def deny_request(self, code): error_table = { - 'AccessDenied': - (401, 'Access denied'), - 'InvalidURI': - (400, 'Could not parse the specified URI'), - } + 'AccessDenied': (401, 'Access denied'), + 'InvalidURI': (400, 'Could not parse the specified URI'), + } resp = webob.Response(content_type='text/xml') resp.status = error_table[code][0] resp.body = error_table[code][1] @@ -92,8 +90,10 @@ class S3Token(object): if self.auth_protocol == 'http': conn = self.http_client_class(self.auth_host, self.auth_port) else: - conn = self.http_client_class(self.auth_host, self.auth_port, - self.key_file, self.cert_file) + conn = self.http_client_class(self.auth_host, + self.auth_port, + self.key_file, + self.cert_file) conn.request('POST', '/v2.0/s3tokens', body=creds_json, headers=headers) @@ -187,7 +187,7 @@ class S3Token(object): return resp(environ, start_response) self.logger.debug('Keystone Reply: Status: %d, Output: %s' % ( - resp.status, output)) + resp.status, output)) try: identity_info = json.loads(output) diff --git a/swift_auth.py b/swift_auth.py index dfdf8fe3..569911ac 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -112,7 +112,7 @@ class SwiftAuth(object): # set the swift.authorize_override environ and want to control the # authentication if (self.allow_overrides and - environ.get('swift.authorize_override', False)): + environ.get('swift.authorize_override', False)): msg = 'Authorizing from an overriding middleware (i.e: tempurl)' self.logger.debug(msg) return self.app(environ, start_response) @@ -208,7 +208,7 @@ class SwiftAuth(object): # Allow ACL at individual user level (tenant:user format) # For backward compatibility, check for ACL in tenant_id:user format if ('%s:%s' % (tenant_name, user) in roles - or '%s:%s' % (tenant_id, user) in roles): + or '%s:%s' % (tenant_id, user) in roles): log_msg = 'user %s:%s or %s:%s allowed in ACL authorizing' self.logger.debug(log_msg % (tenant_name, user, tenant_id, user)) return From eca8cf9e669d4b842f19c7a3f7fd62055cd4f405 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 14 Jun 2012 13:47:06 -0500 Subject: [PATCH 063/121] 400 on unrecognized content type (bug 1012282) Unrecognized content type: http://paste.openstack.org/raw/18537/ Malformed JSON: http://paste.openstack.org/raw/18536/ Change-Id: I76afbf9300bcb1c11bed74eddbe4972c451c5877 --- core.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core.py b/core.py index 0f6c1e63..aa02c62b 100644 --- a/core.py +++ b/core.py @@ -98,21 +98,25 @@ class JsonBodyMiddleware(wsgi.Middleware): """ def process_request(self, request): - # Ignore unrecognized content types. Empty string indicates - # the client did not explicitly set the header - if not request.content_type in ('application/json', ''): - return - + # Abort early if we don't have any work to do params_json = request.body if not params_json: return + # Reject unrecognized content types. Empty string indicates + # the client did not explicitly set the header + if not request.content_type in ('application/json', ''): + e = exception.ValidationError(attribute='application/json', + target='Content-Type header') + return wsgi.render_exception(e) + params_parsed = {} try: params_parsed = json.loads(params_json) except ValueError: - msg = 'Malformed json in request body' - raise webob.exc.HTTPBadRequest(explanation=msg) + e = exception.ValidationError(attribute='valid JSON', + target='request body') + return wsgi.render_exception(e) finally: if not params_parsed: params_parsed = {} From af6e6631aafbbba9fee2ffca1a3f780829c98ef7 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Thu, 7 Jun 2012 16:00:38 -0700 Subject: [PATCH 064/121] Pass serviceCatalog in auth_token middleware * This will allow for chained requests (novaclient -> nova -> cinder) * Fixes bug 1010237 Change-Id: Iab126cb1f2fb01ca7da24fa9fe97ec81ee96e455 --- auth_token.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 419b757d..5471e1f1 100644 --- a/auth_token.py +++ b/auth_token.py @@ -76,6 +76,9 @@ HTTP_X_USER_NAME HTTP_X_ROLES Comma delimited list of case-sensitive Roles +HTTP_X_SERVICE_CATALOG + json encoded keystone service catalog (optional). + HTTP_X_TENANT *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME Keystone-assigned unique identifier, deprecated @@ -394,6 +397,7 @@ class AuthProtocol(object): * X_USER_ID: id of user * X_USER_NAME: name of user * X_ROLES: list of roles + * X_SERVICE_CATALOG: service catalog Additional (deprecated) headers include: * X_USER: name of user @@ -435,7 +439,7 @@ class AuthProtocol(object): user_id = user['id'] user_name = user['name'] - return { + rval = { 'X-Identity-Status': 'Confirmed', 'X-Tenant-Id': tenant_id, 'X-Tenant-Name': tenant_name, @@ -448,6 +452,14 @@ class AuthProtocol(object): 'X-Role': roles, } + try: + catalog = token_info['access']['serviceCatalog'] + rval['X-Service-Catalog'] = json.dumps(catalog) + except KeyError: + pass + + return rval + def _header_to_env_var(self, key): """Convert header to wsgi env variable. From 9e30e4eebddc6763733508de15a91c2fc04ff4db Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Fri, 15 Jun 2012 08:32:41 +0800 Subject: [PATCH 065/121] Reorder imports by full module path Fixes bug #1013441 Sort imports by lexicographical order of full module path Change-Id: I60231d87618466426dc7bfac7bb0644a0dbd079a --- core.py | 4 ++-- swift_auth.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core.py b/core.py index aa02c62b..db571667 100644 --- a/core.py +++ b/core.py @@ -18,10 +18,10 @@ import json import webob.exc -from keystone import config -from keystone import exception from keystone.common import serializer from keystone.common import wsgi +from keystone import config +from keystone import exception CONF = config.CONF diff --git a/swift_auth.py b/swift_auth.py index 569911ac..ddcec4af 100644 --- a/swift_auth.py +++ b/swift_auth.py @@ -31,8 +31,8 @@ import webob -from swift.common import utils as swift_utils from swift.common.middleware import acl as swift_acl +from swift.common import utils as swift_utils class SwiftAuth(object): From d64420a65b2e4307eb9af52431a686cb0c88161e Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 20 Jun 2012 13:13:35 -0500 Subject: [PATCH 066/121] Removed unused import Change-Id: I9fec34122ca28ac9d2d9866cfe6ab203998d177d --- core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/core.py b/core.py index db571667..a35adb0f 100644 --- a/core.py +++ b/core.py @@ -16,8 +16,6 @@ import json -import webob.exc - from keystone.common import serializer from keystone.common import wsgi from keystone import config From 37e7195273ea65f8d83a2f9df70b0a6e414cb195 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Thu, 7 Jun 2012 12:28:26 +0800 Subject: [PATCH 067/121] Keystone should use openstack.common.jsonutils Implements blueprint use-common-jsonutils 1. Edit openstack-common.conf and import keystone/openstack/common/jsonutils.py 2. Remove json package imports and replace with jsonutils Client code in vendor/ hasn't been changed Change-Id: I57c670fde9f2c2241eddab1b012e8d5e6a72deb7 --- auth_token.py | 9 +++++---- core.py | 10 +++++----- s3_token.py | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/auth_token.py b/auth_token.py index 5471e1f1..b383aaf9 100644 --- a/auth_token.py +++ b/auth_token.py @@ -94,13 +94,14 @@ HTTP_X_ROLE """ import httplib -import json import logging import time import webob import webob.exc +from keystone.openstack.common import jsonutils + LOG = logging.getLogger(__name__) @@ -293,7 +294,7 @@ class AuthProtocol(object): kwargs['headers'].update(additional_headers) if body: - kwargs['body'] = json.dumps(body) + kwargs['body'] = jsonutils.dumps(body) try: conn.request(method, path, **kwargs) @@ -306,7 +307,7 @@ class AuthProtocol(object): conn.close() try: - data = json.loads(body) + data = jsonutils.loads(body) except ValueError: LOG.debug('Keystone did not return json-encoded body') data = {} @@ -454,7 +455,7 @@ class AuthProtocol(object): try: catalog = token_info['access']['serviceCatalog'] - rval['X-Service-Catalog'] = json.dumps(catalog) + rval['X-Service-Catalog'] = jsonutils.dumps(catalog) except KeyError: pass diff --git a/core.py b/core.py index a35adb0f..f6b13f71 100644 --- a/core.py +++ b/core.py @@ -14,12 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. -import json - from keystone.common import serializer from keystone.common import wsgi from keystone import config from keystone import exception +from keystone.openstack.common import jsonutils CONF = config.CONF @@ -110,7 +109,7 @@ class JsonBodyMiddleware(wsgi.Middleware): params_parsed = {} try: - params_parsed = json.loads(params_json) + params_parsed = jsonutils.loads(params_json) except ValueError: e = exception.ValidationError(attribute='valid JSON', target='request body') @@ -138,7 +137,7 @@ class XmlBodyMiddleware(wsgi.Middleware): incoming_xml = 'application/xml' in str(request.content_type) if incoming_xml and request.body: request.content_type = 'application/json' - request.body = json.dumps(serializer.from_xml(request.body)) + request.body = jsonutils.dumps(serializer.from_xml(request.body)) def process_response(self, request, response): """Transform the response from JSON to XML.""" @@ -146,7 +145,8 @@ class XmlBodyMiddleware(wsgi.Middleware): if outgoing_xml and response.body: response.content_type = 'application/xml' try: - response.body = serializer.to_xml(json.loads(response.body)) + body_obj = jsonutils.loads(response.body) + response.body = serializer.to_xml(body_obj) except: raise exception.Error(message=response.body) return response diff --git a/s3_token.py b/s3_token.py index bdb2bf78..7cf2e394 100644 --- a/s3_token.py +++ b/s3_token.py @@ -34,10 +34,10 @@ This WSGI component: """ import httplib -import json import webob +from keystone.openstack.common import jsonutils from swift.common import utils as swift_utils @@ -167,7 +167,7 @@ class S3Token(object): creds = {'credentials': {'access': access, 'token': token, 'signature': signature}} - creds_json = json.dumps(creds) + creds_json = jsonutils.dumps(creds) self.logger.debug('Connecting to Keystone sending this JSON: %s' % creds_json) # NOTE(vish): We could save a call to keystone by having @@ -190,7 +190,7 @@ class S3Token(object): resp.status, output)) try: - identity_info = json.loads(output) + identity_info = jsonutils.loads(output) token_id = str(identity_info['access']['token']['id']) tenant = identity_info['access']['token']['tenant'] except (ValueError, KeyError): From e0c292f639679bed9af09c64ccf354fda73049fc Mon Sep 17 00:00:00 2001 From: ayoung Date: Sat, 5 May 2012 14:08:18 -0400 Subject: [PATCH 068/121] Admin Auth URI prefix Allows the prepending of a prefix to the URI used for admin tasks. This allows URIs like https://hostname/keystone/main/v2.0 PEP8 fix Added To Unit test to ensure auth_prefix is checked Bug: 994860 Change-Id: I851e059e8b17c1bc02ab93d8b09a3fb47b9d3fee --- auth_token.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index b383aaf9..d504e20c 100644 --- a/auth_token.py +++ b/auth_token.py @@ -139,6 +139,7 @@ class AuthProtocol(object): default_auth_uri = '%s://%s:%s' % (self.auth_protocol, self.auth_host, self.auth_port) + self.auth_admin_prefix = conf.get('auth_admin_prefix', '') self.auth_uri = conf.get('auth_uri', default_auth_uri) # SSL @@ -296,8 +297,9 @@ class AuthProtocol(object): if body: kwargs['body'] = jsonutils.dumps(body) + full_path = self.auth_admin_prefix + path try: - conn.request(method, path, **kwargs) + conn.request(method, full_path, **kwargs) response = conn.getresponse() body = response.read() except Exception, e: From 12078c1d3fcbc389f55d22d41321a3ce04676cc4 Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Thu, 12 Jul 2012 13:48:43 -0400 Subject: [PATCH 069/121] Prevent service catalog injection in auth_token. Updates the auth_token middleware to explicitly prevent X-Service-Catalog headers from being injected into responses. In general Keystone would override these with its own service catalog... however since X-Service-Catalog is optional and not all implementations/calls return it is good to be safe and just remove incoming X-Service-Catalog headers if they are set. Fixes LP Bug #1023998. Change-Id: I9497937abd1b434b42b40bc943a508dd7f1a3585 --- auth_token.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auth_token.py b/auth_token.py index b383aaf9..0fa35fde 100644 --- a/auth_token.py +++ b/auth_token.py @@ -211,6 +211,7 @@ class AuthProtocol(object): 'X-User-Id', 'X-User-Name', 'X-Roles', + 'X-Service-Catalog', # Deprecated 'X-User', 'X-Tenant', From d0f5300fcad9985b18e85bbaa06225f35d136a5f Mon Sep 17 00:00:00 2001 From: Adam Young Date: Mon, 2 Jul 2012 22:18:36 -0400 Subject: [PATCH 070/121] Cryptographically Signed tokens Uses CMS to create tokens that can be verified without network calls. Tokens encapsulate authorization information. This includes user name and roles in JSON. The JSON document info is cryptographically signed with a private key from Keystone, in accordance with the Cryptographic Message Syntax (CMS) in DER format and then Base64 encoded. The header, footer, and line breaks are stripped to minimize the size, and slashes which are invalid in Base64 are converted to hyphens. Since signed tokens are not validated against the Keystone server, they continue to be valid until the expiration time. This means that even if a user has their roles revoked or their account disabled, those changes will not take effect until their token times out. The prototype for this is Kerberos, which has the same limitation, and has funtioned sucessfully with it for decades. It is possible to set the token time out for much shorter than the default of 8 hours, but that may mean that users tokens will time out prior to completion of long running tasks. This should be a drop in replacement for the current token production code. Although the signed token is longer than the older format, the token is still a unique stream of Alpha-Numeric characters. The auth token middle_ware is capable of handling both uuid and signed tokens. To start with, the PKI functionality is disabled. This will keep from breaking the existing deployments. However, it can be enabled with the config value: [signing] disable_pki = False The 'id_hash' column is added to the SQL schema because SQL alchemy insists on each table having a primary key. However primary keys are limited to roughly 250 Characters (768 Bytes, but there is more than 1 varchar per byte) so the ID field cannot be used as the primary key anymore. id_hash is a hash of the id column, and should be used for lookups as it is indexed. middleware/auth_token.py needs to stand alone in the other services, and uses keystone.common.cms in order to verify tokens. Token needs to have all of the data from the original authenticate code contained in the signed document, as the authenticate RPC will no longer be called in mand cases. The datetime of expiry is signed in the token. The certificates are accessible via web APIs. On the remote service side, certificates needed to authenitcate tokens are stored in /tmp/keystone-signing by default. Remote systems use Paste API to read configuration values. Certificates are retrieved only if they are not on the local system. When authenticating in Keystone systems, it still does the Database checks for token presence. This allows Keystone to continue to enforce Timeout and disabled users. The service catalog has been added to the signed token. Although this greatly increases the size of the token, it makes it consistant with what is fetched during the token authenticate checks This change also fixes time variations in expiry test. Although unrelated to the above changes, it was making testing very frustrating. For the database Upgrade scripts, we now only bring 'token' up to V1 in 001 script. This makes it possible to use the same 002 script for both upgrade and initializing a new database. Upon upgrade, the current UUID tokens are retained in the id_hash and id fields. The mechanisms to verify uuid tokens work the same as before. On downgrade, token_ids are dropped. Takes into account changes for "Raise unauthorized if tenant disabled" Bug 1003962 Change-Id: I89b5aa609143bbe09a36bfaf64758c5306e86de7 --- auth_token.py | 187 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 152 insertions(+), 35 deletions(-) diff --git a/auth_token.py b/auth_token.py index 3c379983..c82e5ef5 100644 --- a/auth_token.py +++ b/auth_token.py @@ -94,14 +94,17 @@ HTTP_X_ROLE """ import httplib +import json import logging +import os +import stat +import subprocess import time - import webob import webob.exc from keystone.openstack.common import jsonutils - +from keystone.common import cms LOG = logging.getLogger(__name__) @@ -146,6 +149,22 @@ class AuthProtocol(object): self.cert_file = conf.get('certfile') self.key_file = conf.get('keyfile') + #signing + self.signing_dirname = conf.get('signing_dir', '/tmp/keystone-signing') + if (os.path.exists(self.signing_dirname) and + not os.access(self.signing_dirname, os.W_OK)): + raise "TODO: Need to find an Exception to raise here." + + if not os.path.exists(self.signing_dirname): + os.makedirs(self.signing_dirname) + #will throw IOError if it cannot change permissions + os.chmod(self.signing_dirname, stat.S_IRWXU) + + val = '%s/signing_cert.pem' % self.signing_dirname + self.signing_cert_file_name = val + val = '%s/cacert.pem' % self.signing_dirname + self.ca_file_name = val + # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = conf.get('admin_token') @@ -271,6 +290,29 @@ class AuthProtocol(object): self.key_file, self.cert_file) + def _http_request(self, method, path): + """HTTP request helper used to make unspecified content type requests. + + :param method: http method + :param path: relative request url + :return (http response object) + :raise ServerError when unable to communicate with keystone + + """ + conn = self._get_http_connection() + + try: + conn.request(method, path) + response = conn.getresponse() + body = response.read() + except Exception, e: + LOG.error('HTTP connection exception: %s' % e) + raise ServiceError('Unable to communicate with keystone') + finally: + conn.close() + + return response, body + def _json_request(self, method, path, body=None, additional_headers=None): """HTTP request helper used to make json requests. @@ -347,49 +389,30 @@ class AuthProtocol(object): raise ServiceError('invalid json response') def _validate_user_token(self, user_token, retry=True): - """Authenticate user token with keystone. + """Authenticate user using PKI :param user_token: user's token id - :param retry: flag that forces the middleware to retry - user authentication when an indeterminate - response is received. Optional. - :return token object received from keystone on success + :param retry: Ignored, as it is not longer relevant + :return uncrypted body of the token if the token is valid :raise InvalidUserToken if token is rejected - :raise ServiceError if unable to authenticate token + :no longer raises ServiceError since it no longer makes RPC """ - cached = self._cache_get(user_token) - if cached: - return cached - - headers = {'X-Auth-Token': self.get_admin_token()} - response, data = self._json_request('GET', - '/v2.0/tokens/%s' % user_token, - additional_headers=headers) - - if response.status == 200: + try: + cached = self._cache_get(user_token) + if cached: + return cached + if (len(user_token) > cms.UUID_TOKEN_LENGTH): + verified = self.verify_signed_token(user_token) + data = json.loads(verified) + else: + data = self.verify_uuid_token(user_token, retry) self._cache_put(user_token, data) return data - if response.status == 404: - # FIXME(ja): I'm assuming the 404 status means that user_token is - # invalid - not that the admin_token is invalid + except Exception as e: self._cache_store_invalid(user_token) LOG.warn("Authorization failed for token %s", user_token) raise InvalidUserToken('Token authorization failed') - if response.status == 401: - LOG.info('Keystone rejected admin token %s, resetting', headers) - self.admin_token = None - else: - LOG.error('Bad response code while validating token: %s' % - response.status) - if retry: - LOG.info('Retrying validation') - return self._validate_user_token(user_token, False) - else: - LOG.warn("Invalid user token: %s. Keystone response: %s.", - user_token, data) - - raise InvalidUserToken() def _build_user_headers(self, token_info): """Convert token object into headers. @@ -541,6 +564,100 @@ class AuthProtocol(object): 'invalid', time=self.token_cache_time) + def cert_file_missing(self, called_proc_err, file_name): + return (called_proc_err.output.find(self.signing_cert_file_name) + and not os.path.exists(self.signing_cert_file_name)) + + def verify_uuid_token(self, user_token, retry=True): + """Authenticate user token with keystone. + + :param user_token: user's token id + :param retry: flag that forces the middleware to retry + user authentication when an indeterminate + response is received. Optional. + :return token object received from keystone on success + :raise InvalidUserToken if token is rejected + :raise ServiceError if unable to authenticate token + + """ + + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', + '/v2.0/tokens/%s' % user_token, + additional_headers=headers) + + if response.status == 200: + self._cache_put(user_token, data) + return data + if response.status == 404: + # FIXME(ja): I'm assuming the 404 status means that user_token is + # invalid - not that the admin_token is invalid + self._cache_store_invalid(user_token) + LOG.warn("Authorization failed for token %s", user_token) + raise InvalidUserToken('Token authorization failed') + if response.status == 401: + LOG.info('Keystone rejected admin token %s, resetting', headers) + self.admin_token = None + else: + LOG.error('Bad response code while validating token: %s' % + response.status) + if retry: + LOG.info('Retrying validation') + return self._validate_user_token(user_token, False) + else: + LOG.warn("Invalid user token: %s. Keystone response: %s.", + user_token, data) + + raise InvalidUserToken() + + def verify_signed_token(self, signed_text): + """ + Converts a block of Base64 encoding to strict PEM format + and verifies the signature of the contensts IAW CMS syntax + If either of the certificate files are missing, fetch them + and retry + """ + + formatted = cms.token_to_cms(signed_text) + + while True: + try: + output = cms.cms_verify(formatted, self.signing_cert_file_name, + self.ca_file_name) + except subprocess.CalledProcessError as err: + if self.cert_file_missing(err, self.signing_cert_file_name): + self.fetch_signing_cert() + continue + if self.cert_file_missing(err, self.ca_file_name): + self.fetch_ca_cert() + continue + raise err + return output + + def fetch_signing_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/signing') + try: + #todo check response + certfile = open(self.signing_cert_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + + def fetch_ca_cert(self): + response, data = self._http_request('GET', + '/v2.0/certificates/ca') + try: + #todo check response + certfile = open(self.ca_file_name, 'w') + certfile.write(data) + certfile.close() + except (AssertionError, KeyError): + LOG.warn("Unexpected response from keystone service: %s", data) + raise ServiceError('invalid json response') + def filter_factory(global_conf, **local_conf): """Returns a WSGI filter app for use with paste.deploy.""" From 6545f1afeae69f8b45a95fee328ce7c5b7beea17 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Mon, 30 Jul 2012 11:11:47 -0400 Subject: [PATCH 071/121] Test for Cert by name Fixes a typo in checking if cert file exists. Bug 1030912 Change-Id: Iea783aaa6bc425a17799d40cd6b378d90ebe6faf --- auth_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index c82e5ef5..e042dbb1 100644 --- a/auth_token.py +++ b/auth_token.py @@ -565,8 +565,8 @@ class AuthProtocol(object): time=self.token_cache_time) def cert_file_missing(self, called_proc_err, file_name): - return (called_proc_err.output.find(self.signing_cert_file_name) - and not os.path.exists(self.signing_cert_file_name)) + return (called_proc_err.output.find(file_name) + and not os.path.exists(file_name)) def verify_uuid_token(self, user_token, retry=True): """Authenticate user token with keystone. From 1d5d958ce438fead2f72c4329a3eef42d111c80d Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Mon, 30 Jul 2012 15:15:04 -0400 Subject: [PATCH 072/121] Set default signing_dir based on os USER. Updates the Keystone auth_token middleware so that it sets the default signing_dir name base on the OS username obtained from the environment. This should help resolve potential permissions issues which can occur when multiple OpenStack services attempt to use the same signing directory name. Fixes LP Bug #1031022. Change-Id: I53bceed27f60721b8f61ffec2d1e91ec2ea464ed --- auth_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index e042dbb1..3835f4c3 100644 --- a/auth_token.py +++ b/auth_token.py @@ -150,7 +150,8 @@ class AuthProtocol(object): self.key_file = conf.get('keyfile') #signing - self.signing_dirname = conf.get('signing_dir', '/tmp/keystone-signing') + default_signing_dir = '/tmp/keystone-signing-%s' % os.environ['USER'] + self.signing_dirname = conf.get('signing_dir', default_signing_dir) if (os.path.exists(self.signing_dirname) and not os.access(self.signing_dirname, os.W_OK)): raise "TODO: Need to find an Exception to raise here." From da9f643beb2ac8441a1c2df3112a6a9762aaf273 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Tue, 31 Jul 2012 16:41:47 -0400 Subject: [PATCH 073/121] Use user home dir as default for cache This is a better and safer default, as it and minimizes the possibility that the cache directory will be prepopulated or unwritable, while still providing a reasonable value for the individual developer Creates a better exception for failure to create the cache dir Logs the name of the cache dir actually used. Bug 1031022 Change-Id: Ia3718107e436ceb034e3a89318ac05265d66d6f1 --- auth_token.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index 3835f4c3..75ab67c7 100644 --- a/auth_token.py +++ b/auth_token.py @@ -117,6 +117,10 @@ class ServiceError(Exception): pass +class ConfigurationError(Exception): + pass + + class AuthProtocol(object): """Auth Middleware that handles authenticating client calls.""" @@ -150,11 +154,14 @@ class AuthProtocol(object): self.key_file = conf.get('keyfile') #signing - default_signing_dir = '/tmp/keystone-signing-%s' % os.environ['USER'] + default_signing_dir = '%s/keystone-signing' % os.environ['HOME'] self.signing_dirname = conf.get('signing_dir', default_signing_dir) + LOG.info('Using %s as cache directory for signing certificate' % + self.signing_dirname) if (os.path.exists(self.signing_dirname) and not os.access(self.signing_dirname, os.W_OK)): - raise "TODO: Need to find an Exception to raise here." + raise ConfigurationError("unable to access signing dir %s" % + self.signing_dirname) if not os.path.exists(self.signing_dirname): os.makedirs(self.signing_dirname) From 3b5f24a0ad70455fe6d71b1b06a52c888c79dd0b Mon Sep 17 00:00:00 2001 From: Maru Newby Date: Wed, 8 Aug 2012 20:49:23 -0400 Subject: [PATCH 074/121] PKI Token revocation Co-authored-by: Adam Young Token revocations are captured in the backends, During upgrade, all previous tickets are defaulted to valid. Revocation list returned as a signed document and can be fetched in an admin context via HTTP Change config values for enable diable PKI In the auth_token middleware, the revocation list is fetched prior to validating tokens. Any tokens that are on the revocation list will be treated as invalid. Added in PKI token tests that check the same logic as the UUID tests. Sample data for the tests is read out of the signing directory. dropped number on sql scripts to pass tests. Also fixes 1031373 Bug 1037683 Change-Id: Icef2f173e50fe3cce4273c161f69d41259bf5d23 --- auth_token.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/auth_token.py b/auth_token.py index 75ab67c7..ef449c67 100644 --- a/auth_token.py +++ b/auth_token.py @@ -93,6 +93,7 @@ HTTP_X_ROLE """ +import datetime import httplib import json import logging @@ -105,6 +106,8 @@ import webob.exc from keystone.openstack.common import jsonutils from keystone.common import cms +from keystone.common import utils +from keystone.openstack.common import timeutils LOG = logging.getLogger(__name__) @@ -172,6 +175,8 @@ class AuthProtocol(object): self.signing_cert_file_name = val val = '%s/cacert.pem' % self.signing_dirname self.ca_file_name = val + val = '%s/revoked.pem' % self.signing_dirname + self.revoked_file_name = val # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call @@ -186,6 +191,10 @@ class AuthProtocol(object): memcache_servers = conf.get('memcache_servers') # By default the token will be cached for 5 minutes self.token_cache_time = conf.get('token_cache_time', 300) + self._token_revocation_list = None + self._token_revocation_list_fetched_time = None + self.token_revocation_list_cache_timeout = \ + datetime.timedelta(seconds=0) if memcache_servers: try: import memcache @@ -418,6 +427,7 @@ class AuthProtocol(object): self._cache_put(user_token, data) return data except Exception as e: + LOG.debug('Token validation failure.', exc_info=True) self._cache_store_invalid(user_token) LOG.warn("Authorization failed for token %s", user_token) raise InvalidUserToken('Token authorization failed') @@ -618,19 +628,30 @@ class AuthProtocol(object): raise InvalidUserToken() - def verify_signed_token(self, signed_text): - """ - Converts a block of Base64 encoding to strict PEM format - and verifies the signature of the contensts IAW CMS syntax - If either of the certificate files are missing, fetch them - and retry - """ + def is_signed_token_revoked(self, signed_text): + """Indicate whether the token appears in the revocation list.""" + revocation_list = self.token_revocation_list + revoked_tokens = revocation_list.get('revoked', []) + if not revoked_tokens: + return + revoked_ids = (x['id'] for x in revoked_tokens) + token_id = utils.hash_signed_token(signed_text) + for revoked_id in revoked_ids: + if token_id == revoked_id: + LOG.debug('Token %s is marked as having been revoked', + token_id) + return True + return False - formatted = cms.token_to_cms(signed_text) + def cms_verify(self, data): + """Verifies the signature of the provided data's IAW CMS syntax. + If either of the certificate files are missing, fetch them and + retry. + """ while True: try: - output = cms.cms_verify(formatted, self.signing_cert_file_name, + output = cms.cms_verify(data, self.signing_cert_file_name, self.ca_file_name) except subprocess.CalledProcessError as err: if self.cert_file_missing(err, self.signing_cert_file_name): @@ -642,6 +663,64 @@ class AuthProtocol(object): raise err return output + def verify_signed_token(self, signed_text): + """Check that the token is unrevoked and has a valid signature.""" + if self.is_signed_token_revoked(signed_text): + raise InvalidUserToken('Token has been revoked') + + formatted = cms.token_to_cms(signed_text) + return self.cms_verify(formatted) + + @property + def token_revocation_list_fetched_time(self): + if not self._token_revocation_list_fetched_time: + # If the fetched list has been written to disk, use its + # modification time. + if os.path.exists(self.revoked_file_name): + mtime = os.path.getmtime(self.revoked_file_name) + fetched_time = datetime.datetime.fromtimestamp(mtime) + # Otherwise the list will need to be fetched. + else: + fetched_time = datetime.datetime.min + self._token_revocation_list_fetched_time = fetched_time + return self._token_revocation_list_fetched_time + + @token_revocation_list_fetched_time.setter + def token_revocation_list_fetched_time(self, value): + self._token_revocation_list_fetched_time = value + + @property + def token_revocation_list(self): + timeout = self.token_revocation_list_fetched_time +\ + self.token_revocation_list_cache_timeout + list_is_current = timeutils.utcnow() < timeout + if list_is_current: + # Load the list from disk if required + if not self._token_revocation_list: + with open(self.revoked_file_name, 'r') as f: + self._token_revocation_list = jsonutils.loads(f.read()) + else: + self.token_revocation_list = self.fetch_revocation_list() + return self._token_revocation_list + + @token_revocation_list.setter + def token_revocation_list(self, value): + """Save a revocation list to memory and to disk. + + :param value: A json-encoded revocation list + + """ + self._token_revocation_list = jsonutils.loads(value) + self.token_revocation_list_fetched_time = timeutils.utcnow() + with open(self.revoked_file_name, 'w') as f: + f.write(value) + + def fetch_revocation_list(self): + response, data = self._http_request('GET', '/v2.0/tokens/revoked') + if response.status != 200: + raise ServiceError('Unable to fetch token revocation list.') + return self.cms_verify(data) + def fetch_signing_cert(self): response, data = self._http_request('GET', '/v2.0/certificates/signing') From 1dd234c9fe96fcdf5d11977bf0722ff3f84f9730 Mon Sep 17 00:00:00 2001 From: Alan Pevec Date: Tue, 31 Jul 2012 03:14:16 +0200 Subject: [PATCH 075/121] allow middleware configuration from app config From markmc's proposal: http://lists.openstack.org/pipermail/openstack-dev/2012-July/000277.html For backward compatiblity, configuration from paste-deploy INI is used if it exists. If not, section [keystone_authtoken] in global configuration is expected, with the same parameter names. Requires application using global cfg.CONF object (nova and glance since folsom-2) and before there's openstack.common library, attempts to use copy/pasted .openstack.common.cfg DocImpact Change-Id: If6aa22280f4ce2cc698d99a130b5792dab808363 --- auth_token.py | 96 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 20 deletions(-) diff --git a/auth_token.py b/auth_token.py index ef449c67..849d877c 100644 --- a/auth_token.py +++ b/auth_token.py @@ -109,8 +109,55 @@ from keystone.common import cms from keystone.common import utils from keystone.openstack.common import timeutils +CONF = None +try: + from openstack.common import cfg + CONF = cfg.CONF +except ImportError: + # cfg is not a library yet, try application copies + for app in 'nova', 'glance', 'quantum', 'cinder': + try: + cfg = __import__('%s.openstack.common.cfg' % app, + fromlist=['%s.openstack.common' % app]) + # test which application middleware is running in + if 'config_file' in cfg.CONF: + CONF = cfg.CONF + break + except ImportError: + pass +if not CONF: + from keystone.openstack.common import cfg + CONF = cfg.CONF LOG = logging.getLogger(__name__) +# alternative middleware configuration in the main application's +# configuration file e.g. in nova.conf +# [keystone_authtoken] +# auth_host = 127.0.0.1 +# auth_port = 35357 +# auth_protocol = http +# admin_tenant_name = admin +# admin_user = admin +# admin_password = badpassword +opts = [ + cfg.StrOpt('auth_admin_prefix', default=''), + cfg.StrOpt('auth_host', default='127.0.0.1'), + cfg.IntOpt('auth_port', default=35357), + cfg.StrOpt('auth_protocol', default='https'), + cfg.StrOpt('auth_uri', default=None), + cfg.BoolOpt('delay_auth_decision', default=False), + cfg.StrOpt('admin_token'), + cfg.StrOpt('admin_user'), + cfg.StrOpt('admin_password'), + cfg.StrOpt('admin_tenant_name', default='admin'), + cfg.StrOpt('certfile'), + cfg.StrOpt('keyfile'), + cfg.StrOpt('signing_dir'), + cfg.ListOpt('memcache_servers'), + cfg.IntOpt('token_cache_time', default=300), +] +CONF.register_opts(opts, group='keystone_authtoken') + class InvalidUserToken(Exception): pass @@ -134,31 +181,33 @@ class AuthProtocol(object): # delay_auth_decision means we still allow unauthenticated requests # through and we let the downstream service make the final decision - self.delay_auth_decision = (conf.get('delay_auth_decision', False) - in ('true', 't', '1', 'on', 'yes', 'y')) + self.delay_auth_decision = (self._conf_get('delay_auth_decision') in + (True, 'true', 't', '1', 'on', 'yes', 'y')) # where to find the auth service (we use this to validate tokens) - self.auth_host = conf.get('auth_host') - self.auth_port = int(conf.get('auth_port', 35357)) - self.auth_protocol = conf.get('auth_protocol', 'https') + self.auth_host = self._conf_get('auth_host') + self.auth_port = int(self._conf_get('auth_port')) + self.auth_protocol = self._conf_get('auth_protocol') if self.auth_protocol == 'http': self.http_client_class = httplib.HTTPConnection else: self.http_client_class = httplib.HTTPSConnection - default_auth_uri = '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port) - self.auth_admin_prefix = conf.get('auth_admin_prefix', '') - self.auth_uri = conf.get('auth_uri', default_auth_uri) + self.auth_admin_prefix = self._conf_get('auth_admin_prefix') + self.auth_uri = self._conf_get('auth_uri') + if self.auth_uri is None: + self.auth_uri = '%s://%s:%s' % (self.auth_protocol, + self.auth_host, + self.auth_port) # SSL - self.cert_file = conf.get('certfile') - self.key_file = conf.get('keyfile') + self.cert_file = self._conf_get('certfile') + self.key_file = self._conf_get('keyfile') #signing - default_signing_dir = '%s/keystone-signing' % os.environ['HOME'] - self.signing_dirname = conf.get('signing_dir', default_signing_dir) + self.signing_dirname = self._conf_get('signing_dir') + if self.signing_dirname is None: + self.signing_dirname = '%s/keystone-signing' % os.environ['HOME'] LOG.info('Using %s as cache directory for signing certificate' % self.signing_dirname) if (os.path.exists(self.signing_dirname) and @@ -180,17 +229,17 @@ class AuthProtocol(object): # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call - self.admin_token = conf.get('admin_token') - self.admin_user = conf.get('admin_user') - self.admin_password = conf.get('admin_password') - self.admin_tenant_name = conf.get('admin_tenant_name', 'admin') + self.admin_token = self._conf_get('admin_token') + self.admin_user = self._conf_get('admin_user') + self.admin_password = self._conf_get('admin_password') + self.admin_tenant_name = self._conf_get('admin_tenant_name') # Token caching via memcache self._cache = None self._iso8601 = None - memcache_servers = conf.get('memcache_servers') + memcache_servers = self._conf_get('memcache_servers') # By default the token will be cached for 5 minutes - self.token_cache_time = conf.get('token_cache_time', 300) + self.token_cache_time = int(self._conf_get('token_cache_time')) self._token_revocation_list = None self._token_revocation_list_fetched_time = None self.token_revocation_list_cache_timeout = \ @@ -205,6 +254,13 @@ class AuthProtocol(object): except ImportError as e: LOG.warn('disabled caching due to missing libraries %s', e) + def _conf_get(self, name): + # try config from paste-deploy first + if name in self.conf: + return self.conf[name] + else: + return CONF.keystone_authtoken[name] + def __call__(self, env, start_response): """Handle incoming request. From 6debd2257881588f2bc2186b8fa6d636e499c7cc Mon Sep 17 00:00:00 2001 From: Adam Young Date: Fri, 17 Aug 2012 19:17:17 -0400 Subject: [PATCH 076/121] Fix auth_token middleware to fetch revocation list as admin. Make the revocation list into a JSON document and get the Vary header. This will also allow the revocation list to carry additional information in the future, to include sufficient information for the calling application to figure out how to get the certificates it requires. Bug 1038309 Change-Id: I4a41cbd8a7352e5b5f951027d6f2063b169bce89 --- auth_token.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index 849d877c..5f9828b0 100644 --- a/auth_token.py +++ b/auth_token.py @@ -772,10 +772,14 @@ class AuthProtocol(object): f.write(value) def fetch_revocation_list(self): - response, data = self._http_request('GET', '/v2.0/tokens/revoked') + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', '/v2.0/tokens/revoked', + additional_headers=headers) if response.status != 200: raise ServiceError('Unable to fetch token revocation list.') - return self.cms_verify(data) + if (not 'signed' in data): + raise ServiceError('Revocation list inmproperly formatted.') + return self.cms_verify(data['signed']) def fetch_signing_cert(self): response, data = self._http_request('GET', From f24263d4d73825367dfdc3a3b4dac1dbd813dbeb Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Wed, 29 Aug 2012 13:55:40 -0500 Subject: [PATCH 077/121] Check for expected cfg impl (bug 1043479) Change-Id: Id2ac85d4ac61713c0ca8e2c10e68cbdeeadff4cb --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 5f9828b0..1f565eb3 100644 --- a/auth_token.py +++ b/auth_token.py @@ -120,7 +120,7 @@ except ImportError: cfg = __import__('%s.openstack.common.cfg' % app, fromlist=['%s.openstack.common' % app]) # test which application middleware is running in - if 'config_file' in cfg.CONF: + if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF: CONF = cfg.CONF break except ImportError: From 3c38812ea98d5063bbaaa162733af4f06904f504 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Wed, 12 Sep 2012 10:28:18 +0800 Subject: [PATCH 078/121] Backslash continuation cleanup Removed unnecessary backslash continuations Added backslash continuation rules to HACKING.rst Change-Id: Id91da5b7e9be4d4587dded95fe7a0415240213ec --- auth_token.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth_token.py b/auth_token.py index 1f565eb3..5d24e251 100644 --- a/auth_token.py +++ b/auth_token.py @@ -242,8 +242,8 @@ class AuthProtocol(object): self.token_cache_time = int(self._conf_get('token_cache_time')) self._token_revocation_list = None self._token_revocation_list_fetched_time = None - self.token_revocation_list_cache_timeout = \ - datetime.timedelta(seconds=0) + cache_timeout = datetime.timedelta(seconds=0) + self.token_revocation_list_cache_timeout = cache_timeout if memcache_servers: try: import memcache @@ -747,8 +747,8 @@ class AuthProtocol(object): @property def token_revocation_list(self): - timeout = self.token_revocation_list_fetched_time +\ - self.token_revocation_list_cache_timeout + timeout = (self.token_revocation_list_fetched_time + + self.token_revocation_list_cache_timeout) list_is_current = timeutils.utcnow() < timeout if list_is_current: # Load the list from disk if required From 2e7fd090f582380e24a55b3e29a46ad1fb338e59 Mon Sep 17 00:00:00 2001 From: Dan Radez Date: Mon, 8 Oct 2012 17:30:41 -0400 Subject: [PATCH 079/121] replacing PKI token detection from content length to content prefix. (bug 1060389) Change-Id: I68b0e4126f2e339c04271fd982f5f5dab198c630 --- auth_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth_token.py b/auth_token.py index 5d24e251..5c198e83 100644 --- a/auth_token.py +++ b/auth_token.py @@ -475,7 +475,7 @@ class AuthProtocol(object): cached = self._cache_get(user_token) if cached: return cached - if (len(user_token) > cms.UUID_TOKEN_LENGTH): + if cms.is_ans1_token(user_token): verified = self.verify_signed_token(user_token) data = json.loads(verified) else: From 59884b4edb17570a9065c0721865ebed2bfe3b0d Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Mon, 29 Oct 2012 15:33:59 +0100 Subject: [PATCH 080/121] Move 'opentack.context' and 'openstack.params' definitions to keystone.common.wsgi Change-Id: Idc4f6765cba20e7baadb61e355076695f36d66ea --- core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core.py b/core.py index f6b13f71..6f8abff2 100644 --- a/core.py +++ b/core.py @@ -29,11 +29,11 @@ AUTH_TOKEN_HEADER = 'X-Auth-Token' # Environment variable used to pass the request context -CONTEXT_ENV = 'openstack.context' +CONTEXT_ENV = wsgi.CONTEXT_ENV # Environment variable used to pass the request params -PARAMS_ENV = 'openstack.params' +PARAMS_ENV = wsgi.PARAMS_ENV class TokenAuthMiddleware(wsgi.Middleware): From 495032509cf09d447b2b43d92152361dfc54ccb5 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Tue, 30 Oct 2012 20:22:24 -0400 Subject: [PATCH 081/121] auth_token hash pki key PKI tokens on hash in memcached when accessed by auth_token middelware Bug 1073343 Change-Id: I32e5481f82fd110c855d7e1138c3d43c73099bbb --- auth_token.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index 5c198e83..e8ed99b3 100644 --- a/auth_token.py +++ b/auth_token.py @@ -472,7 +472,8 @@ class AuthProtocol(object): """ try: - cached = self._cache_get(user_token) + token_id = cms.cms_hash_token(user_token) + cached = self._cache_get(token_id) if cached: return cached if cms.is_ans1_token(user_token): @@ -480,7 +481,7 @@ class AuthProtocol(object): data = json.loads(verified) else: data = self.verify_uuid_token(user_token, retry) - self._cache_put(user_token, data) + self._cache_put(token_id, data) return data except Exception as e: LOG.debug('Token validation failure.', exc_info=True) From 4b125bcdda7ab19f3aa1f03e843b39feac98ca48 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Mon, 5 Nov 2012 10:52:57 -0600 Subject: [PATCH 082/121] HACKING compliance: consistent use of 'except' Change-Id: I8301043965e08ffdec63441e612628d9a60876b7 --- auth_token.py | 6 +++--- core.py | 2 +- s3_token.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/auth_token.py b/auth_token.py index e8ed99b3..cd9b8066 100644 --- a/auth_token.py +++ b/auth_token.py @@ -286,7 +286,7 @@ class AuthProtocol(object): LOG.info('Invalid user token - rejecting request') return self._reject_request(env, start_response) - except ServiceError, e: + except ServiceError as e: LOG.critical('Unable to obtain admin token: %s' % e) resp = webob.exc.HTTPServiceUnavailable() return resp(env, start_response) @@ -378,7 +378,7 @@ class AuthProtocol(object): conn.request(method, path) response = conn.getresponse() body = response.read() - except Exception, e: + except Exception as e: LOG.error('HTTP connection exception: %s' % e) raise ServiceError('Unable to communicate with keystone') finally: @@ -418,7 +418,7 @@ class AuthProtocol(object): conn.request(method, full_path, **kwargs) response = conn.getresponse() body = response.read() - except Exception, e: + except Exception as e: LOG.error('HTTP connection exception: %s' % e) raise ServiceError('Unable to communicate with keystone') finally: diff --git a/core.py b/core.py index 6f8abff2..a49f743b 100644 --- a/core.py +++ b/core.py @@ -147,7 +147,7 @@ class XmlBodyMiddleware(wsgi.Middleware): try: body_obj = jsonutils.loads(response.body) response.body = serializer.to_xml(body_obj) - except: + except Exception: raise exception.Error(message=response.body) return response diff --git a/s3_token.py b/s3_token.py index 7cf2e394..0f207b3d 100644 --- a/s3_token.py +++ b/s3_token.py @@ -99,7 +99,7 @@ class S3Token(object): headers=headers) response = conn.getresponse() output = response.read() - except Exception, e: + except Exception as e: self.logger.info('HTTP connection exception: %s' % e) resp = self.deny_request('InvalidURI') raise ServiceError(resp) @@ -143,7 +143,7 @@ class S3Token(object): auth_header = req.headers['Authorization'] try: access, signature = auth_header.split(' ')[-1].rsplit(':', 1) - except(ValueError): + except ValueError: msg = 'You have an invalid Authorization header: %s' self.logger.debug(msg % (auth_header)) return self.deny_request('InvalidURI')(environ, start_response) From 0ee7bac15dcb65eb2e7a464c71b0996698db5ed0 Mon Sep 17 00:00:00 2001 From: Joe Heck Date: Thu, 1 Nov 2012 15:36:31 -0700 Subject: [PATCH 083/121] fixes bug 1074172 updated diablo token based on output from diablo/stable keystone added expiry to example tokens for test_auth_middleware added a stack based HTTP response to test_auth_middleware to verify sequencing Change-Id: I738b0e9c1a0e62ad86adb95ec0b73f621513f7d4 --- auth_token.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/auth_token.py b/auth_token.py index 5c198e83..43261f29 100644 --- a/auth_token.py +++ b/auth_token.py @@ -159,6 +159,16 @@ opts = [ CONF.register_opts(opts, group='keystone_authtoken') +def will_expire_soon(expiry): + """ Determines if expiration is about to occur. + + :param expiry: a datetime of the expected expiration + :returns: boolean : true if expiration is within 30 seconds + """ + soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) + return expiry < soon + + class InvalidUserToken(Exception): pass @@ -230,6 +240,7 @@ class AuthProtocol(object): # Credentials used to verify this component with the Auth service since # validating tokens is a privileged call self.admin_token = self._conf_get('admin_token') + self.admin_token_expiry = None self.admin_user = self._conf_get('admin_user') self.admin_password = self._conf_get('admin_password') self.admin_tenant_name = self._conf_get('admin_tenant_name') @@ -345,12 +356,21 @@ class AuthProtocol(object): def get_admin_token(self): """Return admin token, possibly fetching a new one. + if self.admin_token_expiry is set from fetching an admin token, check + it for expiration, and request a new token is the existing token + is about to expire. + :return admin token id :raise ServiceError when unable to retrieve token from keystone """ + if self.admin_token_expiry: + if will_expire_soon(self.admin_token_expiry): + self.admin_token = None + if not self.admin_token: - self.admin_token = self._request_admin_token() + (self.admin_token, + self.admin_token_expiry) = self._request_admin_token() return self.admin_token @@ -455,11 +475,17 @@ class AuthProtocol(object): try: token = data['access']['token']['id'] + expiry = data['access']['token']['expires'] assert token - return token + assert expiry + datetime_expiry = timeutils.parse_isotime(expiry) + return (token, timeutils.normalize_time(datetime_expiry)) except (AssertionError, KeyError): LOG.warn("Unexpected response from keystone service: %s", data) raise ServiceError('invalid json response') + except (ValueError): + LOG.warn("Unable to parse expiration time from token: %s", data) + raise ServiceError('invalid json response') def _validate_user_token(self, user_token, retry=True): """Authenticate user using PKI @@ -771,10 +797,16 @@ class AuthProtocol(object): with open(self.revoked_file_name, 'w') as f: f.write(value) - def fetch_revocation_list(self): + def fetch_revocation_list(self, retry=True): headers = {'X-Auth-Token': self.get_admin_token()} response, data = self._json_request('GET', '/v2.0/tokens/revoked', additional_headers=headers) + if response.status == 401: + if retry: + LOG.info('Keystone rejected admin token %s, resetting admin ' + 'token', headers) + self.admin_token = None + return self.fetch_revocation_list(retry=False) if response.status != 200: raise ServiceError('Unable to fetch token revocation list.') if (not 'signed' in data): From 9549262fd87956e85e60c19a549068a27c3dd201 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Fri, 9 Nov 2012 13:53:48 -0800 Subject: [PATCH 084/121] Use the right subprocess based on os monkeypatch This works around the following eventlet bug: https://bitbucket.org/which_linden/eventlet/issue/92 by using the green version of Popen if os has been monkeypatched. It also has the side effect of making the ssl calls not block the reactor for workers that use eventlet. Change-Id: I1457237f52310f0536fbcdcaa42174b17e8edbf5 --- auth_token.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/auth_token.py b/auth_token.py index c4dddc7b..86fcb66b 100644 --- a/auth_token.py +++ b/auth_token.py @@ -99,7 +99,6 @@ import json import logging import os import stat -import subprocess import time import webob import webob.exc @@ -736,7 +735,7 @@ class AuthProtocol(object): try: output = cms.cms_verify(data, self.signing_cert_file_name, self.ca_file_name) - except subprocess.CalledProcessError as err: + except cms.subprocess.CalledProcessError as err: if self.cert_file_missing(err, self.signing_cert_file_name): self.fetch_signing_cert() continue From be7dab6e17c2d919b48ae0cd65ddd28fd4402053 Mon Sep 17 00:00:00 2001 From: Henry Nash Date: Sat, 17 Nov 2012 14:45:18 +0000 Subject: [PATCH 085/121] Import auth_token middleware from keystoneclient Although the master auth_token file is now in keystoneclient, it will take some time to get all the paste files to point to it there rather than here. Hence, we import it back here to provide backward compatibility for a release or so, after which we will remove it from the server. Change-Id: Iccdb7839a611cdda233e4ea96f68c64d6d82f49c --- auth_token.py | 843 +------------------------------------------------- 1 file changed, 11 insertions(+), 832 deletions(-) diff --git a/auth_token.py b/auth_token.py index 86fcb66b..25e2685e 100644 --- a/auth_token.py +++ b/auth_token.py @@ -16,839 +16,18 @@ # limitations under the License. """ -TOKEN-BASED AUTH MIDDLEWARE - -This WSGI component: - -* Verifies that incoming client requests have valid tokens by validating - tokens with the auth service. -* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' - mode, which means the final decision is delegated to the downstream WSGI - component (usually the OpenStack service) -* Collects and forwards identity information based on a valid token - such as user name, tenant, etc - -Refer to: http://keystone.openstack.org/middlewarearchitecture.html - -HEADERS -------- - -* Headers starting with HTTP\_ is a standard http header -* Headers starting with HTTP_X is an extended http header - -Coming in from initial call from client or customer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP_X_AUTH_TOKEN - The client token being passed in. - -HTTP_X_STORAGE_TOKEN - The client token being passed in (legacy Rackspace use) to support - swift/cloud files - -Used for communication between components -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -WWW-Authenticate - HTTP header returned to a user indicating which endpoint to use - to retrieve a new token - -What we add to the request for use by the OpenStack service -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -HTTP_X_IDENTITY_STATUS - 'Confirmed' or 'Invalid' - The underlying service will only see a value of 'Invalid' if the Middleware - is configured to run in 'delay_auth_decision' mode - -HTTP_X_TENANT_ID - Identity service managed unique identifier, string - -HTTP_X_TENANT_NAME - Unique tenant identifier, string - -HTTP_X_USER_ID - Identity-service managed unique identifier, string - -HTTP_X_USER_NAME - Unique user identifier, string - -HTTP_X_ROLES - Comma delimited list of case-sensitive Roles - -HTTP_X_SERVICE_CATALOG - json encoded keystone service catalog (optional). - -HTTP_X_TENANT - *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME - Keystone-assigned unique identifier, deprecated - -HTTP_X_USER - *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME - Unique user name, string - -HTTP_X_ROLE - *Deprecated* in favor of HTTP_X_ROLES - This is being renamed, and the new header contains the same data. - +The actual code for auth_token has now been moved python-keystoneclient. It is +imported back here to ensure backward combatibility for old paste.ini files +that might still refer to here as opposed to keystoneclient """ -import datetime -import httplib -import json -import logging -import os -import stat -import time -import webob -import webob.exc +from keystoneclient.middleware import auth_token as client_auth_token -from keystone.openstack.common import jsonutils -from keystone.common import cms -from keystone.common import utils -from keystone.openstack.common import timeutils +will_expire_soon = client_auth_token.will_expire_soon +InvalidUserToken = client_auth_token.InvalidUserToken +ServiceError = client_auth_token.ServiceError +ConfigurationError = client_auth_token.ConfigurationError +AuthProtocol = client_auth_token.AuthProtocol -CONF = None -try: - from openstack.common import cfg - CONF = cfg.CONF -except ImportError: - # cfg is not a library yet, try application copies - for app in 'nova', 'glance', 'quantum', 'cinder': - try: - cfg = __import__('%s.openstack.common.cfg' % app, - fromlist=['%s.openstack.common' % app]) - # test which application middleware is running in - if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF: - CONF = cfg.CONF - break - except ImportError: - pass -if not CONF: - from keystone.openstack.common import cfg - CONF = cfg.CONF -LOG = logging.getLogger(__name__) - -# alternative middleware configuration in the main application's -# configuration file e.g. in nova.conf -# [keystone_authtoken] -# auth_host = 127.0.0.1 -# auth_port = 35357 -# auth_protocol = http -# admin_tenant_name = admin -# admin_user = admin -# admin_password = badpassword -opts = [ - cfg.StrOpt('auth_admin_prefix', default=''), - cfg.StrOpt('auth_host', default='127.0.0.1'), - cfg.IntOpt('auth_port', default=35357), - cfg.StrOpt('auth_protocol', default='https'), - cfg.StrOpt('auth_uri', default=None), - cfg.BoolOpt('delay_auth_decision', default=False), - cfg.StrOpt('admin_token'), - cfg.StrOpt('admin_user'), - cfg.StrOpt('admin_password'), - cfg.StrOpt('admin_tenant_name', default='admin'), - cfg.StrOpt('certfile'), - cfg.StrOpt('keyfile'), - cfg.StrOpt('signing_dir'), - cfg.ListOpt('memcache_servers'), - cfg.IntOpt('token_cache_time', default=300), -] -CONF.register_opts(opts, group='keystone_authtoken') - - -def will_expire_soon(expiry): - """ Determines if expiration is about to occur. - - :param expiry: a datetime of the expected expiration - :returns: boolean : true if expiration is within 30 seconds - """ - soon = (timeutils.utcnow() + datetime.timedelta(seconds=30)) - return expiry < soon - - -class InvalidUserToken(Exception): - pass - - -class ServiceError(Exception): - pass - - -class ConfigurationError(Exception): - pass - - -class AuthProtocol(object): - """Auth Middleware that handles authenticating client calls.""" - - def __init__(self, app, conf): - LOG.info('Starting keystone auth_token middleware') - self.conf = conf - self.app = app - - # delay_auth_decision means we still allow unauthenticated requests - # through and we let the downstream service make the final decision - self.delay_auth_decision = (self._conf_get('delay_auth_decision') in - (True, 'true', 't', '1', 'on', 'yes', 'y')) - - # where to find the auth service (we use this to validate tokens) - self.auth_host = self._conf_get('auth_host') - self.auth_port = int(self._conf_get('auth_port')) - self.auth_protocol = self._conf_get('auth_protocol') - if self.auth_protocol == 'http': - self.http_client_class = httplib.HTTPConnection - else: - self.http_client_class = httplib.HTTPSConnection - - self.auth_admin_prefix = self._conf_get('auth_admin_prefix') - self.auth_uri = self._conf_get('auth_uri') - if self.auth_uri is None: - self.auth_uri = '%s://%s:%s' % (self.auth_protocol, - self.auth_host, - self.auth_port) - - # SSL - self.cert_file = self._conf_get('certfile') - self.key_file = self._conf_get('keyfile') - - #signing - self.signing_dirname = self._conf_get('signing_dir') - if self.signing_dirname is None: - self.signing_dirname = '%s/keystone-signing' % os.environ['HOME'] - LOG.info('Using %s as cache directory for signing certificate' % - self.signing_dirname) - if (os.path.exists(self.signing_dirname) and - not os.access(self.signing_dirname, os.W_OK)): - raise ConfigurationError("unable to access signing dir %s" % - self.signing_dirname) - - if not os.path.exists(self.signing_dirname): - os.makedirs(self.signing_dirname) - #will throw IOError if it cannot change permissions - os.chmod(self.signing_dirname, stat.S_IRWXU) - - val = '%s/signing_cert.pem' % self.signing_dirname - self.signing_cert_file_name = val - val = '%s/cacert.pem' % self.signing_dirname - self.ca_file_name = val - val = '%s/revoked.pem' % self.signing_dirname - self.revoked_file_name = val - - # Credentials used to verify this component with the Auth service since - # validating tokens is a privileged call - self.admin_token = self._conf_get('admin_token') - self.admin_token_expiry = None - self.admin_user = self._conf_get('admin_user') - self.admin_password = self._conf_get('admin_password') - self.admin_tenant_name = self._conf_get('admin_tenant_name') - - # Token caching via memcache - self._cache = None - self._iso8601 = None - memcache_servers = self._conf_get('memcache_servers') - # By default the token will be cached for 5 minutes - self.token_cache_time = int(self._conf_get('token_cache_time')) - self._token_revocation_list = None - self._token_revocation_list_fetched_time = None - cache_timeout = datetime.timedelta(seconds=0) - self.token_revocation_list_cache_timeout = cache_timeout - if memcache_servers: - try: - import memcache - import iso8601 - LOG.info('Using memcache for caching token') - self._cache = memcache.Client(memcache_servers.split(',')) - self._iso8601 = iso8601 - except ImportError as e: - LOG.warn('disabled caching due to missing libraries %s', e) - - def _conf_get(self, name): - # try config from paste-deploy first - if name in self.conf: - return self.conf[name] - else: - return CONF.keystone_authtoken[name] - - def __call__(self, env, start_response): - """Handle incoming request. - - Authenticate send downstream on success. Reject request if - we can't authenticate. - - """ - LOG.debug('Authenticating user token') - try: - self._remove_auth_headers(env) - user_token = self._get_user_token_from_header(env) - token_info = self._validate_user_token(user_token) - user_headers = self._build_user_headers(token_info) - self._add_headers(env, user_headers) - return self.app(env, start_response) - - except InvalidUserToken: - if self.delay_auth_decision: - LOG.info('Invalid user token - deferring reject downstream') - self._add_headers(env, {'X-Identity-Status': 'Invalid'}) - return self.app(env, start_response) - else: - LOG.info('Invalid user token - rejecting request') - return self._reject_request(env, start_response) - - except ServiceError as e: - LOG.critical('Unable to obtain admin token: %s' % e) - resp = webob.exc.HTTPServiceUnavailable() - return resp(env, start_response) - - def _remove_auth_headers(self, env): - """Remove headers so a user can't fake authentication. - - :param env: wsgi request environment - - """ - auth_headers = ( - 'X-Identity-Status', - 'X-Tenant-Id', - 'X-Tenant-Name', - 'X-User-Id', - 'X-User-Name', - 'X-Roles', - 'X-Service-Catalog', - # Deprecated - 'X-User', - 'X-Tenant', - 'X-Role', - ) - LOG.debug('Removing headers from request environment: %s' % - ','.join(auth_headers)) - self._remove_headers(env, auth_headers) - - def _get_user_token_from_header(self, env): - """Get token id from request. - - :param env: wsgi request environment - :return token id - :raises InvalidUserToken if no token is provided in request - - """ - token = self._get_header(env, 'X-Auth-Token', - self._get_header(env, 'X-Storage-Token')) - if token: - return token - else: - LOG.warn("Unable to find authentication token in headers: %s", env) - raise InvalidUserToken('Unable to find token in headers') - - def _reject_request(self, env, start_response): - """Redirect client to auth server. - - :param env: wsgi request environment - :param start_response: wsgi response callback - :returns HTTPUnauthorized http response - - """ - headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] - resp = webob.exc.HTTPUnauthorized('Authentication required', headers) - return resp(env, start_response) - - def get_admin_token(self): - """Return admin token, possibly fetching a new one. - - if self.admin_token_expiry is set from fetching an admin token, check - it for expiration, and request a new token is the existing token - is about to expire. - - :return admin token id - :raise ServiceError when unable to retrieve token from keystone - - """ - if self.admin_token_expiry: - if will_expire_soon(self.admin_token_expiry): - self.admin_token = None - - if not self.admin_token: - (self.admin_token, - self.admin_token_expiry) = self._request_admin_token() - - return self.admin_token - - def _get_http_connection(self): - if self.auth_protocol == 'http': - return self.http_client_class(self.auth_host, self.auth_port) - else: - return self.http_client_class(self.auth_host, - self.auth_port, - self.key_file, - self.cert_file) - - def _http_request(self, method, path): - """HTTP request helper used to make unspecified content type requests. - - :param method: http method - :param path: relative request url - :return (http response object) - :raise ServerError when unable to communicate with keystone - - """ - conn = self._get_http_connection() - - try: - conn.request(method, path) - response = conn.getresponse() - body = response.read() - except Exception as e: - LOG.error('HTTP connection exception: %s' % e) - raise ServiceError('Unable to communicate with keystone') - finally: - conn.close() - - return response, body - - def _json_request(self, method, path, body=None, additional_headers=None): - """HTTP request helper used to make json requests. - - :param method: http method - :param path: relative request url - :param body: dict to encode to json as request body. Optional. - :param additional_headers: dict of additional headers to send with - http request. Optional. - :return (http response object, response body parsed as json) - :raise ServerError when unable to communicate with keystone - - """ - conn = self._get_http_connection() - - kwargs = { - 'headers': { - 'Content-type': 'application/json', - 'Accept': 'application/json', - }, - } - - if additional_headers: - kwargs['headers'].update(additional_headers) - - if body: - kwargs['body'] = jsonutils.dumps(body) - - full_path = self.auth_admin_prefix + path - try: - conn.request(method, full_path, **kwargs) - response = conn.getresponse() - body = response.read() - except Exception as e: - LOG.error('HTTP connection exception: %s' % e) - raise ServiceError('Unable to communicate with keystone') - finally: - conn.close() - - try: - data = jsonutils.loads(body) - except ValueError: - LOG.debug('Keystone did not return json-encoded body') - data = {} - - return response, data - - def _request_admin_token(self): - """Retrieve new token as admin user from keystone. - - :return token id upon success - :raises ServerError when unable to communicate with keystone - - """ - params = { - 'auth': { - 'passwordCredentials': { - 'username': self.admin_user, - 'password': self.admin_password, - }, - 'tenantName': self.admin_tenant_name, - } - } - - response, data = self._json_request('POST', - '/v2.0/tokens', - body=params) - - try: - token = data['access']['token']['id'] - expiry = data['access']['token']['expires'] - assert token - assert expiry - datetime_expiry = timeutils.parse_isotime(expiry) - return (token, timeutils.normalize_time(datetime_expiry)) - except (AssertionError, KeyError): - LOG.warn("Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - except (ValueError): - LOG.warn("Unable to parse expiration time from token: %s", data) - raise ServiceError('invalid json response') - - def _validate_user_token(self, user_token, retry=True): - """Authenticate user using PKI - - :param user_token: user's token id - :param retry: Ignored, as it is not longer relevant - :return uncrypted body of the token if the token is valid - :raise InvalidUserToken if token is rejected - :no longer raises ServiceError since it no longer makes RPC - - """ - try: - token_id = cms.cms_hash_token(user_token) - cached = self._cache_get(token_id) - if cached: - return cached - if cms.is_ans1_token(user_token): - verified = self.verify_signed_token(user_token) - data = json.loads(verified) - else: - data = self.verify_uuid_token(user_token, retry) - self._cache_put(token_id, data) - return data - except Exception as e: - LOG.debug('Token validation failure.', exc_info=True) - self._cache_store_invalid(user_token) - LOG.warn("Authorization failed for token %s", user_token) - raise InvalidUserToken('Token authorization failed') - - def _build_user_headers(self, token_info): - """Convert token object into headers. - - Build headers that represent authenticated user: - * X_IDENTITY_STATUS: Confirmed or Invalid - * X_TENANT_ID: id of tenant if tenant is present - * X_TENANT_NAME: name of tenant if tenant is present - * X_USER_ID: id of user - * X_USER_NAME: name of user - * X_ROLES: list of roles - * X_SERVICE_CATALOG: service catalog - - Additional (deprecated) headers include: - * X_USER: name of user - * X_TENANT: For legacy compatibility before we had ID and Name - * X_ROLE: list of roles - - :param token_info: token object returned by keystone on authentication - :raise InvalidUserToken when unable to parse token object - - """ - user = token_info['access']['user'] - token = token_info['access']['token'] - roles = ','.join([role['name'] for role in user.get('roles', [])]) - - def get_tenant_info(): - """Returns a (tenant_id, tenant_name) tuple from context.""" - def essex(): - """Essex puts the tenant ID and name on the token.""" - return (token['tenant']['id'], token['tenant']['name']) - - def pre_diablo(): - """Pre-diablo, Keystone only provided tenantId.""" - return (token['tenantId'], token['tenantId']) - - def default_tenant(): - """Assume the user's default tenant.""" - return (user['tenantId'], user['tenantName']) - - for method in [essex, pre_diablo, default_tenant]: - try: - return method() - except KeyError: - pass - - raise InvalidUserToken('Unable to determine tenancy.') - - tenant_id, tenant_name = get_tenant_info() - - user_id = user['id'] - user_name = user['name'] - - rval = { - 'X-Identity-Status': 'Confirmed', - 'X-Tenant-Id': tenant_id, - 'X-Tenant-Name': tenant_name, - 'X-User-Id': user_id, - 'X-User-Name': user_name, - 'X-Roles': roles, - # Deprecated - 'X-User': user_name, - 'X-Tenant': tenant_name, - 'X-Role': roles, - } - - try: - catalog = token_info['access']['serviceCatalog'] - rval['X-Service-Catalog'] = jsonutils.dumps(catalog) - except KeyError: - pass - - return rval - - def _header_to_env_var(self, key): - """Convert header to wsgi env variable. - - :param key: http header name (ex. 'X-Auth-Token') - :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') - - """ - return 'HTTP_%s' % key.replace('-', '_').upper() - - def _add_headers(self, env, headers): - """Add http headers to environment.""" - for (k, v) in headers.iteritems(): - env_key = self._header_to_env_var(k) - env[env_key] = v - - def _remove_headers(self, env, keys): - """Remove http headers from environment.""" - for k in keys: - env_key = self._header_to_env_var(k) - try: - del env[env_key] - except KeyError: - pass - - def _get_header(self, env, key, default=None): - """Get http header from environment.""" - env_key = self._header_to_env_var(key) - return env.get(env_key, default) - - def _cache_get(self, token): - """Return token information from cache. - - If token is invalid raise InvalidUserToken - return token only if fresh (not expired). - """ - if self._cache and token: - key = 'tokens/%s' % token - cached = self._cache.get(key) - if cached == 'invalid': - LOG.debug('Cached Token %s is marked unauthorized', token) - raise InvalidUserToken('Token authorization failed') - if cached: - data, expires = cached - if time.time() < float(expires): - LOG.debug('Returning cached token %s', token) - return data - else: - LOG.debug('Cached Token %s seems expired', token) - - def _cache_put(self, token, data): - """Put token data into the cache. - - Stores the parsed expire date in cache allowing - quick check of token freshness on retrieval. - """ - if self._cache and data: - key = 'tokens/%s' % token - if 'token' in data.get('access', {}): - timestamp = data['access']['token']['expires'] - expires = self._iso8601.parse_date(timestamp).strftime('%s') - else: - LOG.error('invalid token format') - return - LOG.debug('Storing %s token in memcache', token) - self._cache.set(key, - (data, expires), - time=self.token_cache_time) - - def _cache_store_invalid(self, token): - """Store invalid token in cache.""" - if self._cache: - key = 'tokens/%s' % token - LOG.debug('Marking token %s as unauthorized in memcache', token) - self._cache.set(key, - 'invalid', - time=self.token_cache_time) - - def cert_file_missing(self, called_proc_err, file_name): - return (called_proc_err.output.find(file_name) - and not os.path.exists(file_name)) - - def verify_uuid_token(self, user_token, retry=True): - """Authenticate user token with keystone. - - :param user_token: user's token id - :param retry: flag that forces the middleware to retry - user authentication when an indeterminate - response is received. Optional. - :return token object received from keystone on success - :raise InvalidUserToken if token is rejected - :raise ServiceError if unable to authenticate token - - """ - - headers = {'X-Auth-Token': self.get_admin_token()} - response, data = self._json_request('GET', - '/v2.0/tokens/%s' % user_token, - additional_headers=headers) - - if response.status == 200: - self._cache_put(user_token, data) - return data - if response.status == 404: - # FIXME(ja): I'm assuming the 404 status means that user_token is - # invalid - not that the admin_token is invalid - self._cache_store_invalid(user_token) - LOG.warn("Authorization failed for token %s", user_token) - raise InvalidUserToken('Token authorization failed') - if response.status == 401: - LOG.info('Keystone rejected admin token %s, resetting', headers) - self.admin_token = None - else: - LOG.error('Bad response code while validating token: %s' % - response.status) - if retry: - LOG.info('Retrying validation') - return self._validate_user_token(user_token, False) - else: - LOG.warn("Invalid user token: %s. Keystone response: %s.", - user_token, data) - - raise InvalidUserToken() - - def is_signed_token_revoked(self, signed_text): - """Indicate whether the token appears in the revocation list.""" - revocation_list = self.token_revocation_list - revoked_tokens = revocation_list.get('revoked', []) - if not revoked_tokens: - return - revoked_ids = (x['id'] for x in revoked_tokens) - token_id = utils.hash_signed_token(signed_text) - for revoked_id in revoked_ids: - if token_id == revoked_id: - LOG.debug('Token %s is marked as having been revoked', - token_id) - return True - return False - - def cms_verify(self, data): - """Verifies the signature of the provided data's IAW CMS syntax. - - If either of the certificate files are missing, fetch them and - retry. - """ - while True: - try: - output = cms.cms_verify(data, self.signing_cert_file_name, - self.ca_file_name) - except cms.subprocess.CalledProcessError as err: - if self.cert_file_missing(err, self.signing_cert_file_name): - self.fetch_signing_cert() - continue - if self.cert_file_missing(err, self.ca_file_name): - self.fetch_ca_cert() - continue - raise err - return output - - def verify_signed_token(self, signed_text): - """Check that the token is unrevoked and has a valid signature.""" - if self.is_signed_token_revoked(signed_text): - raise InvalidUserToken('Token has been revoked') - - formatted = cms.token_to_cms(signed_text) - return self.cms_verify(formatted) - - @property - def token_revocation_list_fetched_time(self): - if not self._token_revocation_list_fetched_time: - # If the fetched list has been written to disk, use its - # modification time. - if os.path.exists(self.revoked_file_name): - mtime = os.path.getmtime(self.revoked_file_name) - fetched_time = datetime.datetime.fromtimestamp(mtime) - # Otherwise the list will need to be fetched. - else: - fetched_time = datetime.datetime.min - self._token_revocation_list_fetched_time = fetched_time - return self._token_revocation_list_fetched_time - - @token_revocation_list_fetched_time.setter - def token_revocation_list_fetched_time(self, value): - self._token_revocation_list_fetched_time = value - - @property - def token_revocation_list(self): - timeout = (self.token_revocation_list_fetched_time + - self.token_revocation_list_cache_timeout) - list_is_current = timeutils.utcnow() < timeout - if list_is_current: - # Load the list from disk if required - if not self._token_revocation_list: - with open(self.revoked_file_name, 'r') as f: - self._token_revocation_list = jsonutils.loads(f.read()) - else: - self.token_revocation_list = self.fetch_revocation_list() - return self._token_revocation_list - - @token_revocation_list.setter - def token_revocation_list(self, value): - """Save a revocation list to memory and to disk. - - :param value: A json-encoded revocation list - - """ - self._token_revocation_list = jsonutils.loads(value) - self.token_revocation_list_fetched_time = timeutils.utcnow() - with open(self.revoked_file_name, 'w') as f: - f.write(value) - - def fetch_revocation_list(self, retry=True): - headers = {'X-Auth-Token': self.get_admin_token()} - response, data = self._json_request('GET', '/v2.0/tokens/revoked', - additional_headers=headers) - if response.status == 401: - if retry: - LOG.info('Keystone rejected admin token %s, resetting admin ' - 'token', headers) - self.admin_token = None - return self.fetch_revocation_list(retry=False) - if response.status != 200: - raise ServiceError('Unable to fetch token revocation list.') - if (not 'signed' in data): - raise ServiceError('Revocation list inmproperly formatted.') - return self.cms_verify(data['signed']) - - def fetch_signing_cert(self): - response, data = self._http_request('GET', - '/v2.0/certificates/signing') - try: - #todo check response - certfile = open(self.signing_cert_file_name, 'w') - certfile.write(data) - certfile.close() - except (AssertionError, KeyError): - LOG.warn("Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - - def fetch_ca_cert(self): - response, data = self._http_request('GET', - '/v2.0/certificates/ca') - try: - #todo check response - certfile = open(self.ca_file_name, 'w') - certfile.write(data) - certfile.close() - except (AssertionError, KeyError): - LOG.warn("Unexpected response from keystone service: %s", data) - raise ServiceError('invalid json response') - - -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 - - -def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return AuthProtocol(None, conf) +filter_factory = client_auth_token.filter_factory +app_factory = client_auth_token.app_factory From 8592912949bc256b7e825e03161039e8ffb9654e Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Sat, 8 Dec 2012 13:38:45 +0100 Subject: [PATCH 086/121] Remove swift auth. - This has been moved since last release to swift main repository. Change-Id: I11fc4001fbc4a1d78823d41450cdfcc97677c420 --- swift_auth.py | 295 -------------------------------------------------- 1 file changed, 295 deletions(-) delete mode 100644 swift_auth.py diff --git a/swift_auth.py b/swift_auth.py deleted file mode 100644 index ddcec4af..00000000 --- a/swift_auth.py +++ /dev/null @@ -1,295 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 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. - -# Copyright (c) 2012 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. - -import webob - -from swift.common.middleware import acl as swift_acl -from swift.common import utils as swift_utils - - -class SwiftAuth(object): - """Swift middleware to Keystone authorization system. - - In Swift's proxy-server.conf add this middleware to your pipeline:: - - [pipeline:main] - pipeline = catch_errors cache authtoken keystone proxy-server - - Make sure you have the authtoken middleware before the swiftauth - middleware. authtoken will take care of validating the user and - swiftauth will authorize access. If support is required for - unvalidated users (as with anonymous access) or for - tempurl/formpost middleware, authtoken will need to be configured with - delay_auth_decision set to 1. See the documentation for more - detail on how to configure the authtoken middleware. - - Set account auto creation to true:: - - [app:proxy-server] - account_autocreate = true - - And add a swift authorization filter section, such as:: - - [filter:keystone] - paste.filter_factory = keystone.middleware.auth_token:filter_factory - operator_roles = admin, swiftoperator - - This maps tenants to account in Swift. - - The user whose able to give ACL / create Containers permissions - will be the one that are inside the operator_roles - setting which by default includes the admin and the swiftoperator - roles. - - The option is_admin if set to true will allow the - username that has the same name as the account name to be the owner. - - Example: If we have the account called hellocorp with a user - hellocorp that user will be admin on that account and can give ACL - to all other users for hellocorp. - - If you need to have a different reseller_prefix to be able to - mix different auth servers you can configure the option - reseller_prefix in your swiftauth entry like this : - - reseller_prefix = NEWAUTH_ - - Make sure you have a underscore at the end of your new - reseller_prefix option. - - :param app: The next WSGI app in the pipeline - :param conf: The dict of configuration values - """ - def __init__(self, app, conf): - self.app = app - self.conf = conf - self.logger = swift_utils.get_logger(conf, log_route='keystoneauth') - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_').strip() - self.operator_roles = conf.get('operator_roles', - 'admin, swiftoperator') - self.reseller_admin_role = conf.get('reseller_admin_role', - 'ResellerAdmin') - config_is_admin = conf.get('is_admin', "false").lower() - self.is_admin = config_is_admin in swift_utils.TRUE_VALUES - cfg_synchosts = conf.get('allowed_sync_hosts', '127.0.0.1') - self.allowed_sync_hosts = [h.strip() for h in cfg_synchosts.split(',') - if h.strip()] - config_overrides = conf.get('allow_overrides', 't').lower() - self.allow_overrides = config_overrides in swift_utils.TRUE_VALUES - - def __call__(self, environ, start_response): - identity = self._keystone_identity(environ) - - # Check if one of the middleware like tempurl or formpost have - # set the swift.authorize_override environ and want to control the - # authentication - if (self.allow_overrides and - environ.get('swift.authorize_override', False)): - msg = 'Authorizing from an overriding middleware (i.e: tempurl)' - self.logger.debug(msg) - return self.app(environ, start_response) - - if identity: - self.logger.debug('Using identity: %r' % (identity)) - environ['keystone.identity'] = identity - environ['REMOTE_USER'] = identity.get('tenant') - environ['swift.authorize'] = self.authorize - else: - self.logger.debug('Authorizing as anonymous') - environ['swift.authorize'] = self.authorize_anonymous - - environ['swift.clean_acl'] = swift_acl.clean_acl - - return self.app(environ, start_response) - - def _keystone_identity(self, environ): - """Extract the identity from the Keystone auth component.""" - if environ.get('HTTP_X_IDENTITY_STATUS') != 'Confirmed': - return - roles = [] - if 'HTTP_X_ROLES' in environ: - roles = environ['HTTP_X_ROLES'].split(',') - identity = {'user': environ.get('HTTP_X_USER_NAME'), - 'tenant': (environ.get('HTTP_X_TENANT_ID'), - environ.get('HTTP_X_TENANT_NAME')), - 'roles': roles} - return identity - - def _get_account_for_tenant(self, tenant_id): - return '%s%s' % (self.reseller_prefix, tenant_id) - - def _reseller_check(self, account, tenant_id): - """Check reseller prefix.""" - return account == self._get_account_for_tenant(tenant_id) - - def authorize(self, req): - env = req.environ - env_identity = env.get('keystone.identity', {}) - tenant_id, tenant_name = env_identity.get('tenant') - - try: - part = swift_utils.split_path(req.path, 1, 4, True) - version, account, container, obj = part - except ValueError: - return webob.exc.HTTPNotFound(request=req) - - user_roles = env_identity.get('roles', []) - - # Give unconditional access to a user with the reseller_admin - # role. - if self.reseller_admin_role in user_roles: - msg = 'User %s has reseller admin authorizing' - self.logger.debug(msg % tenant_id) - req.environ['swift_owner'] = True - return - - # Check if a user tries to access an account that does not match their - # token - if not self._reseller_check(account, tenant_id): - log_msg = 'tenant mismatch: %s != %s' % (account, tenant_id) - self.logger.debug(log_msg) - return self.denied_response(req) - - # Check the roles the user is belonging to. If the user is - # part of the role defined in the config variable - # operator_roles (like admin) then it will be - # promoted as an admin of the account/tenant. - for role in self.operator_roles.split(','): - role = role.strip() - if role in user_roles: - log_msg = 'allow user with role %s as account admin' % (role) - self.logger.debug(log_msg) - req.environ['swift_owner'] = True - return - - # If user is of the same name of the tenant then make owner of it. - user = env_identity.get('user', '') - if self.is_admin and user == tenant_name: - req.environ['swift_owner'] = True - return - - referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) - - authorized = self._authorize_unconfirmed_identity(req, obj, referrers, - roles) - if authorized: - return - elif authorized is not None: - return self.denied_response(req) - - # Allow ACL at individual user level (tenant:user format) - # For backward compatibility, check for ACL in tenant_id:user format - if ('%s:%s' % (tenant_name, user) in roles - or '%s:%s' % (tenant_id, user) in roles): - log_msg = 'user %s:%s or %s:%s allowed in ACL authorizing' - self.logger.debug(log_msg % (tenant_name, user, tenant_id, user)) - return - - # Check if we have the role in the userroles and allow it - for user_role in user_roles: - if user_role in roles: - log_msg = 'user %s:%s allowed in ACL: %s authorizing' - self.logger.debug(log_msg % (tenant_name, user, user_role)) - return - - return self.denied_response(req) - - def authorize_anonymous(self, req): - """ - Authorize an anonymous request. - - :returns: None if authorization is granted, an error page otherwise. - """ - try: - part = swift_utils.split_path(req.path, 1, 4, True) - version, account, container, obj = part - except ValueError: - return webob.exc.HTTPNotFound(request=req) - - is_authoritative_authz = (account and - account.startswith(self.reseller_prefix)) - if not is_authoritative_authz: - return self.denied_response(req) - - referrers, roles = swift_acl.parse_acl(getattr(req, 'acl', None)) - authorized = self._authorize_unconfirmed_identity(req, obj, referrers, - roles) - if not authorized: - return self.denied_response(req) - - def _authorize_unconfirmed_identity(self, req, obj, referrers, roles): - """" - Perform authorization for access that does not require a - confirmed identity. - - :returns: A boolean if authorization is granted or denied. None if - a determination could not be made. - """ - # Allow container sync. - if (req.environ.get('swift_sync_key') - and req.environ['swift_sync_key'] == - req.headers.get('x-container-sync-key', None) - and 'x-timestamp' in req.headers - and (req.remote_addr in self.allowed_sync_hosts - or swift_utils.get_remote_client(req) - in self.allowed_sync_hosts)): - log_msg = 'allowing proxy %s for container-sync' % req.remote_addr - self.logger.debug(log_msg) - return True - - # Check if referrer is allowed. - if swift_acl.referrer_allowed(req.referer, referrers): - if obj or '.rlistings' in roles: - log_msg = 'authorizing %s via referer ACL' % req.referrer - self.logger.debug(log_msg) - return True - return False - - def denied_response(self, req): - """Deny WSGI Response. - - Returns a standard WSGI response callable with the status of 403 or 401 - depending on whether the REMOTE_USER is set or not. - """ - if req.remote_user: - return webob.exc.HTTPForbidden(request=req) - else: - return webob.exc.HTTPUnauthorized(request=req) - - -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 SwiftAuth(app, conf) - return auth_filter From b7c31b96f5a4ecf654c1362053a7836582652b4d Mon Sep 17 00:00:00 2001 From: Dan Prince Date: Sat, 12 Jan 2013 22:22:42 -0500 Subject: [PATCH 087/121] Limit the size of HTTP requests. Adds a new RequestBodySizeLimiter middleware to guard against really large HTTP requests. The default max request size is 112k although this limit is configurable via the 'max_request_body_size' config parameter. Fixes LP Bug #1099025. Change-Id: Id51be3d9a0d829d63d55a92dca61a39a17629785 --- core.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core.py b/core.py index a49f743b..24495c98 100644 --- a/core.py +++ b/core.py @@ -14,7 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. +import webob.dec + from keystone.common import serializer +from keystone.common import utils from keystone.common import wsgi from keystone import config from keystone import exception @@ -164,3 +167,21 @@ class NormalizingFilter(wsgi.Middleware): # Rewrites path to root if no path is given. elif not request.environ['PATH_INFO']: request.environ['PATH_INFO'] = '/' + + +class RequestBodySizeLimiter(wsgi.Middleware): + """Limit the size of an incoming request.""" + + def __init__(self, *args, **kwargs): + super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + + if req.content_length > CONF.max_request_body_size: + raise exception.RequestTooLarge() + if req.content_length is None and req.is_body_readable: + limiter = utils.LimitingReader(req.body_file, + CONF.max_request_body_size) + req.body_file = limiter + return self.application From 46e6e977ccf1069d28b2a25ebd378176887387f7 Mon Sep 17 00:00:00 2001 From: Zhongyue Luo Date: Thu, 31 Jan 2013 14:23:27 +0800 Subject: [PATCH 088/121] Fixes 'not in' operator usage Change-Id: I50a5bbe4800fc88b631701a6be0a0f9feec597d0 --- core.py | 2 +- s3_token.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core.py b/core.py index 24495c98..4582f01e 100644 --- a/core.py +++ b/core.py @@ -105,7 +105,7 @@ class JsonBodyMiddleware(wsgi.Middleware): # Reject unrecognized content types. Empty string indicates # the client did not explicitly set the header - if not request.content_type in ('application/json', ''): + if request.content_type not in ('application/json', ''): e = exception.ValidationError(attribute='application/json', target='Content-Type header') return wsgi.render_exception(e) diff --git a/s3_token.py b/s3_token.py index 0f207b3d..477de999 100644 --- a/s3_token.py +++ b/s3_token.py @@ -128,7 +128,7 @@ class S3Token(object): return self.app(environ, start_response) # Read request signature and access id. - if not 'Authorization' in req.headers: + if 'Authorization' not in req.headers: msg = 'No Authorization header. skipping.' self.logger.debug(msg) return self.app(environ, start_response) From e53c617a369ba6deaa2def330f79f503c0529795 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 12 Feb 2013 11:17:31 -0800 Subject: [PATCH 089/121] Fix spelling mistakes git ls-files | misspellings -f - Source: https://github.com/lyda/misspell-check Change-Id: Icbd2412aa65bc8135e5dcd83ee69e94f5a42f7a2 --- s3_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/s3_token.py b/s3_token.py index 477de999..6c39b9d5 100644 --- a/s3_token.py +++ b/s3_token.py @@ -177,7 +177,7 @@ class S3Token(object): # NOTE(chmou): We still have the same problem we would need to # change token_auth to detect if we already # identified and not doing a second query and just - # pass it thru to swiftauth in this case. + # pass it through to swiftauth in this case. try: resp, output = self._json_request(creds_json) except ServiceError as e: From 914ddf76486351609b7d8c5a68ccf9c7b33be3c7 Mon Sep 17 00:00:00 2001 From: Guang Yee Date: Tue, 8 Jan 2013 08:46:20 -0800 Subject: [PATCH 090/121] v3 token API Also implemented the following: blueprint pluggable-identity-authentication-handlers blueprint stop-ids-in-uris blueprint multi-factor-authn (just the plumbing) What's missing? * domain scoping (will be implemented by Henry?) Change-Id: I191c0b2cb3367b2a5f8a2dc674c284bb13ea97e3 --- core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core.py b/core.py index 4582f01e..d904e3c0 100644 --- a/core.py +++ b/core.py @@ -31,6 +31,10 @@ CONF = config.CONF AUTH_TOKEN_HEADER = 'X-Auth-Token' +# Header used to transmit the subject token +SUBJECT_TOKEN_HEADER = 'X-Subject-Token' + + # Environment variable used to pass the request context CONTEXT_ENV = wsgi.CONTEXT_ENV @@ -44,6 +48,9 @@ class TokenAuthMiddleware(wsgi.Middleware): token = request.headers.get(AUTH_TOKEN_HEADER) context = request.environ.get(CONTEXT_ENV, {}) context['token_id'] = token + if SUBJECT_TOKEN_HEADER in request.headers: + context['subject_token_id'] = ( + request.headers.get(SUBJECT_TOKEN_HEADER)) request.environ[CONTEXT_ENV] = context From fa8bdb330a320b680f75c841e8fea15c6167a59b Mon Sep 17 00:00:00 2001 From: Chmouel Boudjnah Date: Tue, 19 Feb 2013 12:03:47 +0100 Subject: [PATCH 091/121] Rework S3Token middleware tests. - Split between good and bad tests. - Add more tests to get to 100% coverage. Change-Id: Iffd00c2b557e54b122f29f8b0ec7f7ab7a92d16e --- s3_token.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/s3_token.py b/s3_token.py index 6c39b9d5..74be5864 100644 --- a/s3_token.py +++ b/s3_token.py @@ -85,15 +85,14 @@ class S3Token(object): def _json_request(self, creds_json): headers = {'Content-Type': 'application/json'} - + if self.auth_protocol == 'http': + conn = self.http_client_class(self.auth_host, self.auth_port) + else: + conn = self.http_client_class(self.auth_host, + self.auth_port, + self.key_file, + self.cert_file) try: - if self.auth_protocol == 'http': - conn = self.http_client_class(self.auth_host, self.auth_port) - else: - conn = self.http_client_class(self.auth_host, - self.auth_port, - self.key_file, - self.cert_file) conn.request('POST', '/v2.0/s3tokens', body=creds_json, headers=headers) From 3b701cdaa25cca58588a3d29626507655160dac1 Mon Sep 17 00:00:00 2001 From: Guang Yee Date: Mon, 25 Feb 2013 12:46:16 -0800 Subject: [PATCH 092/121] bug 1131840: fix auth and token data for XML translation Change-Id: I4408b3e6e0752ca75bc36399f5148890820e9a89 --- core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core.py b/core.py index d904e3c0..29a6832b 100644 --- a/core.py +++ b/core.py @@ -16,6 +16,7 @@ import webob.dec +from keystone.common import logging from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi @@ -25,6 +26,7 @@ from keystone.openstack.common import jsonutils CONF = config.CONF +LOG = logging.getLogger(__name__) # Header used to transmit the auth token @@ -158,6 +160,7 @@ class XmlBodyMiddleware(wsgi.Middleware): body_obj = jsonutils.loads(response.body) response.body = serializer.to_xml(body_obj) except Exception: + LOG.exception('Serializer failed') raise exception.Error(message=response.body) return response From b824faa91ba291069d9cbeb1403c6656f2387cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=B6ppner?= <0xffea@gmail.com> Date: Sun, 10 Mar 2013 20:04:07 +0100 Subject: [PATCH 093/121] xml_body returns backtrace on XMLSyntaxError Protected against XMLSyntaxError that can occur in from_xml. Return a validation error (400) instead of an internal server error (500). Change-Id: Ic5160f4f6c810e96b74dbf9563547ac739a54c5e Fixes: bug #1101043 --- core.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core.py b/core.py index 29a6832b..99cba4a0 100644 --- a/core.py +++ b/core.py @@ -149,7 +149,14 @@ class XmlBodyMiddleware(wsgi.Middleware): incoming_xml = 'application/xml' in str(request.content_type) if incoming_xml and request.body: request.content_type = 'application/json' - request.body = jsonutils.dumps(serializer.from_xml(request.body)) + try: + request.body = jsonutils.dumps( + serializer.from_xml(request.body)) + except Exception: + LOG.exception('Serializer failed') + e = exception.ValidationError(attribute='valid XML', + target='request body') + return wsgi.render_exception(e) def process_response(self, request, response): """Transform the response from JSON to XML.""" From ac1a9ac9ff7c19b0373e35eaf8fe78a3701551bb Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Tue, 19 Mar 2013 17:15:25 -0500 Subject: [PATCH 094/121] Wrap config module and require manual setup (bug 1143998) This moves keystone.config to keystone.common.config, which requires .configure() to be called manually in order for options to be registered. keystone.config preserves the existing behavior of automatically registering options when imported. keystone.middleware.auth_token and it's dependencies within keystone no longer cause config options to be automatically registered. This is an alternative to https://review.openstack.org/#/c/24251/ Change-Id: If9eb5799bf77595ecb71f2000f8b6d1610ea9700 --- core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core.py b/core.py index 99cba4a0..863ef948 100644 --- a/core.py +++ b/core.py @@ -16,11 +16,11 @@ import webob.dec +from keystone.common import config from keystone.common import logging from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi -from keystone import config from keystone import exception from keystone.openstack.common import jsonutils From 8161cdb4debaa273b6a53e9327d14ff94a0fd56b Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 16 May 2013 14:06:59 -0500 Subject: [PATCH 095/121] Satisfy flake8 import rules F401 and F403 - Removed unused imports - Ignore wildcard and unused imports from core modules (and avoid wildcard imports otherwise) to __init__ modules Change-Id: Ie2e5f61ae37481f5d248788cfd83dc92ffddbd91 --- __init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/__init__.py b/__init__.py index 85ed395c..1135f784 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,5 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 +# flake8: noqa # Copyright 2012 OpenStack LLC # From 7890a570ad2849c2289434206aef19648c207edb Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 23 May 2013 15:09:14 -0500 Subject: [PATCH 096/121] import only modules (flake8 H302) Change-Id: I0fa6fc6bf9d51b60fa987a0040168f3f0ef78a4a --- ec2_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index daac10aa..7cd007c7 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -22,7 +22,7 @@ Starting point for routing EC2 requests. """ -from urlparse import urlparse +import urlparse from eventlet.green import httplib import webob.dec @@ -73,7 +73,7 @@ class EC2Token(wsgi.Middleware): # Disable 'has no x member' pylint error # for httplib and urlparse # pylint: disable-msg=E1101 - o = urlparse(FLAGS.keystone_ec2_url) + o = urlparse.urlparse(FLAGS.keystone_ec2_url) if o.scheme == 'http': conn = httplib.HTTPConnection(o.netloc) else: From e29b2d3ed128cc7e51c4efe158cb3221faa556ab Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Thu, 30 May 2013 17:48:04 +1000 Subject: [PATCH 097/121] Isolate eventlet code into environment. The environment module will be configured once, during code initialization. Subsequently all other possibly-evented modules will retrieve from environment and transparently obtain either the eventlet or standard library modules. If eventlet, httplib, subprocess or other environment dependant module is referenced outside of the environment module it should be considered a bug. The changes to tests are required to ensure that test is imported first to setup the environment. Hopefully these can all be replaced with an __init__.py in a post-nose keystone. Implements: blueprint extract-eventlet Change-Id: Icacd6f2ee0906ac5d303777c1f87a184f38283bf --- ec2_token.py | 6 +++--- s3_token.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index 7cd007c7..265ccaa6 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -24,7 +24,6 @@ Starting point for routing EC2 requests. import urlparse -from eventlet.green import httplib import webob.dec import webob.exc @@ -32,6 +31,7 @@ from nova import flags from nova import utils from nova import wsgi +from keystone.common import environment FLAGS = flags.FLAGS flags.DEFINE_string('keystone_ec2_url', @@ -75,9 +75,9 @@ class EC2Token(wsgi.Middleware): # pylint: disable-msg=E1101 o = urlparse.urlparse(FLAGS.keystone_ec2_url) if o.scheme == 'http': - conn = httplib.HTTPConnection(o.netloc) + conn = environment.httplib.HTTPConnection(o.netloc) else: - conn = httplib.HTTPSConnection(o.netloc) + conn = environment.httplib.HTTPSConnection(o.netloc) conn.request('POST', o.path, body=creds_json, headers=headers) response = conn.getresponse().read() conn.close() diff --git a/s3_token.py b/s3_token.py index 74be5864..a5eff289 100644 --- a/s3_token.py +++ b/s3_token.py @@ -33,13 +33,13 @@ This WSGI component: """ -import httplib - import webob -from keystone.openstack.common import jsonutils from swift.common import utils as swift_utils +from keystone.common import environment +from keystone.openstack.common import jsonutils + PROTOCOL_NAME = 'S3 Token Authentication' @@ -62,9 +62,9 @@ class S3Token(object): self.auth_port = int(conf.get('auth_port', 35357)) self.auth_protocol = conf.get('auth_protocol', 'https') if self.auth_protocol == 'http': - self.http_client_class = httplib.HTTPConnection + self.http_client_class = environment.httplib.HTTPConnection else: - self.http_client_class = httplib.HTTPSConnection + self.http_client_class = environment.httplib.HTTPSConnection # SSL self.cert_file = conf.get('certfile') self.key_file = conf.get('keyfile') From ed0e82019584c6c4f070ff2f2aa84e6bb2995608 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 26 Jun 2013 09:45:58 +1000 Subject: [PATCH 098/121] Revert environment module usage in middleware. Devstack is pulling s3_token into the swift pipeline and so depending on keystone.environment is breaking devstack installs. Fixes bug 1193112 Change-Id: Ifd89e542f79a2bee00113e7df676d30da0f05e59 --- ec2_token.py | 6 +++--- s3_token.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index 265ccaa6..7cd007c7 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -24,6 +24,7 @@ Starting point for routing EC2 requests. import urlparse +from eventlet.green import httplib import webob.dec import webob.exc @@ -31,7 +32,6 @@ from nova import flags from nova import utils from nova import wsgi -from keystone.common import environment FLAGS = flags.FLAGS flags.DEFINE_string('keystone_ec2_url', @@ -75,9 +75,9 @@ class EC2Token(wsgi.Middleware): # pylint: disable-msg=E1101 o = urlparse.urlparse(FLAGS.keystone_ec2_url) if o.scheme == 'http': - conn = environment.httplib.HTTPConnection(o.netloc) + conn = httplib.HTTPConnection(o.netloc) else: - conn = environment.httplib.HTTPSConnection(o.netloc) + conn = httplib.HTTPSConnection(o.netloc) conn.request('POST', o.path, body=creds_json, headers=headers) response = conn.getresponse().read() conn.close() diff --git a/s3_token.py b/s3_token.py index a5eff289..2b7f99a0 100644 --- a/s3_token.py +++ b/s3_token.py @@ -33,11 +33,11 @@ This WSGI component: """ +import httplib import webob from swift.common import utils as swift_utils -from keystone.common import environment from keystone.openstack.common import jsonutils @@ -62,9 +62,9 @@ class S3Token(object): self.auth_port = int(conf.get('auth_port', 35357)) self.auth_protocol = conf.get('auth_protocol', 'https') if self.auth_protocol == 'http': - self.http_client_class = environment.httplib.HTTPConnection + self.http_client_class = httplib.HTTPConnection else: - self.http_client_class = environment.httplib.HTTPSConnection + self.http_client_class = httplib.HTTPSConnection # SSL self.cert_file = conf.get('certfile') self.key_file = conf.get('keyfile') From 7e8e0a594827a765f5bb76d480d8343408280761 Mon Sep 17 00:00:00 2001 From: Kun Huang Date: Sun, 21 Jul 2013 23:55:45 +0800 Subject: [PATCH 099/121] remove swift dependency of s3 middleware In middleware/s3_token.py, here only use swift for a logger and path split functionality. We should remove swift dependency by using new codes. fixes bug #1178738 Change-Id: Icc2648720e220a873d1fb8e9961d777ceabef70b --- s3_token.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/s3_token.py b/s3_token.py index 2b7f99a0..b346893b 100644 --- a/s3_token.py +++ b/s3_token.py @@ -34,14 +34,62 @@ This WSGI component: """ import httplib +import urllib import webob -from swift.common import utils as swift_utils - +from keystone.common import logging from keystone.openstack.common import jsonutils PROTOCOL_NAME = 'S3 Token Authentication' +LOG = logging.getLogger(__name__) + + +# TODO(kun): remove it after oslo merge this. +def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): + """Validate and split the given HTTP request path. + + **Examples**:: + + ['a'] = split_path('/a') + ['a', None] = split_path('/a', 1, 2) + ['a', 'c'] = split_path('/a/c', 1, 2) + ['a', 'c', 'o/r'] = split_path('/a/c/o/r', 1, 3, True) + + :param path: HTTP Request path to be split + :param minsegs: Minimum number of segments to be extracted + :param maxsegs: Maximum number of segments to be extracted + :param rest_with_last: If True, trailing data will be returned as part + of last segment. If False, and there is + trailing data, raises ValueError. + :returns: list of segments with a length of maxsegs (non-existant + segments will return as None) + :raises: ValueError if given an invalid path + """ + if not maxsegs: + maxsegs = minsegs + if minsegs > maxsegs: + raise ValueError('minsegs > maxsegs: %d > %d' % (minsegs, maxsegs)) + if rest_with_last: + segs = path.split('/', maxsegs) + minsegs += 1 + maxsegs += 1 + count = len(segs) + if (segs[0] or count < minsegs or count > maxsegs or + '' in segs[1:minsegs]): + raise ValueError('Invalid path: %s' % urllib.quote(path)) + else: + minsegs += 1 + maxsegs += 1 + segs = path.split('/', maxsegs) + count = len(segs) + if (segs[0] or count < minsegs or count > maxsegs + 1 or + '' in segs[1:minsegs] or + (count == maxsegs + 1 and segs[maxsegs])): + raise ValueError('Invalid path: %s' % urllib.quote(path)) + segs = segs[1:maxsegs] + segs.extend([None] * (maxsegs - 1 - len(segs))) + return segs class ServiceError(Exception): @@ -54,7 +102,7 @@ class S3Token(object): def __init__(self, app, conf): """Common initialization code.""" self.app = app - self.logger = swift_utils.get_logger(conf, log_route='s3token') + self.logger = LOG self.logger.debug('Starting the %s component' % PROTOCOL_NAME) self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_') # where to find the auth service (we use this to validate tokens) @@ -119,7 +167,7 @@ class S3Token(object): self.logger.debug('Calling S3Token middleware.') try: - parts = swift_utils.split_path(req.path, 1, 4, True) + parts = split_path(req.path, 1, 4, True) version, account, container, obj = parts except ValueError: msg = 'Not a path query, skipping.' From 48819af6cb344ef828a1b97786105a23701bc9e9 Mon Sep 17 00:00:00 2001 From: Lance Bragstad Date: Mon, 12 Aug 2013 17:41:40 +0000 Subject: [PATCH 100/121] Refactor Keystone to use unified logging from Oslo Modifications to use log from /keystone/openstack/common/log.py instead of /keystone/common/logging.py. This change also includes some refactoring to remove the WriteableLogger class from common/wsgi.py since that is already included in the unified logging sync from Oslo. This also moves fail_gracefully from /keystone/common/logging.py to service.py as it is only used within that module. blueprint unified-logging-in-keystone Change-Id: I24b319bd6cfe5e345ea903196188f2394f4ef102 --- core.py | 3 +-- s3_token.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core.py b/core.py index 863ef948..92b179c3 100644 --- a/core.py +++ b/core.py @@ -17,13 +17,12 @@ import webob.dec from keystone.common import config -from keystone.common import logging from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi from keystone import exception from keystone.openstack.common import jsonutils - +from keystone.openstack.common import log as logging CONF = config.CONF LOG = logging.getLogger(__name__) diff --git a/s3_token.py b/s3_token.py index b346893b..39678591 100644 --- a/s3_token.py +++ b/s3_token.py @@ -37,8 +37,8 @@ import httplib import urllib import webob -from keystone.common import logging from keystone.openstack.common import jsonutils +from keystone.openstack.common import log as logging PROTOCOL_NAME = 'S3 Token Authentication' From 034bbc4402b8b57e7f8f0fb3e56797dc14b4be38 Mon Sep 17 00:00:00 2001 From: Sean Winn Date: Mon, 26 Aug 2013 06:32:10 -0700 Subject: [PATCH 101/121] Changed header from LLC to Foundation based on trademark policies Fixes: Bug 1214176 Change-Id: Ie937081a53d377671b8b88f422642c8131002f88 --- __init__.py | 2 +- auth_token.py | 2 +- core.py | 2 +- ec2_token.py | 2 +- s3_token.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 1135f784..9bfd68ff 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # flake8: noqa -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # # 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 diff --git a/auth_token.py b/auth_token.py index 25e2685e..63b7365a 100644 --- a/auth_token.py +++ b/auth_token.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010-2012 OpenStack LLC +# Copyright 2010-2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/core.py b/core.py index 92b179c3..fc88af61 100644 --- a/core.py +++ b/core.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # # 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 diff --git a/ec2_token.py b/ec2_token.py index 7cd007c7..0cd58418 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. diff --git a/s3_token.py b/s3_token.py index 39678591..342cee46 100644 --- a/s3_token.py +++ b/s3_token.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2012 OpenStack LLC +# Copyright 2012 OpenStack Foundation # Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011,2012 Akira YOSHIYAMA From ee01781dee5ac1457a8416b25a791bb4b6476700 Mon Sep 17 00:00:00 2001 From: Lei Zhang Date: Tue, 8 Oct 2013 17:40:37 +0800 Subject: [PATCH 102/121] remove the nova dependency in the ec2_token middleware Change-Id: I34812522b55e38d3ea030638bbae75d65f507c90 Closes-Bug: #1178740 --- ec2_token.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index 0cd58418..4e58eacb 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -25,18 +25,22 @@ Starting point for routing EC2 requests. import urlparse from eventlet.green import httplib +from oslo.config import cfg import webob.dec import webob.exc -from nova import flags -from nova import utils -from nova import wsgi +from keystone.common import config +from keystone.common import wsgi +from keystone.openstack.common import jsonutils +keystone_ec2_opts = [ + cfg.StrOpt('keystone_ec2_url', + default='http://localhost:5000/v2.0/ec2tokens', + help='URL to get token from ec2 request.'), +] -FLAGS = flags.FLAGS -flags.DEFINE_string('keystone_ec2_url', - 'http://localhost:5000/v2.0/ec2tokens', - 'URL to get token from ec2 request.') +CONF = config.CONF +CONF.register_opts(keystone_ec2_opts) class EC2Token(wsgi.Middleware): @@ -67,13 +71,13 @@ class EC2Token(wsgi.Middleware): 'params': auth_params, } } - creds_json = utils.dumps(creds) + creds_json = jsonutils.dumps(creds) headers = {'Content-Type': 'application/json'} # Disable 'has no x member' pylint error # for httplib and urlparse # pylint: disable-msg=E1101 - o = urlparse.urlparse(FLAGS.keystone_ec2_url) + o = urlparse.urlparse(CONF.keystone_ec2_url) if o.scheme == 'http': conn = httplib.HTTPConnection(o.netloc) else: @@ -86,7 +90,7 @@ class EC2Token(wsgi.Middleware): # having keystone return token, tenant, # user, and roles from this call. - result = utils.loads(response) + result = jsonutils.loads(response) try: token_id = result['access']['token']['id'] except (AttributeError, KeyError): From d6f3297275e9a2446fd6a9bee2548b648a1a0a5b Mon Sep 17 00:00:00 2001 From: Yaguang Tang Date: Wed, 13 Nov 2013 11:57:38 +0800 Subject: [PATCH 103/121] Remove deprecated auth_token middleware The actual code for auth_token has been moved python-keystoneclient since Grizzly, and keep it in keystone for backward compatibility over Havana Release. So now we can safely remove it in Icehouse. bp icehouse-deprecated-removed Change-Id: I6bfc92c2cb9d836bdc7130910a679a8b660096e9 --- auth_token.py | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 auth_token.py diff --git a/auth_token.py b/auth_token.py deleted file mode 100644 index 63b7365a..00000000 --- a/auth_token.py +++ /dev/null @@ -1,33 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2012 OpenStack Foundation -# -# 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. - -""" -The actual code for auth_token has now been moved python-keystoneclient. It is -imported back here to ensure backward combatibility for old paste.ini files -that might still refer to here as opposed to keystoneclient -""" - -from keystoneclient.middleware import auth_token as client_auth_token - -will_expire_soon = client_auth_token.will_expire_soon -InvalidUserToken = client_auth_token.InvalidUserToken -ServiceError = client_auth_token.ServiceError -ConfigurationError = client_auth_token.ConfigurationError -AuthProtocol = client_auth_token.AuthProtocol - -filter_factory = client_auth_token.filter_factory -app_factory = client_auth_token.app_factory From f6b62de9e4aa66ba395d21277b4894b209a64094 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Wed, 1 Jan 2014 15:48:32 -0800 Subject: [PATCH 104/121] Make common log import consistent Across the project, make the common log import consistent. Change-Id: I937cd5d337db5d2296dd9238685803bbba3391b3 --- core.py | 4 ++-- s3_token.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core.py b/core.py index fc88af61..09b8c4e5 100644 --- a/core.py +++ b/core.py @@ -22,10 +22,10 @@ from keystone.common import utils from keystone.common import wsgi from keystone import exception from keystone.openstack.common import jsonutils -from keystone.openstack.common import log as logging +from keystone.openstack.common import log CONF = config.CONF -LOG = logging.getLogger(__name__) +LOG = log.getLogger(__name__) # Header used to transmit the auth token diff --git a/s3_token.py b/s3_token.py index 342cee46..b1ab6790 100644 --- a/s3_token.py +++ b/s3_token.py @@ -38,11 +38,11 @@ import urllib import webob from keystone.openstack.common import jsonutils -from keystone.openstack.common import log as logging +from keystone.openstack.common import log PROTOCOL_NAME = 'S3 Token Authentication' -LOG = logging.getLogger(__name__) +LOG = log.getLogger(__name__) # TODO(kun): remove it after oslo merge this. From 64ecfb9337adb1863b597c8ed58038348de1b9f8 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Tue, 21 Jan 2014 20:42:55 +0800 Subject: [PATCH 105/121] Use six to make dict work in Python 2 and Python 3 Dict's interface changed a lot between Python 2 and Python 3. Use six to make dict work in them. This is mapping: six Python 2 Python 3 six.iterkeys dict.iterkeys dict.keys six.itervalues dict.itervalues dict.values six.iteritems dict.iteritems dict.items Part of blueprint keystone-py3kcompat Change-Id: If41b19353f07853e39064f848ba74e9cdfd9bd14 --- core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core.py b/core.py index 09b8c4e5..6c93352b 100644 --- a/core.py +++ b/core.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +import six import webob.dec from keystone.common import config @@ -81,7 +82,7 @@ class PostParamsMiddleware(wsgi.Middleware): def process_request(self, request): params_parsed = request.params params = {} - for k, v in params_parsed.iteritems(): + for k, v in six.iteritems(params_parsed): if k in ('self', 'context'): continue if k.startswith('_'): @@ -130,7 +131,7 @@ class JsonBodyMiddleware(wsgi.Middleware): params_parsed = {} params = {} - for k, v in params_parsed.iteritems(): + for k, v in six.iteritems(params_parsed): if k in ('self', 'context'): continue if k.startswith('_'): From 184b3b4ddd755929d808776296da27b3d5719042 Mon Sep 17 00:00:00 2001 From: guang-yee Date: Wed, 13 Nov 2013 23:17:56 -0800 Subject: [PATCH 106/121] build auth context from middleware Refactors the logic where we build the credentials (or auth context), to be used for authorization decision, from the incoming auth token to be handled by middleware. The authorization code is isolated into keystone.common.authorization. This patch lays the plumbing for bp client-ssl-certificate-authorization. DocImpact: added a new build_auth_context middleware filter to the API pipelines in keystone-paste.ini. It is responsible for building the auth context (a.k.a. policy-check credentials) based on the incoming auth token to facilitate API authorization. Auth context is required for API authorization. Change-Id: Icee58a5bd47b0ded3b53a8838012864c1c0abcc5 --- core.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core.py b/core.py index 6c93352b..11bd21d7 100644 --- a/core.py +++ b/core.py @@ -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 From 15572ddc55c9aa19c84a3f457835732c8e96a1e8 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Wed, 22 Jan 2014 22:33:37 +0800 Subject: [PATCH 107/121] Replace urllib/urlparse with six.moves.* urllib and urlparse are divided to different module in Python 3. Use six.moves.* to make them on Python 2 and Python 3. Part of blueprint keystone-py3kcompat Change-Id: I54d322c51f0f87e3ec00c69016251d6ea5015adb --- ec2_token.py | 5 ++--- s3_token.py | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index 4e58eacb..1f4e8c8a 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -22,10 +22,9 @@ Starting point for routing EC2 requests. """ -import urlparse - from eventlet.green import httplib from oslo.config import cfg +from six.moves import urllib import webob.dec import webob.exc @@ -77,7 +76,7 @@ class EC2Token(wsgi.Middleware): # Disable 'has no x member' pylint error # for httplib and urlparse # pylint: disable-msg=E1101 - o = urlparse.urlparse(CONF.keystone_ec2_url) + o = urllib.parse.urlparse(CONF.keystone_ec2_url) if o.scheme == 'http': conn = httplib.HTTPConnection(o.netloc) else: diff --git a/s3_token.py b/s3_token.py index b1ab6790..6e55001d 100644 --- a/s3_token.py +++ b/s3_token.py @@ -34,7 +34,8 @@ This WSGI component: """ import httplib -import urllib + +from six.moves import urllib import webob from keystone.openstack.common import jsonutils @@ -77,7 +78,7 @@ def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): count = len(segs) if (segs[0] or count < minsegs or count > maxsegs or '' in segs[1:minsegs]): - raise ValueError('Invalid path: %s' % urllib.quote(path)) + raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) else: minsegs += 1 maxsegs += 1 @@ -86,7 +87,7 @@ def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): if (segs[0] or count < minsegs or count > maxsegs + 1 or '' in segs[1:minsegs] or (count == maxsegs + 1 and segs[maxsegs])): - raise ValueError('Invalid path: %s' % urllib.quote(path)) + raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) segs = segs[1:maxsegs] segs.extend([None] * (maxsegs - 1 - len(segs))) return segs From 43a375dbeeae4504ad1af112e3ef8def27158218 Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Thu, 6 Feb 2014 17:30:00 -0600 Subject: [PATCH 108/121] Deprecate s3_token middleware The s3_token middleware is now deprecated in favor of the s3_token middleware in python-keystoneclient. bp s3-token-to-keystoneclient bp deprecated-as-of-icehouse Change-Id: Ic39b0d444124020ca52e1621d640fdd512cc8df4 --- s3_token.py | 239 ++++------------------------------------------------ 1 file changed, 16 insertions(+), 223 deletions(-) diff --git a/s3_token.py b/s3_token.py index 6e55001d..b9c51bf4 100644 --- a/s3_token.py +++ b/s3_token.py @@ -24,6 +24,9 @@ """ S3 TOKEN MIDDLEWARE +The S3 Token middleware is deprecated as of IceHouse. It's been moved into +python-keystoneclient, `keystoneclient.middleware.s3_token`. + This WSGI component: * Get a request from the swift3 middleware with an S3 Authorization @@ -33,233 +36,23 @@ This WSGI component: """ -import httplib +from keystoneclient.middleware import s3_token -from six.moves import urllib -import webob - -from keystone.openstack.common import jsonutils -from keystone.openstack.common import log +from keystone.openstack.common import versionutils -PROTOCOL_NAME = 'S3 Token Authentication' -LOG = log.getLogger(__name__) +PROTOCOL_NAME = s3_token.PROTOCOL_NAME +split_path = s3_token.split_path +ServiceError = s3_token.ServiceError +filter_factory = s3_token.filter_factory -# TODO(kun): remove it after oslo merge this. -def split_path(path, minsegs=1, maxsegs=None, rest_with_last=False): - """Validate and split the given HTTP request path. - - **Examples**:: - - ['a'] = split_path('/a') - ['a', None] = split_path('/a', 1, 2) - ['a', 'c'] = split_path('/a/c', 1, 2) - ['a', 'c', 'o/r'] = split_path('/a/c/o/r', 1, 3, True) - - :param path: HTTP Request path to be split - :param minsegs: Minimum number of segments to be extracted - :param maxsegs: Maximum number of segments to be extracted - :param rest_with_last: If True, trailing data will be returned as part - of last segment. If False, and there is - trailing data, raises ValueError. - :returns: list of segments with a length of maxsegs (non-existant - segments will return as None) - :raises: ValueError if given an invalid path - """ - if not maxsegs: - maxsegs = minsegs - if minsegs > maxsegs: - raise ValueError('minsegs > maxsegs: %d > %d' % (minsegs, maxsegs)) - if rest_with_last: - segs = path.split('/', maxsegs) - minsegs += 1 - maxsegs += 1 - count = len(segs) - if (segs[0] or count < minsegs or count > maxsegs or - '' in segs[1:minsegs]): - raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) - else: - minsegs += 1 - maxsegs += 1 - segs = path.split('/', maxsegs) - count = len(segs) - if (segs[0] or count < minsegs or count > maxsegs + 1 or - '' in segs[1:minsegs] or - (count == maxsegs + 1 and segs[maxsegs])): - raise ValueError('Invalid path: %s' % urllib.parse.quote(path)) - segs = segs[1:maxsegs] - segs.extend([None] * (maxsegs - 1 - len(segs))) - return segs - - -class ServiceError(Exception): - pass - - -class S3Token(object): - """Auth Middleware that handles S3 authenticating client calls.""" +class S3Token(s3_token.S3Token): + @versionutils.deprecated( + versionutils.deprecated.ICEHOUSE, + in_favor_of='keystoneclient.middleware.s3_token', + remove_in=+1, + what='keystone.middleware.s3_token') def __init__(self, app, conf): - """Common initialization code.""" - self.app = app - self.logger = LOG - self.logger.debug('Starting the %s component' % PROTOCOL_NAME) - self.reseller_prefix = conf.get('reseller_prefix', 'AUTH_') - # where to find the auth service (we use this to validate tokens) - self.auth_host = conf.get('auth_host') - self.auth_port = int(conf.get('auth_port', 35357)) - self.auth_protocol = conf.get('auth_protocol', 'https') - if self.auth_protocol == 'http': - self.http_client_class = httplib.HTTPConnection - else: - self.http_client_class = httplib.HTTPSConnection - # SSL - self.cert_file = conf.get('certfile') - self.key_file = conf.get('keyfile') - - def deny_request(self, code): - error_table = { - 'AccessDenied': (401, 'Access denied'), - 'InvalidURI': (400, 'Could not parse the specified URI'), - } - resp = webob.Response(content_type='text/xml') - resp.status = error_table[code][0] - resp.body = error_table[code][1] - resp.body = ('\r\n' - '\r\n %s\r\n ' - '%s\r\n\r\n' % - (code, error_table[code][1])) - return resp - - def _json_request(self, creds_json): - headers = {'Content-Type': 'application/json'} - if self.auth_protocol == 'http': - conn = self.http_client_class(self.auth_host, self.auth_port) - else: - conn = self.http_client_class(self.auth_host, - self.auth_port, - self.key_file, - self.cert_file) - try: - conn.request('POST', '/v2.0/s3tokens', - body=creds_json, - headers=headers) - response = conn.getresponse() - output = response.read() - except Exception as e: - self.logger.info('HTTP connection exception: %s' % e) - resp = self.deny_request('InvalidURI') - raise ServiceError(resp) - finally: - conn.close() - - if response.status < 200 or response.status >= 300: - self.logger.debug('Keystone reply error: status=%s reason=%s' % - (response.status, response.reason)) - resp = self.deny_request('AccessDenied') - raise ServiceError(resp) - - return (response, output) - - def __call__(self, environ, start_response): - """Handle incoming request. authenticate and send downstream.""" - req = webob.Request(environ) - self.logger.debug('Calling S3Token middleware.') - - try: - parts = split_path(req.path, 1, 4, True) - version, account, container, obj = parts - except ValueError: - msg = 'Not a path query, skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - # Read request signature and access id. - if 'Authorization' not in req.headers: - msg = 'No Authorization header. skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - token = req.headers.get('X-Auth-Token', - req.headers.get('X-Storage-Token')) - if not token: - msg = 'You did not specify a auth or a storage token. skipping.' - self.logger.debug(msg) - return self.app(environ, start_response) - - auth_header = req.headers['Authorization'] - try: - access, signature = auth_header.split(' ')[-1].rsplit(':', 1) - except ValueError: - msg = 'You have an invalid Authorization header: %s' - self.logger.debug(msg % (auth_header)) - return self.deny_request('InvalidURI')(environ, start_response) - - # NOTE(chmou): This is to handle the special case with nova - # when we have the option s3_affix_tenant. We will force it to - # connect to another account than the one - # authenticated. Before people start getting worried about - # security, I should point that we are connecting with - # username/token specified by the user but instead of - # connecting to its own account we will force it to go to an - # another account. In a normal scenario if that user don't - # have the reseller right it will just fail but since the - # reseller account can connect to every account it is allowed - # by the swift_auth middleware. - force_tenant = None - if ':' in access: - access, force_tenant = access.split(':') - - # Authenticate request. - creds = {'credentials': {'access': access, - 'token': token, - 'signature': signature}} - creds_json = jsonutils.dumps(creds) - self.logger.debug('Connecting to Keystone sending this JSON: %s' % - creds_json) - # NOTE(vish): We could save a call to keystone by having - # keystone return token, tenant, user, and roles - # from this call. - # - # NOTE(chmou): We still have the same problem we would need to - # change token_auth to detect if we already - # identified and not doing a second query and just - # pass it through to swiftauth in this case. - try: - resp, output = self._json_request(creds_json) - except ServiceError as e: - resp = e.args[0] - msg = 'Received error, exiting middleware with error: %s' - self.logger.debug(msg % (resp.status)) - return resp(environ, start_response) - - self.logger.debug('Keystone Reply: Status: %d, Output: %s' % ( - resp.status, output)) - - try: - identity_info = jsonutils.loads(output) - token_id = str(identity_info['access']['token']['id']) - tenant = identity_info['access']['token']['tenant'] - except (ValueError, KeyError): - error = 'Error on keystone reply: %d %s' - self.logger.debug(error % (resp.status, str(output))) - return self.deny_request('InvalidURI')(environ, start_response) - - req.headers['X-Auth-Token'] = token_id - tenant_to_connect = force_tenant or tenant['id'] - self.logger.debug('Connecting with tenant: %s' % (tenant_to_connect)) - new_tenant_name = '%s%s' % (self.reseller_prefix, tenant_to_connect) - environ['PATH_INFO'] = environ['PATH_INFO'].replace(account, - new_tenant_name) - return self.app(environ, start_response) - - -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 S3Token(app, conf) - return auth_filter + super(S3Token, self).__init__(app, conf) From 465a3dddc2a9f6b16eeddef6128f0c3371a818e0 Mon Sep 17 00:00:00 2001 From: Eric Guo Date: Sat, 8 Feb 2014 23:15:34 +0800 Subject: [PATCH 109/121] Remove vim header We don't need vim modelines in each source file, it can be set in user's vimrc. Change-Id: Ie51ad62946afdf39eadcd59edaf8134ec10265c6 Closes-Bug: #1229324 --- __init__.py | 1 - core.py | 2 -- ec2_token.py | 2 -- s3_token.py | 2 -- 4 files changed, 7 deletions(-) diff --git a/__init__.py b/__init__.py index 9bfd68ff..d805f82d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 # flake8: noqa # Copyright 2012 OpenStack Foundation diff --git a/core.py b/core.py index 11bd21d7..8d6cf518 100644 --- a/core.py +++ b/core.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/ec2_token.py b/ec2_token.py index 1f4e8c8a..36589084 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -1,5 +1,3 @@ -# 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. diff --git a/s3_token.py b/s3_token.py index 6e55001d..5533427a 100644 --- a/s3_token.py +++ b/s3_token.py @@ -1,5 +1,3 @@ -# 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. From 0587004053e6cd3a675ee5f6095760e0f6117956 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Tue, 4 Feb 2014 17:22:40 +1000 Subject: [PATCH 110/121] Use WebOb directly for locale testing In the convert to Pecan we can't overload the Request type like we do currently. At the moment we add only one method to request about determining a locale which can be easily converted to a standalone function. Change-Id: I55de36d5ebc35ae868c9048dd4972df1634e2ee4 Blueprint: keystone-pecan --- core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core.py b/core.py index 11bd21d7..dc0788bc 100644 --- a/core.py +++ b/core.py @@ -193,7 +193,7 @@ class RequestBodySizeLimiter(wsgi.Middleware): def __init__(self, *args, **kwargs): super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) - @webob.dec.wsgify(RequestClass=wsgi.Request) + @webob.dec.wsgify() def __call__(self, req): if req.content_length > CONF.max_request_body_size: From 972b34ed8f4ac8184bc774be90d30e7db192e6ca Mon Sep 17 00:00:00 2001 From: Steve Martinelli Date: Thu, 13 Feb 2014 10:04:53 -0600 Subject: [PATCH 111/121] Use WebOb directly in ec2_token middleware When https://review.openstack.org/#/c/71494/ was introduced, it forgot to change ec2_token as well. This caused errors when building docs. Change-Id: I051519c815ed5ae60ec07203073131db1e031e10 --- ec2_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ec2_token.py b/ec2_token.py index 36589084..db5010c6 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -43,7 +43,7 @@ CONF.register_opts(keystone_ec2_opts) class EC2Token(wsgi.Middleware): """Authenticate an EC2 request with keystone and convert to token.""" - @webob.dec.wsgify(RequestClass=wsgi.Request) + @webob.dec.wsgify() def __call__(self, req): # Read request signature and access id. try: From 43b07944cc092fe67e91572443be9638c46ce408 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Sat, 1 Mar 2014 07:40:38 -0600 Subject: [PATCH 112/121] deprecate XML support in favor of JSON Change-Id: I365261b4666c4dbe21218a2bb421273b5bdcdbb8 Implements: bp deprecated-as-of-icehouse --- core.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core.py b/core.py index 6569ca1f..4fa50ac7 100644 --- a/core.py +++ b/core.py @@ -23,6 +23,7 @@ from keystone.common import wsgi from keystone import exception from keystone.openstack.common import jsonutils from keystone.openstack.common import log +from keystone.openstack.common import versionutils CONF = config.CONF LOG = log.getLogger(__name__) @@ -143,6 +144,14 @@ class JsonBodyMiddleware(wsgi.Middleware): class XmlBodyMiddleware(wsgi.Middleware): """De/serializes XML to/from JSON.""" + @versionutils.deprecated( + what='keystone.middleware.core.XmlBodyMiddleware', + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='support for "application/json" only', + remove_in=+2) + def __init__(self, *args, **kwargs): + super(XmlBodyMiddleware, self).__init__(*args, **kwargs) + def process_request(self, request): """Transform the request from XML to JSON.""" incoming_xml = 'application/xml' in str(request.content_type) From a9e420f79944fed2759321b9e42eeed2b0e939a9 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Tue, 21 Jan 2014 15:06:48 -0500 Subject: [PATCH 113/121] Token Revocation Extension Base API for reporting revocation events. The KVS Backend uses the Dogpile backed KVS stores. Modifies the places that were directly deleting tokens to also generate revocation events. Where possible the revocations are triggered by listening to the notifications. Some places, the callers have been modified instead. This is usually due to the need to iterate through a collection, such as users in a group. Adds a config file option to disable the existing mechanisms that support revoking a token by that token's id: revoke_by_id. This flag is necessary to test that the revocation mechanism is working as defined, but will also be part of the phased removal of the older mechanisms. TokenRevoke tests have been extended to test both with and without revoke-by-id enabled. Note: The links aren't populated in the list_events response. SQL Backend for Revocation Events Initializes the SQL Database for the revocation backend. This patch refactors the sql migration call from the CLI so that the test framework can use it as well. The sql backend for revcations is exercized by test_notifications and must be properly initialized. Revoke By Search Tree Co-Authored-By: Yuriy Taraday (Yoriksar) create a set of nested maps for the events. Look up revocation by traversing down the tree. Blueprint: revocation-events Change-Id: If76c8cd5d01a5b991c58a4d1a9d534b2a3da875a --- core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core.py b/core.py index 4fa50ac7..dbd9933a 100644 --- a/core.py +++ b/core.py @@ -231,6 +231,14 @@ class AuthContextMiddleware(wsgi.Middleware): try: token_ref = self.token_api.get_token(token_id) + # TODO(ayoung): These two functions return the token in different + # formats instead of two calls, only make one. However, the call + # to get_token hits the caching layer, and does not validate the + # token. In the future, this should be reduced to one call. + if not CONF.token.revoke_by_id: + self.token_api.token_provider_api.validate_token( + context['token_id']) + # TODO(gyee): validate_token_bind should really be its own # middleware wsgi.validate_token_bind(context, token_ref) From 71c7b8c284fcb938b1ef32b12811b49b57d4d0ec Mon Sep 17 00:00:00 2001 From: wanghong Date: Thu, 26 Dec 2013 11:53:40 +0800 Subject: [PATCH 114/121] V3 xml responses should use v3 namespace. Currently, all v3 xml responses use v2 namespace, But v2 and v3 are incompatible. This change will fix it. Change-Id: I5818bb0a268ae0e48263d5ea5795fdebeae9c388 Closes-Bug: #1195813 --- core.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/core.py b/core.py index dbd9933a..997ae985 100644 --- a/core.py +++ b/core.py @@ -151,6 +151,7 @@ class XmlBodyMiddleware(wsgi.Middleware): remove_in=+2) def __init__(self, *args, **kwargs): super(XmlBodyMiddleware, self).__init__(*args, **kwargs) + self.xmlns = None def process_request(self, request): """Transform the request from XML to JSON.""" @@ -173,13 +174,29 @@ class XmlBodyMiddleware(wsgi.Middleware): response.content_type = 'application/xml' try: body_obj = jsonutils.loads(response.body) - response.body = serializer.to_xml(body_obj) + response.body = serializer.to_xml(body_obj, xmlns=self.xmlns) except Exception: LOG.exception('Serializer failed') raise exception.Error(message=response.body) return response +class XmlBodyMiddlewareV2(XmlBodyMiddleware): + """De/serializes XML to/from JSON for v2.0 API.""" + + def __init__(self, *args, **kwargs): + super(XmlBodyMiddlewareV2, self).__init__(*args, **kwargs) + self.xmlns = 'http://docs.openstack.org/identity/api/v2.0' + + +class XmlBodyMiddlewareV3(XmlBodyMiddleware): + """De/serializes XML to/from JSON for v3 API.""" + + def __init__(self, *args, **kwargs): + super(XmlBodyMiddlewareV3, self).__init__(*args, **kwargs) + self.xmlns = 'http://docs.openstack.org/identity/api/v3' + + class NormalizingFilter(wsgi.Middleware): """Middleware filter to handle URL normalization.""" From 8c7ea33ba35efa4f373da154d96c52705ca52353 Mon Sep 17 00:00:00 2001 From: Daniel Gollub Date: Wed, 26 Feb 2014 06:56:13 +0100 Subject: [PATCH 115/121] Replace httplib.HTTPSConnection in ec2_token httplib.HTTPSConnection is known to not verify SSL certificates in Python 2.x. Implementation got adapted to make use of the requests module instead. SSL Verification is from now on enabled by default. Can be disabled via an additional introduced configuration option: `keystone_ec2_insecure=True` SecurityImpact DocImpact Partial-Bug: 1188189 Change-Id: Ie6a6620685995add56f38dc34c9a0a733558146a --- ec2_token.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/ec2_token.py b/ec2_token.py index db5010c6..9d40ef84 100644 --- a/ec2_token.py +++ b/ec2_token.py @@ -20,9 +20,8 @@ Starting point for routing EC2 requests. """ -from eventlet.green import httplib from oslo.config import cfg -from six.moves import urllib +import requests import webob.dec import webob.exc @@ -34,6 +33,16 @@ keystone_ec2_opts = [ cfg.StrOpt('keystone_ec2_url', default='http://localhost:5000/v2.0/ec2tokens', help='URL to get token from ec2 request.'), + cfg.StrOpt('keystone_ec2_keyfile', help='Required if EC2 server requires ' + 'client certificate.'), + cfg.StrOpt('keystone_ec2_certfile', help='Client certificate key ' + 'filename. Required if EC2 server requires client ' + 'certificate.'), + cfg.StrOpt('keystone_ec2_cafile', help='A PEM encoded certificate ' + 'authority to use when verifying HTTPS connections. Defaults ' + 'to the system CAs.'), + cfg.BoolOpt('keystone_ec2_insecure', default=False, help='Disable SSL ' + 'certificate verification.'), ] CONF = config.CONF @@ -71,23 +80,26 @@ class EC2Token(wsgi.Middleware): creds_json = jsonutils.dumps(creds) headers = {'Content-Type': 'application/json'} - # Disable 'has no x member' pylint error - # for httplib and urlparse - # pylint: disable-msg=E1101 - o = urllib.parse.urlparse(CONF.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() + verify = True + if CONF.keystone_ec2_insecure: + verify = False + elif CONF.keystone_ec2_cafile: + verify = CONF.keystone_ec2_cafile + + cert = None + if CONF.keystone_ec2_certfile and CONF.keystone_ec2_keyfile: + cert = (CONF.keystone_ec2_certfile, CONF.keystone_ec2_keyfile) + elif CONF.keystone_ec2_certfile: + cert = CONF.keystone_ec2_certfile + + response = requests.post(CONF.keystone_ec2_url, data=creds_json, + headers=headers, verify=verify, cert=cert) # NOTE(vish): We could save a call to keystone by # having keystone return token, tenant, # user, and roles from this call. - result = jsonutils.loads(response) + result = response.json() try: token_id = result['access']['token']['id'] except (AttributeError, KeyError): From e03661d94c1c84916f792889a5bc68b74d8f054e Mon Sep 17 00:00:00 2001 From: Ilya Pekelny Date: Fri, 29 Nov 2013 16:46:08 +0200 Subject: [PATCH 116/121] Uses explicit imports for _ Previously `_` was monkeypatched in tests/core.py and bin/keystone-*. This meant that if a developer was not running the tests exactly as the documentation described they would not work. Even importing certain modules in a interactive Python interpreter would fail unless keystone.tests was imported first. Monkeypatching was removed and explicit import for `_` was added. Co-Authored-By: David Stanek Change-Id: I8b25b5b6d83fb873e25a8fab7686babf1d2261fa Closes-Bug: #1255518 --- core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core.py b/core.py index 997ae985..410cebdf 100644 --- a/core.py +++ b/core.py @@ -21,6 +21,7 @@ from keystone.common import serializer from keystone.common import utils from keystone.common import wsgi from keystone import exception +from keystone.openstack.common.gettextutils import _ from keystone.openstack.common import jsonutils from keystone.openstack.common import log from keystone.openstack.common import versionutils From 09f4f26aa575c750d87e594b298d92949644e553 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 5 Mar 2014 12:06:30 +1000 Subject: [PATCH 117/121] Change the default version discovery URLs The default discovery URLs for when the admin_endpoint and public_endpoint configuration values are unset is to point to the localhost. This is wrong in all but the most trivial cases. It also has the problem of not being able to distinguish for the public service whether it was accessed via the 'public' or 'private' endpoint, meaning that all clients that correctly do discovery will end up routing to the public URL. The most sensible default is to simply use the requested URL as the basis for pointing to the versioned endpoints as it at least assumes that the endpoint is accessible relative to the location used to arrive on the page. As mentioned in comments this is not a perfect solution. HOST_URL is the URL not including path (ie http://server:port) so we do not have access to the prefix automatically. Unfortunately the way keystone uses these endpoints I don't see a way of improving that without a more substantial redesign. This patch is ugly because our layers are so intertwined. It should be nicer with pecan. DocImpact: Changes the default values of admin_endpoint and public_endpoint and how they are used. In most situations now these values should be ignored in configuration. Change-Id: Ia6d9fbeb60ada661dc2052c9bd51db7a1dc8cd4b Closes-Bug: #1288009 --- core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core.py b/core.py index 410cebdf..7ca9be44 100644 --- a/core.py +++ b/core.py @@ -118,7 +118,7 @@ class JsonBodyMiddleware(wsgi.Middleware): if request.content_type not in ('application/json', ''): e = exception.ValidationError(attribute='application/json', target='Content-Type header') - return wsgi.render_exception(e) + return wsgi.render_exception(e, request=request) params_parsed = {} try: @@ -126,7 +126,7 @@ class JsonBodyMiddleware(wsgi.Middleware): except ValueError: e = exception.ValidationError(attribute='valid JSON', target='request body') - return wsgi.render_exception(e) + return wsgi.render_exception(e, request=request) finally: if not params_parsed: params_parsed = {} @@ -166,7 +166,7 @@ class XmlBodyMiddleware(wsgi.Middleware): LOG.exception('Serializer failed') e = exception.ValidationError(attribute='valid XML', target='request body') - return wsgi.render_exception(e) + return wsgi.render_exception(e, request=request) def process_response(self, request, response): """Transform the response from JSON to XML.""" From a8d758db461e3b6b9a7d2e45a49ece27204a9176 Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Thu, 27 Mar 2014 15:09:21 -0500 Subject: [PATCH 118/121] Safer noqa handling "# flake8: noqa" was used in several files. This causes the entire file to not be checked by flake8. This is unsafe, and "# noqa" should be used only on those lines that require it. E712 doesn't honor #noqa, so work around it by assigning True to a variable. Change-Id: I1ddd1c4f4230793f0560241e4559095cb4183d71 --- __init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/__init__.py b/__init__.py index d805f82d..efbaa7c9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,3 @@ -# flake8: noqa - # Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,4 +12,4 @@ # License for the specific language governing permissions and limitations # under the License. -from keystone.middleware.core import * +from keystone.middleware.core import * # noqa From 26fdb88ef2be0b9f7804e0543b93a3f55b162c5f Mon Sep 17 00:00:00 2001 From: David Stanek Date: Sun, 30 Mar 2014 12:58:59 +0000 Subject: [PATCH 119/121] Fixed the size limit tests in Python 3 - None cannot be compared to an integer in Python 3 - In Python 3 a request body must be bytes bp python3 Change-Id: If3944f84d41f54089a8d245848c3012d566385f1 --- core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core.py b/core.py index 7ca9be44..7773f3cc 100644 --- a/core.py +++ b/core.py @@ -220,13 +220,13 @@ class RequestBodySizeLimiter(wsgi.Middleware): @webob.dec.wsgify() def __call__(self, req): - - if req.content_length > CONF.max_request_body_size: + if req.content_length is None: + if req.is_body_readable: + limiter = utils.LimitingReader(req.body_file, + CONF.max_request_body_size) + req.body_file = limiter + elif req.content_length > CONF.max_request_body_size: raise exception.RequestTooLarge() - if req.content_length is None and req.is_body_readable: - limiter = utils.LimitingReader(req.body_file, - CONF.max_request_body_size) - req.body_file = limiter return self.application From 91b27cc213f282b5f05908875ecdc35651fcb5d8 Mon Sep 17 00:00:00 2001 From: Roman Bodnarchuk Date: Thu, 8 May 2014 06:48:05 -0400 Subject: [PATCH 120/121] Fix 500 error if request body is not JSON object In `JsonBodyMiddleware` we expect POST body to be a JSON dictionary, and failed with 500 error if body was a valid JSON, but not a dictionary. Additional check was added along with a test for described case. Change-Id: I08ae3c8fa4eb53b67604d8b8791ca19d9c1682e6 Closes-Bug: #1316657 --- core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core.py b/core.py index 7773f3cc..4bc1927a 100644 --- a/core.py +++ b/core.py @@ -131,6 +131,11 @@ class JsonBodyMiddleware(wsgi.Middleware): if not params_parsed: params_parsed = {} + if not isinstance(params_parsed, dict): + e = exception.ValidationError(attribute='valid JSON object', + target='request body') + return wsgi.render_exception(e, request=request) + params = {} for k, v in six.iteritems(params_parsed): if k in ('self', 'context'): From 3323483a77a1b489e007591306c76ce4edd4205c Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Thu, 19 Jun 2014 16:22:15 -0700 Subject: [PATCH 121/121] Move ec2_token to new location --- __init__.py | 15 - core.py | 287 ------------------ .../ec2_token.py | 0 s3_token.py | 56 ---- 4 files changed, 358 deletions(-) delete mode 100644 __init__.py delete mode 100644 core.py rename ec2_token.py => keystonemiddleware/ec2_token.py (100%) delete mode 100644 s3_token.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index efbaa7c9..00000000 --- a/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# 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. - -from keystone.middleware.core import * # noqa diff --git a/core.py b/core.py deleted file mode 100644 index 4bc1927a..00000000 --- a/core.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# -# 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 six -import webob.dec - -from keystone.common import authorization -from keystone.common import config -from keystone.common import serializer -from keystone.common import utils -from keystone.common import wsgi -from keystone import exception -from keystone.openstack.common.gettextutils import _ -from keystone.openstack.common import jsonutils -from keystone.openstack.common import log -from keystone.openstack.common import versionutils - -CONF = config.CONF -LOG = log.getLogger(__name__) - - -# Header used to transmit the auth token -AUTH_TOKEN_HEADER = 'X-Auth-Token' - - -# Header used to transmit the subject token -SUBJECT_TOKEN_HEADER = 'X-Subject-Token' - - -# Environment variable used to pass the request context -CONTEXT_ENV = wsgi.CONTEXT_ENV - - -# Environment variable used to pass the request params -PARAMS_ENV = wsgi.PARAMS_ENV - - -class TokenAuthMiddleware(wsgi.Middleware): - def process_request(self, request): - token = request.headers.get(AUTH_TOKEN_HEADER) - context = request.environ.get(CONTEXT_ENV, {}) - context['token_id'] = token - if SUBJECT_TOKEN_HEADER in request.headers: - context['subject_token_id'] = ( - request.headers.get(SUBJECT_TOKEN_HEADER)) - request.environ[CONTEXT_ENV] = context - - -class AdminTokenAuthMiddleware(wsgi.Middleware): - """A trivial filter that checks for a pre-defined admin token. - - Sets 'is_admin' to true in the context, expected to be checked by - methods that are admin-only. - - """ - - def process_request(self, request): - token = request.headers.get(AUTH_TOKEN_HEADER) - context = request.environ.get(CONTEXT_ENV, {}) - context['is_admin'] = (token == CONF.admin_token) - request.environ[CONTEXT_ENV] = context - - -class PostParamsMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as POST parameters. - - Filters out the parameters `self`, `context` and anything beginning with - an underscore. - - """ - - def process_request(self, request): - params_parsed = request.params - params = {} - for k, v in six.iteritems(params_parsed): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v - - request.environ[PARAMS_ENV] = params - - -class JsonBodyMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as serialized JSON. - - Accepting arguments as JSON is useful for accepting data that may be more - complex than simple primitives. - - In this case we accept it as urlencoded data under the key 'json' as in - json= but this could be extended to accept raw JSON - in the POST body. - - Filters out the parameters `self`, `context` and anything beginning with - an underscore. - - """ - def process_request(self, request): - # Abort early if we don't have any work to do - params_json = request.body - if not params_json: - return - - # Reject unrecognized content types. Empty string indicates - # the client did not explicitly set the header - if request.content_type not in ('application/json', ''): - e = exception.ValidationError(attribute='application/json', - target='Content-Type header') - return wsgi.render_exception(e, request=request) - - params_parsed = {} - try: - params_parsed = jsonutils.loads(params_json) - except ValueError: - e = exception.ValidationError(attribute='valid JSON', - target='request body') - return wsgi.render_exception(e, request=request) - finally: - if not params_parsed: - params_parsed = {} - - if not isinstance(params_parsed, dict): - e = exception.ValidationError(attribute='valid JSON object', - target='request body') - return wsgi.render_exception(e, request=request) - - params = {} - for k, v in six.iteritems(params_parsed): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v - - request.environ[PARAMS_ENV] = params - - -class XmlBodyMiddleware(wsgi.Middleware): - """De/serializes XML to/from JSON.""" - - @versionutils.deprecated( - what='keystone.middleware.core.XmlBodyMiddleware', - as_of=versionutils.deprecated.ICEHOUSE, - in_favor_of='support for "application/json" only', - remove_in=+2) - def __init__(self, *args, **kwargs): - super(XmlBodyMiddleware, self).__init__(*args, **kwargs) - self.xmlns = None - - def process_request(self, request): - """Transform the request from XML to JSON.""" - incoming_xml = 'application/xml' in str(request.content_type) - if incoming_xml and request.body: - request.content_type = 'application/json' - try: - request.body = jsonutils.dumps( - serializer.from_xml(request.body)) - except Exception: - LOG.exception('Serializer failed') - e = exception.ValidationError(attribute='valid XML', - target='request body') - return wsgi.render_exception(e, request=request) - - def process_response(self, request, response): - """Transform the response from JSON to XML.""" - outgoing_xml = 'application/xml' in str(request.accept) - if outgoing_xml and response.body: - response.content_type = 'application/xml' - try: - body_obj = jsonutils.loads(response.body) - response.body = serializer.to_xml(body_obj, xmlns=self.xmlns) - except Exception: - LOG.exception('Serializer failed') - raise exception.Error(message=response.body) - return response - - -class XmlBodyMiddlewareV2(XmlBodyMiddleware): - """De/serializes XML to/from JSON for v2.0 API.""" - - def __init__(self, *args, **kwargs): - super(XmlBodyMiddlewareV2, self).__init__(*args, **kwargs) - self.xmlns = 'http://docs.openstack.org/identity/api/v2.0' - - -class XmlBodyMiddlewareV3(XmlBodyMiddleware): - """De/serializes XML to/from JSON for v3 API.""" - - def __init__(self, *args, **kwargs): - super(XmlBodyMiddlewareV3, self).__init__(*args, **kwargs) - self.xmlns = 'http://docs.openstack.org/identity/api/v3' - - -class NormalizingFilter(wsgi.Middleware): - """Middleware filter to handle URL normalization.""" - - def process_request(self, request): - """Normalizes URLs.""" - # Removes a trailing slash from the given path, if any. - if (len(request.environ['PATH_INFO']) > 1 and - request.environ['PATH_INFO'][-1] == '/'): - request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1] - # Rewrites path to root if no path is given. - elif not request.environ['PATH_INFO']: - request.environ['PATH_INFO'] = '/' - - -class RequestBodySizeLimiter(wsgi.Middleware): - """Limit the size of an incoming request.""" - - def __init__(self, *args, **kwargs): - super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) - - @webob.dec.wsgify() - def __call__(self, req): - if req.content_length is None: - if req.is_body_readable: - limiter = utils.LimitingReader(req.body_file, - CONF.max_request_body_size) - req.body_file = limiter - elif req.content_length > CONF.max_request_body_size: - raise exception.RequestTooLarge() - 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(ayoung): These two functions return the token in different - # formats instead of two calls, only make one. However, the call - # to get_token hits the caching layer, and does not validate the - # token. In the future, this should be reduced to one call. - if not CONF.token.revoke_by_id: - self.token_api.token_provider_api.validate_token( - context['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 diff --git a/ec2_token.py b/keystonemiddleware/ec2_token.py similarity index 100% rename from ec2_token.py rename to keystonemiddleware/ec2_token.py diff --git a/s3_token.py b/s3_token.py deleted file mode 100644 index 97231e8f..00000000 --- a/s3_token.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011,2012 Akira YOSHIYAMA -# 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. - -# This source code is based ./auth_token.py and ./ec2_token.py. -# See them for their copyright. - -""" -S3 TOKEN MIDDLEWARE - -The S3 Token middleware is deprecated as of IceHouse. It's been moved into -python-keystoneclient, `keystoneclient.middleware.s3_token`. - -This WSGI component: - -* Get a request from the swift3 middleware with an S3 Authorization - access key. -* Validate s3 token in Keystone. -* Transform the account name to AUTH_%(tenant_name). - -""" - -from keystoneclient.middleware import s3_token - -from keystone.openstack.common import versionutils - - -PROTOCOL_NAME = s3_token.PROTOCOL_NAME -split_path = s3_token.split_path -ServiceError = s3_token.ServiceError -filter_factory = s3_token.filter_factory - - -class S3Token(s3_token.S3Token): - - @versionutils.deprecated( - versionutils.deprecated.ICEHOUSE, - in_favor_of='keystoneclient.middleware.s3_token', - remove_in=+1, - what='keystone.middleware.s3_token') - def __init__(self, app, conf): - super(S3Token, self).__init__(app, conf)