Merge lp:~khussein/swift/authn into lp:~hudson-openstack/swift/trunk

Proposed by Khaled Hussein
Status: Superseded
Proposed branch: lp:~khussein/swift/authn
Merge into: lp:~hudson-openstack/swift/trunk
Diff against target: 418 lines (+256/-106)
4 files modified
setup.py (+3/-1)
swift/common/middleware/auth1.py (+147/-0)
swift/common/middleware/devauth.py (+43/-105)
swift/common/middleware/papiauth.py (+63/-0)
To merge this branch: bzr merge lp:~khussein/swift/authn
Reviewer Review Type Date Requested Status
Mike Barton (community) Needs Fixing
Review via email: mp+43057@code.launchpad.net

This proposal has been superseded by a proposal from 2011-01-07.

Description of the change

Implemented the proposed protocol to handle authentication in OpenStack services.

To post a comment you must log in.
Revision history for this message
Mike Barton (redbo) wrote :

> self.response_status = status

self.response_status is storing per-request info in a shared object. That could be wrong under concurrency.

> ''.join(response_itr)
> return [resp.read()]
> response.content_length = sum(map(len, response.app_iter))

Storing many GB of data in RAM is a bad idea.

> usersConfig.readfp(open('/etc/openstack/users.ini'))

You shouldn't re-read and parse the basic auth credentials file on every request. The local imports there are kind of ugly too.

> def validateCreds(self, username, password):

only major pep8 violation I see is those camel caps.

> - 'auth=swift.auth.server:app_factory',
> + #'auth=swift.auth.server:app_factory',
> + 'auth=swift.auth.basicauth:app_factory',

Was leaving it this way a mistake? The basic auth isn't super useful right now, and removing the devauth filter completely breaks our dev and testing environments.

review: Needs Fixing
Revision history for this message
gholt (gholt) wrote :

We meant for this to be a work-in-progress; sorry.

Revision history for this message
gholt (gholt) wrote :

Oh, and these weren't the droids you were looking for anyhow. Move along; move along.

Revision history for this message
Khaled Hussein (khussein) wrote :

The proxy-server config now would be [auth1 devauth papiauth]

lp:~khussein/swift/authn updated
152. By Khaled Hussein

Fixed unit tests

153. By Khaled Hussein

Merged trunk

154. By Khaled Hussein

setup.py config changes

155. By Khaled Hussein

fixed functional tests

156. By Khaled Hussein

fixing conflicts

157. By Khaled Hussein

Refactored and Added unit tests

158. By Khaled Hussein

PEP8 Stuff :)

159. By Khaled Hussein

saio documentation changes

160. By Khaled Hussein

documentation change

161. By Khaled Hussein

merge trunk

Unmerged revisions

161. By Khaled Hussein

merge trunk

160. By Khaled Hussein

documentation change

159. By Khaled Hussein

saio documentation changes

158. By Khaled Hussein

PEP8 Stuff :)

157. By Khaled Hussein

Refactored and Added unit tests

156. By Khaled Hussein

fixing conflicts

155. By Khaled Hussein

fixed functional tests

154. By Khaled Hussein

setup.py config changes

153. By Khaled Hussein

Merged trunk

152. By Khaled Hussein

