Merge lp:~khussein/swift/authn into lp:~hudson-openstack/swift/trunk
- authn
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
Authentication in OpenStack
(Undefined)
|
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.
Commit message
Description of the change
Implemented the proposed protocol to handle authentication in OpenStack services.
gholt (gholt) wrote : | # |
We meant for this to be a work-in-progress; sorry.
gholt (gholt) wrote : | # |
Oh, and these weren't the droids you were looking for anyhow. Move along; move along.
Khaled Hussein (khussein) wrote : | # |
The proxy-server config now would be [auth1 devauth papiauth]
- 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
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 | + |
> 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) content_ length = sum(map(len, response.app_iter))
> return [resp.read()]
> response.
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' , swift.auth. server: app_factory' , auth.basicauth: app_factory' ,
> + #'auth=
> + 'auth=swift.
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.