Fixed unit tests

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup.py'
2--- setup.py 2011-01-04 23:34:43 +0000
3+++ setup.py 2011-01-07 16:38:27 +0000
4@@ -89,7 +89,9 @@
5 'auth=swift.auth.server:app_factory',
6 ],
7 'paste.filter_factory': [
8- 'auth=swift.common.middleware.auth:filter_factory',
9+ 'auth=swift.common.middleware.auth1:filter_factory',
10+ 'devauth=swift.common.middleware.devauth:filter_factory',
11+ 'papiauth=swift.common.middleware.papiauth:filter_factory',
12 'healthcheck=swift.common.middleware.healthcheck:filter_factory',
13 'memcache=swift.common.middleware.memcache:filter_factory',
14 'ratelimit=swift.common.middleware.ratelimit:filter_factory',
15
16=== added file 'swift/common/middleware/auth1.py'
17--- swift/common/middleware/auth1.py 1970-01-01 00:00:00 +0000
18+++ swift/common/middleware/auth1.py 2011-01-07 16:38:27 +0000
19@@ -0,0 +1,147 @@
20+# Copyright (c) 2010 OpenStack, LLC.
21+#
22+# Licensed under the Apache License, Version 2.0 (the "License");
23+# you may not use this file except in compliance with the License.
24+# You may obtain a copy of the License at
25+#
26+# http://www.apache.org/licenses/LICENSE-2.0
27+#
28+# Unless required by applicable law or agreed to in writing, software
29+# distributed under the License is distributed on an "AS IS" BASIS,
30+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
31+# implied.
32+# See the License for the specific language governing permissions and
33+# limitations under the License.
34+
35+from time import time
36+
37+from eventlet.timeout import Timeout
38+from webob.exc import HTTPUnauthorized, HTTPNotFound
39+
40+from swift.common.bufferedhttp import http_connect_raw as http_connect
41+from swift.common.utils import cache_from_env, split_path, TRUE_VALUES
42+
43+class Auth1(object):
44+ """Auth Middleware that uses the dev auth server."""
45+
46+ def __init__(self, app, conf):
47+ self.app = app
48+ self.conf = conf
49+ self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
50+ if self.reseller_prefix and self.reseller_prefix[-1] != '_':
51+ self.reseller_prefix += '_'
52+ self.auth_host = conf.get('ip', '127.0.0.1')
53+ self.auth_port = int(conf.get('port', 11000))
54+ self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES
55+ self.timeout = int(conf.get('node_timeout', 10))
56+ self.delegated = int(conf.get('delegated', 0))
57+
58+ def __call__(self, env, start_response):
59+ """
60+ Accepts a standard WSGI application call and authenticates the request.
61+ For an authenticated request, REMOTE_USER will be set to a comma
62+ separated list of the user's groups. It'll also set X-Authorization
63+ header to 'Proxy [Username]'. If it is running in a delegated mode, it
64+ sets the X-Identity-Status header to 'Confirmed' if the token is valid,
65+ or 'Indeterminate' if the token doesn't exist.
66+
67+ With a non-empty reseller prefix, acts as the definitive auth service
68+ for just tokens and accounts that begin with that prefix, but will deny
69+ requests outside this prefix if no other auth middleware overrides it.
70+
71+ With an empty reseller prefix, acts as the definitive auth service only
72+ for tokens that validate to a non-empty set of groups. For all other
73+ requests, acts as the fallback auth service when no other auth
74+ middleware overrides it.
75+ """
76+
77+ def custom_start_response(status, headers, exc_info=None):
78+ if self.delegated:
79+ headers.append(('WWW-Authenticate', "Basic realm='API Realm'"))
80+ return start_response(status, headers, exc_info)
81+
82+ token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
83+ if token and token.startswith(self.reseller_prefix):
84+ # Note: Empty reseller_prefix will match all tokens.
85+ # Attempt to auth my token with my auth server
86+ user, groups = \
87+ self.get_groups(token, memcache_client=cache_from_env(env))
88+ if groups:
89+ env['SWIFT_GROUPS'] = groups
90+ env['HTTP_X_AUTHORIZATION'] = "Proxy " + user
91+ if self.delegated:
92+ env['HTTP_X_IDENTITY_STATUS'] = "Confirmed"
93+ # We know the proxy logs the token, so we augment it just a bit
94+ # to also log the authenticated user.
95+ env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token)
96+ else:
97+ # Unauthorized token
98+ return HTTPUnauthorized()(env, custom_start_response)
99+ else:
100+ env['HTTP_X_AUTHORIZATION'] = "Proxy"
101+ if not token and self.delegated:
102+ env['HTTP_X_IDENTITY_STATUS'] = "Indeterminate"
103+
104+ if self.reseller_prefix:
105+ # With a non-empty reseller_prefix, I would like to be called
106+ # back for anonymous access to accounts I know I'm the
107+ # definitive auth for.
108+ try:
109+ version, rest = split_path(env.get('PATH_INFO', ''),
110+ 1, 2, True)
111+ except ValueError:
112+ return HTTPNotFound()(env, custom_start_response)
113+
114+ env['HTTP_AUTHORIZATION'] = "Basic dTpw"
115+ return self.app(env, custom_start_response)
116+
117+ def get_groups(self, token, memcache_client=None):
118+ """
119+ Get user and groups info for the given token.
120+
121+ If memcache_client is set, token credentials will be cached
122+ appropriately.
123+
124+ With a cache miss, or no memcache_client, the configurated external
125+ authentication server will be queried for the group information.
126+
127+ :param token: Token to validate and return a group string for.
128+ :param memcache_client: Memcached client to use for caching token
129+ credentials; None if no caching is desired.
130+ :returns: None if the token is invalid or a string for the username and
131+ a string containing a comma separated list of groups the
132+ authenticated user is a member of. The first group in the
133+ list is also considered a unique identifier for that user.
134+ """
135+ groups = None
136+ key = '%s/token/%s' % (self.reseller_prefix, token)
137+ cached_auth_data = memcache_client and memcache_client.get(key)
138+ if cached_auth_data:
139+ start, expiration, groups = cached_auth_data
140+ if time() - start > expiration:
141+ groups = None
142+ if not groups:
143+ with Timeout(self.timeout):
144+ conn = http_connect(self.auth_host, self.auth_port, 'GET',
145+ '/token/%s' % token, ssl=self.ssl)
146+ resp = conn.getresponse()
147+ resp.read()
148+ conn.close()
149+ if resp.status // 100 != 2:
150+ return None
151+ expiration = float(resp.getheader('x-auth-ttl'))
152+ groups = resp.getheader('x-auth-groups')
153+ if memcache_client:
154+ memcache_client.set(key, (time(), expiration, groups),
155+ timeout=expiration)
156+ user = groups and groups.split(',', 1)[0] or ''
157+ return user, groups
158+
159+def filter_factory(global_conf, **local_conf):
160+ """Returns a WSGI filter app for use with paste.deploy."""
161+ conf = global_conf.copy()
162+ conf.update(local_conf)
163+
164+ def auth_filter(app):
165+ return Auth1(app, conf)
166+ return auth_filter
167
168=== renamed file 'swift/common/middleware/auth.py' => 'swift/common/middleware/devauth.py'
169--- swift/common/middleware/auth.py 2011-01-04 23:34:43 +0000
170+++ swift/common/middleware/devauth.py 2011-01-07 16:38:27 +0000
171@@ -13,14 +13,10 @@
172 # See the License for the specific language governing permissions and
173 # limitations under the License.
174
175-from time import time
176-
177-from eventlet.timeout import Timeout
178 from webob.exc import HTTPForbidden, HTTPUnauthorized, HTTPNotFound
179
180-from swift.common.bufferedhttp import http_connect_raw as http_connect
181 from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
182-from swift.common.utils import cache_from_env, split_path, TRUE_VALUES
183+from swift.common.utils import split_path, TRUE_VALUES
184
185
186 class DevAuth(object):
187@@ -41,110 +37,49 @@
188 """
189 Accepts a standard WSGI application call, authenticating the request
190 and installing callback hooks for authorization and ACL header
191- validation. For an authenticated request, REMOTE_USER will be set to a
192- comma separated list of the user's groups.
193-
194- With a non-empty reseller prefix, acts as the definitive auth service
195- for just tokens and accounts that begin with that prefix, but will deny
196- requests outside this prefix if no other auth middleware overrides it.
197-
198- With an empty reseller prefix, acts as the definitive auth service only
199- for tokens that validate to a non-empty set of groups. For all other
200- requests, acts as the fallback auth service when no other auth
201- middleware overrides it.
202+ validation.
203 """
204- token = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN'))
205- if token and token.startswith(self.reseller_prefix):
206- # Note: Empty reseller_prefix will match all tokens.
207- # Attempt to auth my token with my auth server
208- groups = \
209- self.get_groups(token, memcache_client=cache_from_env(env))
210- if groups:
211- env['REMOTE_USER'] = groups
212- user = groups and groups.split(',', 1)[0] or ''
213- # We know the proxy logs the token, so we augment it just a bit
214- # to also log the authenticated user.
215- env['HTTP_X_AUTH_TOKEN'] = '%s,%s' % (user, token)
216+ groups = None
217+ if 'SWIFT_GROUPS' in env:
218+ groups = env['SWIFT_GROUPS']
219+ env['REMOTE_USER'] = groups
220+ self.authorize = self.dev_authorize
221+ env['swift.clean_acl'] = clean_acl
222+ elif 'swift.authorize' not in env:
223+ self.authorize = self.empty_authorize
224+
225+ if self.reseller_prefix:
226+ # With a non-empty reseller_prefix, I would like to be called
227+ # back for anonymous access to accounts I know I'm the
228+ # definitive auth for.
229+ try:
230+ version, rest = split_path(env.get('PATH_INFO', ''),
231+ 1, 2, True)
232+ except ValueError:
233+ return HTTPNotFound()(env, start_response)
234+ if rest and rest.startswith(self.reseller_prefix):
235+ # Handle anonymous access to accounts I'm the definitive
236+ # auth for.
237 env['swift.authorize'] = self.authorize
238 env['swift.clean_acl'] = clean_acl
239- else:
240- # Unauthorized token
241- if self.reseller_prefix:
242- # Because I know I'm the definitive auth for this token, I
243- # can deny it outright.
244- return HTTPUnauthorized()(env, start_response)
245- # Because I'm not certain if I'm the definitive auth for empty
246- # reseller_prefixed tokens, I won't overwrite swift.authorize.
247- elif 'swift.authorize' not in env:
248- env['swift.authorize'] = self.denied_response
249- else:
250- if self.reseller_prefix:
251- # With a non-empty reseller_prefix, I would like to be called
252- # back for anonymous access to accounts I know I'm the
253- # definitive auth for.
254- try:
255- version, rest = split_path(env.get('PATH_INFO', ''),
256- 1, 2, True)
257- except ValueError:
258- return HTTPNotFound()(env, start_response)
259- if rest and rest.startswith(self.reseller_prefix):
260- # Handle anonymous access to accounts I'm the definitive
261- # auth for.
262- env['swift.authorize'] = self.authorize
263- env['swift.clean_acl'] = clean_acl
264- # Not my token, not my account, I can't authorize this request,
265- # deny all is a good idea if not already set...
266- elif 'swift.authorize' not in env:
267- env['swift.authorize'] = self.denied_response
268- # Because I'm not certain if I'm the definitive auth for empty
269- # reseller_prefixed accounts, I won't overwrite swift.authorize.
270+ # Not my token, not my account, I can't authorize this request,
271+ # deny all is a good idea if not already set...
272 elif 'swift.authorize' not in env:
273- env['swift.authorize'] = self.authorize
274- env['swift.clean_acl'] = clean_acl
275+ env['swift.authorize'] = self.denied_response
276+ # Because I'm not certain if I'm the definitive auth for empty
277+ # reseller_prefixed accounts, I won't overwrite swift.authorize.
278+ elif 'swift.authorize' not in env:
279+ env['swift.authorize'] = self.authorize
280+ env['swift.clean_acl'] = clean_acl
281 return self.app(env, start_response)
282
283- def get_groups(self, token, memcache_client=None):
284- """
285- Get groups for the given token.
286-
287- If memcache_client is set, token credentials will be cached
288- appropriately.
289-
290- With a cache miss, or no memcache_client, the configurated external
291- authentication server will be queried for the group information.
292-
293- :param token: Token to validate and return a group string for.
294- :param memcache_client: Memcached client to use for caching token
295- credentials; None if no caching is desired.
296- :returns: None if the token is invalid or a string containing a comma
297- separated list of groups the authenticated user is a member
298- of. The first group in the list is also considered a unique
299- identifier for that user.
300- """
301- groups = None
302- key = '%s/token/%s' % (self.reseller_prefix, token)
303- cached_auth_data = memcache_client and memcache_client.get(key)
304- if cached_auth_data:
305- start, expiration, groups = cached_auth_data
306- if time() - start > expiration:
307- groups = None
308- if not groups:
309- with Timeout(self.timeout):
310- conn = http_connect(self.auth_host, self.auth_port, 'GET',
311- '/token/%s' % token, ssl=self.ssl)
312- resp = conn.getresponse()
313- resp.read()
314- conn.close()
315- if resp.status // 100 != 2:
316- return None
317- expiration = float(resp.getheader('x-auth-ttl'))
318- groups = resp.getheader('x-auth-groups')
319- if memcache_client:
320- memcache_client.set(key, (time(), expiration, groups),
321- timeout=expiration)
322- return groups
323-
324- def authorize(self, req):
325+ def empty_authorize(self, req):
326+ if 'x_identity_status' in req.headers and \
327+ req.headers['x_identity_status'] == 'Indeterminate':
328+ return self.denied_response(req)
329+ return None
330+
331+ def dev_authorize(self, req):
332 """
333 Returns None if the request is authorized to continue or a standard
334 WSGI response callable if not.
335@@ -177,10 +112,13 @@
336 Returns a standard WSGI response callable with the status of 403 or 401
337 depending on whether the REMOTE_USER is set or not.
338 """
339+ headers = [('www-authenticate', 'delegated')]
340 if req.remote_user:
341- return HTTPForbidden(request=req)
342+ resp = HTTPForbidden(headers=headers, request=req)
343 else:
344- return HTTPUnauthorized(request=req)
345+ resp = HTTPUnauthorized(headers=headers, request=req)
346+
347+ return resp
348
349
350 def filter_factory(global_conf, **local_conf):
351
352=== added file 'swift/common/middleware/papiauth.py'
353--- swift/common/middleware/papiauth.py 1970-01-01 00:00:00 +0000
354+++ swift/common/middleware/papiauth.py 2011-01-07 16:38:27 +0000
355@@ -0,0 +1,63 @@
356+# Copyright (c) 2010 OpenStack, LLC.
357+#
358+# Licensed under the Apache License, Version 2.0 (the "License");
359+# you may not use this file except in compliance with the License.
360+# You may obtain a copy of the License at
361+#
362+# http://www.apache.org/licenses/LICENSE-2.0
363+#
364+# Unless required by applicable law or agreed to in writing, software
365+# distributed under the License is distributed on an "AS IS" BASIS,
366+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
367+# implied.
368+# See the License for the specific language governing permissions and
369+# limitations under the License.
370+
371+from webob.exc import HTTPUseProxy, HTTPUnauthorized
372+
373+from swift.common.utils import TRUE_VALUES
374+
375+
376+class PAPIAuth(object):
377+ """Auth Middleware that uses the dev auth server."""
378+
379+ def __init__(self, app, conf):
380+ self.app = app
381+ self.conf = conf
382+ self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
383+ if self.reseller_prefix and self.reseller_prefix[-1] != '_':
384+ self.reseller_prefix += '_'
385+ self.auth_host = conf.get('ip', '127.0.0.1')
386+ self.auth_port = int(conf.get('port', 11000))
387+ self.auth_pass = conf.get('pass', 'dTpw')
388+ self.ssl = conf.get('ssl', 'false').lower() in TRUE_VALUES
389+ self.timeout = int(conf.get('node_timeout', 10))
390+
391+ def __call__(self, env, start_response):
392+ # Make sure that the user has been authenticated by the Auth Service
393+ if 'HTTP_X_AUTHORIZATION' not in env:
394+ proxy_location = 'http://' + self.auth_host + ':' + \
395+ str(self.auth_port) + '/'
396+ return HTTPUseProxy(location=proxy_location)(env, start_response)
397+
398+ # Authenticate the Auth component itself.
399+ headers = [('www-authenticate', 'Basic realm="swift"')]
400+ if 'HTTP_AUTHORIZATION' not in env:
401+ return HTTPUnauthorized(headers=headers)(env, start_response)
402+ else:
403+ auth_type, encoded_creds = env['HTTP_AUTHORIZATION'].split(None, 1)
404+ if encoded_creds != self.auth_pass:
405+ return HTTPUnauthorized(headers=headers)(env, start_response)
406+
407+ return self.app(env, start_response)
408+
409+
410+def filter_factory(global_conf, **local_conf):
411+ """Returns a WSGI filter app for use with paste.deploy."""
412+ conf = global_conf.copy()
413+ conf.update(local_conf)
414+
415+ def auth_filter(app):
416+ return PAPIAuth(app, conf)
417+ return auth_filter
418+