Merge lp:~gandelman-a/keystone/pkg_changes_03062011 into lp:~ubuntu-server-dev/keystone/essex
- pkg_changes_03062011
- Merge into essex
Proposed by
Adam Gandelman
Status: | Merged |
---|---|
Merge reported by: | Chuck Short |
Merged at revision: | not available |
Proposed branch: | lp:~gandelman-a/keystone/pkg_changes_03062011 |
Merge into: | lp:~ubuntu-server-dev/keystone/essex |
Diff against target: |
786 lines (+9/-748) 4 files modified
debian/changelog (+8/-0) debian/patches/keystone-auth.patch (+0/-746) debian/patches/series (+0/-1) debian/patches/sql_connection.patch (+1/-1) |
To merge this branch: | bzr merge lp:~gandelman-a/keystone/pkg_changes_03062011 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu Server Developers | Pending | ||
Review via email: mp+96283@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2012-03-03 01:38:46 +0000 |
3 | +++ debian/changelog 2012-03-07 02:13:21 +0000 |
4 | @@ -1,3 +1,11 @@ |
5 | +keystone (2012.1~e4-0ubuntu3) UNRELEASED; urgency=low |
6 | + |
7 | + * debian/patches/keystone-auth.patch: Drop, applied upstream at commit |
8 | + 29337e66. |
9 | + * debian/patches/sql_connection.patch: Refresh |
10 | + |
11 | + -- Adam Gandelman <adamg@canonical.com> Tue, 06 Mar 2012 16:13:37 -0800 |
12 | + |
13 | keystone (2012.1~e4-0ubuntu2) precise; urgency=low |
14 | |
15 | * debian/keystone.preinst: Create group before creating user (LP: #945299) |
16 | |
17 | === removed file 'debian/patches/keystone-auth.patch' |
18 | --- debian/patches/keystone-auth.patch 2012-03-02 14:54:39 +0000 |
19 | +++ debian/patches/keystone-auth.patch 1970-01-01 00:00:00 +0000 |
20 | @@ -1,746 +0,0 @@ |
21 | -Description: Backport improvements to auth_token middleware. |
22 | -Author: Chuck Short <zulcss@ubuntu.com> |
23 | - |
24 | -diff -Naurp keystone-2012.1.orig/keystone/middleware/auth_token.py keystone-2012.1/keystone/middleware/auth_token.py |
25 | ---- keystone-2012.1.orig/keystone/middleware/auth_token.py 2012-02-29 05:16:06.000000000 -0500 |
26 | -+++ keystone-2012.1/keystone/middleware/auth_token.py 2012-03-02 09:44:16.498651385 -0500 |
27 | -@@ -18,18 +18,17 @@ |
28 | - """ |
29 | - TOKEN-BASED AUTH MIDDLEWARE |
30 | - |
31 | --This WSGI component performs multiple jobs: |
32 | -+This WSGI component: |
33 | - |
34 | --* it verifies that incoming client requests have valid tokens by verifying |
35 | -+* Verifies that incoming client requests have valid tokens by validating |
36 | - tokens with the auth service. |
37 | --* it will reject unauthenticated requests UNLESS it is in 'delay_auth_decision' |
38 | -+* Rejects unauthenticated requests UNLESS it is in 'delay_auth_decision' |
39 | - mode, which means the final decision is delegated to the downstream WSGI |
40 | - component (usually the OpenStack service) |
41 | --* it will collect and forward identity information from a valid token |
42 | -- such as user name etc... |
43 | -- |
44 | --Refer to: http://wiki.openstack.org/openstack-authn |
45 | -+* Collects and forwards identity information based on a valid token |
46 | -+ such as user name, tenant, etc |
47 | - |
48 | -+Refer to: http://keystone.openstack.org/middleware_architecture.html |
49 | - |
50 | - HEADERS |
51 | - ------- |
52 | -@@ -41,17 +40,18 @@ Coming in from initial call from client |
53 | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
54 | - |
55 | - HTTP_X_AUTH_TOKEN |
56 | -- the client token being passed in |
57 | -+ The client token being passed in. |
58 | - |
59 | - HTTP_X_STORAGE_TOKEN |
60 | -- the client token being passed in (legacy Rackspace use) to support |
61 | -- cloud files |
62 | -+ The client token being passed in (legacy Rackspace use) to support |
63 | -+ swift/cloud files |
64 | - |
65 | - Used for communication between components |
66 | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
67 | - |
68 | --www-authenticate |
69 | -- only used if this component is being used remotely |
70 | -+WWW-Authenticate |
71 | -+ HTTP header returned to a user indicating which endpoint to use |
72 | -+ to retrieve a new token |
73 | - |
74 | - HTTP_AUTHORIZATION |
75 | - basic auth password used to validate the connection |
76 | -@@ -60,368 +60,370 @@ What we add to the request for use by th |
77 | - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
78 | - |
79 | - HTTP_X_AUTHORIZATION |
80 | -- the client identity being passed in |
81 | -+ The client identity being passed in |
82 | -+ |
83 | -+HTTP_X_IDENTITY_STATUS |
84 | -+ 'Confirmed' or 'Invalid' |
85 | -+ The underlying service will only see a value of 'Invalid' if the Middleware |
86 | -+ is configured to run in 'delay_auth_decision' mode |
87 | -+ |
88 | -+HTTP_X_TENANT_ID |
89 | -+ Identity service managed unique identifier, string |
90 | -+ |
91 | -+HTTP_X_TENANT_NAME |
92 | -+ Unique tenant identifier, string |
93 | -+ |
94 | -+HTTP_X_USER_ID |
95 | -+ Identity-service managed unique identifier, string |
96 | -+ |
97 | -+HTTP_X_USER_NAME |
98 | -+ Unique user identifier, string |
99 | -+ |
100 | -+HTTP_X_ROLES |
101 | -+ Comma delimited list of case-sensitive Roles |
102 | -+ |
103 | -+HTTP_X_TENANT |
104 | -+ *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME |
105 | -+ Keystone-assigned unique identifier, deprecated |
106 | -+ |
107 | -+HTTP_X_USER |
108 | -+ *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME |
109 | -+ Unique user name, string |
110 | -+ |
111 | -+HTTP_X_ROLE |
112 | -+ *Deprecated* in favor of HTTP_X_ROLES |
113 | -+ This is being renamed, and the new header contains the same data. |
114 | - |
115 | - """ |
116 | -+ |
117 | - import httplib |
118 | - import json |
119 | --import os |
120 | -+import logging |
121 | - |
122 | --import eventlet |
123 | --from eventlet import wsgi |
124 | --from paste import deploy |
125 | --from urlparse import urlparse |
126 | - import webob |
127 | - import webob.exc |
128 | --from webob.exc import HTTPUnauthorized |
129 | - |
130 | --from keystone.common.bufferedhttp import http_connect_raw as http_connect |
131 | - |
132 | --ADMIN_TENANTNAME = 'admin' |
133 | --PROTOCOL_NAME = 'Token Authentication' |
134 | -+logger = logging.getLogger('keystone.middleware.auth_token') |
135 | - |
136 | - |
137 | --class AuthProtocol(object): |
138 | -- """Auth Middleware that handles authenticating client calls""" |
139 | -+class InvalidUserToken(Exception): |
140 | -+ pass |
141 | -+ |
142 | -+ |
143 | -+class ServiceError(Exception): |
144 | -+ pass |
145 | - |
146 | -- def _init_protocol_common(self, app, conf): |
147 | -- """ Common initialization code""" |
148 | -- print 'Starting the %s component' % PROTOCOL_NAME |
149 | - |
150 | -+class AuthProtocol(object): |
151 | -+ """Auth Middleware that handles authenticating client calls.""" |
152 | -+ |
153 | -+ def __init__(self, app, conf): |
154 | -+ logger.info('Starting keystone auth_token middleware') |
155 | - self.conf = conf |
156 | - self.app = app |
157 | -- #if app is set, then we are in a WSGI pipeline and requests get passed |
158 | -- # on to app. If it is not set, this component should forward requests |
159 | -- |
160 | -- # where to find the OpenStack service (if not in local WSGI chain) |
161 | -- # these settings are only used if this component is acting as a proxy |
162 | -- # and the OpenSTack service is running remotely |
163 | -- self.service_protocol = conf.get('service_protocol', 'https') |
164 | -- self.service_host = conf.get('service_host') |
165 | -- self.service_port = int(conf.get('service_port')) |
166 | -- self.service_url = '%s://%s:%s' % (self.service_protocol, |
167 | -- self.service_host, |
168 | -- self.service_port) |
169 | -- # used to verify this component with the OpenStack service or PAPIAuth |
170 | -- self.service_pass = conf.get('service_pass') |
171 | - |
172 | - # delay_auth_decision means we still allow unauthenticated requests |
173 | - # through and we let the downstream service make the final decision |
174 | - self.delay_auth_decision = int(conf.get('delay_auth_decision', 0)) |
175 | - |
176 | -- def _init_protocol(self, conf): |
177 | -- """ Protocol specific initialization """ |
178 | -- |
179 | - # where to find the auth service (we use this to validate tokens) |
180 | - self.auth_host = conf.get('auth_host') |
181 | - self.auth_port = int(conf.get('auth_port')) |
182 | -- self.auth_protocol = conf.get('auth_protocol', 'https') |
183 | - |
184 | -- # where to tell clients to find the auth service (default to url |
185 | -- # constructed based on endpoint we have for the service to use) |
186 | -- self.auth_location = conf.get('auth_uri', |
187 | -- '%s://%s:%s' % (self.auth_protocol, |
188 | -- self.auth_host, |
189 | -- self.auth_port)) |
190 | -+ auth_protocol = conf.get('auth_protocol', 'https') |
191 | -+ if auth_protocol == 'http': |
192 | -+ self.http_client_class = httplib.HTTPConnection |
193 | -+ else: |
194 | -+ self.http_client_class = httplib.HTTPSConnection |
195 | -+ |
196 | -+ default_auth_uri = '%s://%s:%s' % (auth_protocol, |
197 | -+ self.auth_host, |
198 | -+ self.auth_port) |
199 | -+ self.auth_uri = conf.get('auth_uri', default_auth_uri) |
200 | - |
201 | - # Credentials used to verify this component with the Auth service since |
202 | - # validating tokens is a privileged call |
203 | - self.admin_token = conf.get('admin_token') |
204 | - self.admin_user = conf.get('admin_user') |
205 | - self.admin_password = conf.get('admin_password') |
206 | -+ self.admin_tenant_name = conf.get('admin_tenant_name', 'admin') |
207 | - |
208 | -- def __init__(self, app, conf): |
209 | -- """ Common initialization code """ |
210 | -+ def __call__(self, env, start_response): |
211 | -+ """Handle incoming request. |
212 | - |
213 | -- #TODO(ziad): maybe we refactor this into a superclass |
214 | -- self._init_protocol_common(app, conf) # Applies to all protocols |
215 | -- self._init_protocol(conf) # Specific to this protocol |
216 | -+ Authenticate send downstream on success. Reject request if |
217 | -+ we can't authenticate. |
218 | - |
219 | -- def __call__(self, env, start_response): |
220 | -- """ Handle incoming request. Authenticate. And send downstream. """ |
221 | -+ """ |
222 | -+ logger.debug('Authenticating user token') |
223 | -+ try: |
224 | -+ self._remove_auth_headers(env) |
225 | -+ user_token = self._get_user_token_from_header(env) |
226 | -+ token_info = self._validate_user_token(user_token) |
227 | -+ user_headers = self._build_user_headers(token_info) |
228 | -+ self._add_headers(env, user_headers) |
229 | -+ return self.app(env, start_response) |
230 | - |
231 | -- #Prep headers to forward request to local or remote downstream service |
232 | -- proxy_headers = env.copy() |
233 | -- for header in proxy_headers.iterkeys(): |
234 | -- if header.startswith('HTTP_'): |
235 | -- proxy_headers[header[5:]] = proxy_headers[header] |
236 | -- del proxy_headers[header] |
237 | -- |
238 | -- #Look for authentication claims |
239 | -- claims = self._get_claims(env) |
240 | -- if not claims: |
241 | -- #No claim(s) provided |
242 | -+ except InvalidUserToken: |
243 | - if self.delay_auth_decision: |
244 | -- #Configured to allow downstream service to make final decision. |
245 | -- #So mark status as Invalid and forward the request downstream |
246 | -- self._decorate_request('X_IDENTITY_STATUS', |
247 | -- 'Invalid', |
248 | -- env, |
249 | -- proxy_headers) |
250 | -+ logger.info('Invalid user token - deferring reject downstream') |
251 | -+ self._add_headers(env, {'X-Identity-Status': 'Invalid'}) |
252 | -+ return self.app(env, start_response) |
253 | - else: |
254 | -- #Respond to client as appropriate for this auth protocol |
255 | -+ logger.info('Invalid user token - rejecting request') |
256 | - return self._reject_request(env, start_response) |
257 | -+ |
258 | -+ except ServiceError, e: |
259 | -+ logger.critical('Unable to obtain admin token: %s' % e) |
260 | -+ resp = webob.exc.HTTPServiceUnavailable() |
261 | -+ return resp(env, start_response) |
262 | -+ |
263 | -+ def _remove_auth_headers(self, env): |
264 | -+ """Remove headers so a user can't fake authentication. |
265 | -+ |
266 | -+ :param env: wsgi request environment |
267 | -+ |
268 | -+ """ |
269 | -+ auth_headers = ( |
270 | -+ 'X-Identity-Status', |
271 | -+ 'X-Tenant-Id', |
272 | -+ 'X-Tenant-Name', |
273 | -+ 'X-User-Id', |
274 | -+ 'X-User-Name', |
275 | -+ 'X-Roles', |
276 | -+ # Deprecated |
277 | -+ 'X-User', |
278 | -+ 'X-Tenant', |
279 | -+ 'X-Role', |
280 | -+ ) |
281 | -+ logger.debug('Removing headers from request environment: %s' % |
282 | -+ ','.join(auth_headers)) |
283 | -+ self._remove_headers(env, auth_headers) |
284 | -+ |
285 | -+ def _get_user_token_from_header(self, env): |
286 | -+ """Get token id from request. |
287 | -+ |
288 | -+ :param env: wsgi request environment |
289 | -+ :return token id |
290 | -+ :raises InvalidUserToken if no token is provided in request |
291 | -+ |
292 | -+ """ |
293 | -+ token = self._get_header(env, 'X-Auth-Token', |
294 | -+ self._get_header(env, 'X-Storage-Token')) |
295 | -+ if token: |
296 | -+ return token |
297 | - else: |
298 | -- # this request is presenting claims. Let's validate them |
299 | -- valid = self._validate_claims(claims) |
300 | -- if not valid: |
301 | -- # Keystone rejected claim |
302 | -- if self.delay_auth_decision: |
303 | -- # Downstream service will receive call still and decide |
304 | -- self._decorate_request('X_IDENTITY_STATUS', |
305 | -- 'Invalid', |
306 | -- env, |
307 | -- proxy_headers) |
308 | -- else: |
309 | -- #Respond to client as appropriate for this auth protocol |
310 | -- return self._reject_claims(env, start_response) |
311 | -- else: |
312 | -- self._decorate_request('X_IDENTITY_STATUS', |
313 | -- 'Confirmed', |
314 | -- env, |
315 | -- proxy_headers) |
316 | -- |
317 | -- #Collect information about valid claims |
318 | -- if valid: |
319 | -- claims = self._expound_claims(claims) |
320 | -- |
321 | -- # Store authentication data |
322 | -- if claims: |
323 | -- self._decorate_request('X_AUTHORIZATION', |
324 | -- 'Proxy %s' % claims['user'], |
325 | -- env, |
326 | -- proxy_headers) |
327 | -- |
328 | -- # For legacy compatibility before we had ID and Name |
329 | -- self._decorate_request('X_TENANT', |
330 | -- claims['tenant'], |
331 | -- env, |
332 | -- proxy_headers) |
333 | -- |
334 | -- # Services should use these |
335 | -- self._decorate_request('X_TENANT_NAME', |
336 | -- claims.get('tenantName', |
337 | -- claims['tenant']), |
338 | -- env, |
339 | -- proxy_headers) |
340 | -- self._decorate_request('X_TENANT_ID', |
341 | -- claims['tenant'], |
342 | -- env, |
343 | -- proxy_headers) |
344 | -- |
345 | -- self._decorate_request('X_USER', |
346 | -- claims['userName'], |
347 | -- env, |
348 | -- proxy_headers) |
349 | -- self._decorate_request('X_USER_ID', |
350 | -- claims['user'], |
351 | -- env, |
352 | -- proxy_headers) |
353 | -- |
354 | -- # NOTE(lzyeval): claims has a key 'roles' which is |
355 | -- # guaranteed to be a list (see note below) |
356 | -- roles = ','.join(filter(lambda x: x, claims['roles'])) |
357 | -- self._decorate_request('X_ROLE', |
358 | -- roles, |
359 | -- env, |
360 | -- proxy_headers) |
361 | -- |
362 | -- # NOTE(todd): unused |
363 | -- self.expanded = True |
364 | -- |
365 | -- #Send request downstream |
366 | -- return self._forward_request(env, start_response, proxy_headers) |
367 | -- |
368 | -- def _get_claims(self, env): |
369 | -- """Get claims from request""" |
370 | -- claims = env.get('HTTP_X_AUTH_TOKEN', env.get('HTTP_X_STORAGE_TOKEN')) |
371 | -- return claims |
372 | -+ raise InvalidUserToken('Unable to find token in headers') |
373 | - |
374 | - def _reject_request(self, env, start_response): |
375 | -- """Redirect client to auth server""" |
376 | -- headers = [('WWW-Authenticate', |
377 | -- "Keystone uri='%s'" % self.auth_location)] |
378 | -+ """Redirect client to auth server. |
379 | -+ |
380 | -+ :param env: wsgi request environment |
381 | -+ :param start_response: wsgi response callback |
382 | -+ :returns HTTPUnauthorized http response |
383 | -+ |
384 | -+ """ |
385 | -+ headers = [('WWW-Authenticate', 'Keystone uri=\'%s\'' % self.auth_uri)] |
386 | - resp = webob.exc.HTTPUnauthorized('Authentication required', headers) |
387 | - return resp(env, start_response) |
388 | - |
389 | -- def _reject_claims(self, env, start_response): |
390 | -- """Client sent bad claims""" |
391 | -- resp = webob.exc.HTTPUnauthorized() |
392 | -- return resp(env, start_response) |
393 | -+ def get_admin_token(self): |
394 | -+ """Return admin token, possibly fetching a new one. |
395 | -+ |
396 | -+ :return admin token id |
397 | -+ :raise ServiceError when unable to retrieve token from keystone |
398 | - |
399 | -- def _get_admin_auth_token(self, username, password): |
400 | - """ |
401 | -- This function gets an admin auth token to be used by this service to |
402 | -- validate a user's token. Validate_token is a priviledged call so |
403 | -- it needs to be authenticated by a service that is calling it |
404 | -+ if not self.admin_token: |
405 | -+ self.admin_token = self._request_admin_token() |
406 | -+ |
407 | -+ return self.admin_token |
408 | -+ |
409 | -+ def _get_http_connection(self): |
410 | -+ return self.http_client_class(self.auth_host, self.auth_port) |
411 | -+ |
412 | -+ def _json_request(self, method, path, body=None, additional_headers=None): |
413 | -+ """HTTP request helper used to make json requests. |
414 | -+ |
415 | -+ :param method: http method |
416 | -+ :param path: relative request url |
417 | -+ :param body: dict to encode to json as request body. Optional. |
418 | -+ :param additional_headers: dict of additional headers to send with |
419 | -+ http request. Optional. |
420 | -+ :return (http response object, response body parsed as json) |
421 | -+ :raise ServerError when unable to communicate with keystone |
422 | -+ |
423 | - """ |
424 | -- headers = { |
425 | -- "Content-type": "application/json", |
426 | -- "Accept": "application/json", |
427 | -- } |
428 | -- params = { |
429 | -- "auth": { |
430 | -- "passwordCredentials": { |
431 | -- "username": username, |
432 | -- "password": password, |
433 | -- }, |
434 | -- "tenantName": ADMIN_TENANTNAME, |
435 | -- } |
436 | -- } |
437 | -- if self.auth_protocol == "http": |
438 | -- conn = httplib.HTTPConnection(self.auth_host, self.auth_port) |
439 | -- else: |
440 | -- conn = httplib.HTTPSConnection(self.auth_host, |
441 | -- self.auth_port, |
442 | -- cert_file=self.cert_file) |
443 | -- conn.request("POST", |
444 | -- '/v2.0/tokens', |
445 | -- json.dumps(params), |
446 | -- headers=headers) |
447 | -- response = conn.getresponse() |
448 | -- data = response.read() |
449 | -- conn.close() |
450 | -+ conn = self._get_http_connection() |
451 | -+ |
452 | -+ kwargs = { |
453 | -+ 'headers': { |
454 | -+ 'Content-type': 'application/json', |
455 | -+ 'Accept': 'application/json', |
456 | -+ }, |
457 | -+ } |
458 | -+ |
459 | -+ if additional_headers: |
460 | -+ kwargs['headers'].update(additional_headers) |
461 | -+ |
462 | -+ if body: |
463 | -+ kwargs['body'] = json.dumps(body) |
464 | -+ |
465 | - try: |
466 | -- return json.loads(data)["access"]["token"]["id"] |
467 | -- except KeyError: |
468 | -- return None |
469 | -+ conn.request(method, path, **kwargs) |
470 | -+ response = conn.getresponse() |
471 | -+ body = response.read() |
472 | -+ data = json.loads(body) |
473 | -+ except Exception, e: |
474 | -+ logger.error('HTTP connection exception: %s' % e) |
475 | -+ raise ServiceError('Unable to communicate with keystone') |
476 | -+ finally: |
477 | -+ conn.close() |
478 | - |
479 | -- def _validate_claims(self, claims, retry=True): |
480 | -- """Validate claims, and provide identity information isf applicable """ |
481 | -+ return response, data |
482 | - |
483 | -- # Step 1: We need to auth with the keystone service, so get an |
484 | -- # admin token |
485 | -- if not self.admin_token: |
486 | -- self.admin_token = self._get_admin_auth_token(self.admin_user, |
487 | -- self.admin_password) |
488 | -+ def _request_admin_token(self): |
489 | -+ """Retrieve new token as admin user from keystone. |
490 | - |
491 | -- # Step 2: validate the user's token with the auth service |
492 | -- # since this is a priviledged op,m we need to auth ourselves |
493 | -- # by using an admin token |
494 | -- headers = { |
495 | -- 'Content-type': 'application/json', |
496 | -- 'Accept': 'application/json', |
497 | -- 'X-Auth-Token': self.admin_token, |
498 | -+ :return token id upon success |
499 | -+ :raises ServerError when unable to communicate with keystone |
500 | -+ |
501 | -+ """ |
502 | -+ params = { |
503 | -+ 'auth': { |
504 | -+ 'passwordCredentials': { |
505 | -+ 'username': self.admin_user, |
506 | -+ 'password': self.admin_password, |
507 | -+ }, |
508 | -+ 'tenantName': self.admin_tenant_name, |
509 | - } |
510 | -- ##TODO(ziad):we need to figure out how to auth to keystone |
511 | -- #since validate_token is a priviledged call |
512 | -- #Khaled's version uses creds to get a token |
513 | -- # 'X-Auth-Token': admin_token} |
514 | -- # we're using a test token from the ini file for now |
515 | -- conn = http_connect(self.auth_host, |
516 | -- self.auth_port, |
517 | -- 'GET', |
518 | -- '/v2.0/tokens/%s' % claims, |
519 | -- headers=headers) |
520 | -- resp = conn.getresponse() |
521 | -- # data = resp.read() |
522 | -- conn.close() |
523 | -- |
524 | -- if not str(resp.status).startswith('20'): |
525 | -- if retry: |
526 | -- self.admin_token = None |
527 | -- return self._validate_claims(claims, False) |
528 | -- else: |
529 | -- return False |
530 | -+ } |
531 | -+ |
532 | -+ response, data = self._json_request('POST', |
533 | -+ '/v2.0/tokens', |
534 | -+ body=params) |
535 | -+ |
536 | -+ try: |
537 | -+ token = data['access']['token']['id'] |
538 | -+ assert token |
539 | -+ return token |
540 | -+ except (AssertionError, KeyError): |
541 | -+ raise ServiceError('invalid json response') |
542 | -+ |
543 | -+ def _validate_user_token(self, user_token, retry=True): |
544 | -+ """Authenticate user token with keystone. |
545 | -+ |
546 | -+ :param user_token: user's token id |
547 | -+ :param retry: flag that forces the middleware to retry |
548 | -+ user authentication when an indeterminate |
549 | -+ response is received. Optional. |
550 | -+ :return token object received from keystone on success |
551 | -+ :raise InvalidUserToken if token is rejected |
552 | -+ :raise ServiceError if unable to authenticate token |
553 | -+ |
554 | -+ """ |
555 | -+ headers = {'X-Auth-Token': self.get_admin_token()} |
556 | -+ response, data = self._json_request('GET', |
557 | -+ '/v2.0/tokens/%s' % user_token, |
558 | -+ additional_headers=headers) |
559 | -+ |
560 | -+ if response.status == 200: |
561 | -+ return data |
562 | -+ if response.status == 404: |
563 | -+ # FIXME(ja): I'm assuming the 404 status means that user_token is |
564 | -+ # invalid - not that the admin_token is invalid |
565 | -+ raise InvalidUserToken('Token authorization failed') |
566 | -+ if response.status == 401: |
567 | -+ logger.info('Keystone rejected admin token, resetting') |
568 | -+ self.admin_token = None |
569 | - else: |
570 | -- #TODO(Ziad): there is an optimization we can do here. We have just |
571 | -- #received data from Keystone that we can use instead of making |
572 | -- #another call in _expound_claims |
573 | -- return True |
574 | -- |
575 | -- def _expound_claims(self, claims): |
576 | -- # Valid token. Get user data and put it in to the call |
577 | -- # so the downstream service can use it |
578 | -- headers = { |
579 | -- 'Content-type': 'application/json', |
580 | -- 'Accept': 'application/json', |
581 | -- 'X-Auth-Token': self.admin_token, |
582 | -- } |
583 | -- ##TODO(ziad):we need to figure out how to auth to keystone |
584 | -- #since validate_token is a priviledged call |
585 | -- #Khaled's version uses creds to get a token |
586 | -- # 'X-Auth-Token': admin_token} |
587 | -- # we're using a test token from the ini file for now |
588 | -- conn = http_connect(self.auth_host, |
589 | -- self.auth_port, |
590 | -- 'GET', |
591 | -- '/v2.0/tokens/%s' % claims, |
592 | -- headers=headers) |
593 | -- resp = conn.getresponse() |
594 | -- data = resp.read() |
595 | -- conn.close() |
596 | -- |
597 | -- if not str(resp.status).startswith('20'): |
598 | -- raise LookupError('Unable to locate claims: %s' % resp.status) |
599 | -- |
600 | -- token_info = json.loads(data) |
601 | -- access_user = token_info['access']['user'] |
602 | -- access_token = token_info['access']['token'] |
603 | -- # Nova looks for the non case-sensitive role 'admin' |
604 | -- # to determine admin-ness |
605 | -- # NOTE(lzyeval): roles is always a list |
606 | -- roles = map(lambda y: y['name'], access_user.get('roles', [])) |
607 | -+ logger.error('Bad response code while validating token: %s' % |
608 | -+ response.status) |
609 | -+ if retry: |
610 | -+ logger.info('Retrying validation') |
611 | -+ return self._validate_user_token(user_token, False) |
612 | -+ else: |
613 | -+ raise InvalidUserToken() |
614 | -+ |
615 | -+ def _build_user_headers(self, token_info): |
616 | -+ """Convert token object into headers. |
617 | - |
618 | -+ Build headers that represent authenticated user: |
619 | -+ * X_IDENTITY_STATUS: Confirmed or Invalid |
620 | -+ * X_TENANT_ID: id of tenant if tenant is present |
621 | -+ * X_TENANT_NAME: name of tenant if tenant is present |
622 | -+ * X_USER_ID: id of user |
623 | -+ * X_USER_NAME: name of user |
624 | -+ * X_ROLES: list of roles |
625 | -+ |
626 | -+ Additional (deprecated) headers include: |
627 | -+ * X_USER: name of user |
628 | -+ * X_TENANT: For legacy compatibility before we had ID and Name |
629 | -+ * X_ROLE: list of roles |
630 | -+ |
631 | -+ :param token_info: token object returned by keystone on authentication |
632 | -+ :raise InvalidUserToken when unable to parse token object |
633 | -+ |
634 | -+ """ |
635 | -+ user = token_info['access']['user'] |
636 | -+ token = token_info['access']['token'] |
637 | -+ roles = ','.join([role['name'] for role in user.get('roles', [])]) |
638 | -+ |
639 | -+ # FIXME(ja): I think we are checking in both places because: |
640 | -+ # tenant might not be returned, and there was a pre-release |
641 | -+ # that put tenant objects inside the user object? |
642 | - try: |
643 | -- tenant = access_token['tenant']['id'] |
644 | -- tenant_name = access_token['tenant']['name'] |
645 | -+ tenant_id = token['tenant']['id'] |
646 | -+ tenant_name = token['tenant']['name'] |
647 | - except: |
648 | -- tenant = None |
649 | -- tenant_name = None |
650 | -- if not tenant: |
651 | -- tenant = access_user.get('tenantId') |
652 | -- tenant_name = access_user.get('tenantName') |
653 | -- verified_claims = { |
654 | -- 'user': access_user['id'], |
655 | -- 'userName': access_user['username'], |
656 | -- 'tenant': tenant, |
657 | -- 'roles': roles, |
658 | -- } |
659 | -- if tenant_name: |
660 | -- verified_claims['tenantName'] = tenant_name |
661 | -- return verified_claims |
662 | -- |
663 | -- def _decorate_request(self, index, value, env, proxy_headers): |
664 | -- """Add headers to request""" |
665 | -- proxy_headers[index] = value |
666 | -- env['HTTP_%s' % index] = value |
667 | -- |
668 | -- def _forward_request(self, env, start_response, proxy_headers): |
669 | -- """Token/Auth processed & claims added to headers""" |
670 | -- self._decorate_request('AUTHORIZATION', |
671 | -- 'Basic %s' % self.service_pass, env, proxy_headers) |
672 | -- #now decide how to pass on the call |
673 | -- if self.app: |
674 | -- # Pass to downstream WSGI component |
675 | -- return self.app(env, start_response) |
676 | -- #.custom_start_response) |
677 | -- else: |
678 | -- # We are forwarding to a remote service (no downstream WSGI app) |
679 | -- req = webob.Request(proxy_headers) |
680 | -- parsed = urlparse(req.url) |
681 | -- |
682 | -- conn = http_connect(self.service_host, |
683 | -- self.service_port, |
684 | -- req.method, |
685 | -- parsed.path, |
686 | -- proxy_headers, |
687 | -- ssl=(self.service_protocol == 'https')) |
688 | -- resp = conn.getresponse() |
689 | -- data = resp.read() |
690 | -- |
691 | -- #TODO(ziad): use a more sophisticated proxy |
692 | -- # we are rewriting the headers now |
693 | -- |
694 | -- if resp.status == 401 or resp.status == 305: |
695 | -- # Add our own headers to the list |
696 | -- headers = [('WWW_AUTHENTICATE', |
697 | -- "Keystone uri='%s'" % self.auth_location)] |
698 | -- return webob.Response(status=resp.status, |
699 | -- body=data, |
700 | -- headerlist=headers)(env, start_response) |
701 | -- else: |
702 | -- return webob.Response(status=resp.status, |
703 | -- body=data)(env, start_response) |
704 | -+ tenant_id = user.get('tenantId') |
705 | -+ tenant_name = user.get('tenantName') |
706 | -+ |
707 | -+ user_id = user['id'] |
708 | -+ user_name = user['username'] |
709 | -+ |
710 | -+ return { |
711 | -+ 'X-Identity-Status': 'Confirmed', |
712 | -+ 'X-Tenant-Id': tenant_id, |
713 | -+ 'X-Tenant-Name': tenant_name, |
714 | -+ 'X-User-Id': user_id, |
715 | -+ 'X-User-Name': user_name, |
716 | -+ 'X-Roles': roles, |
717 | -+ # Deprecated |
718 | -+ 'X-User': user_name, |
719 | -+ 'X-Tenant': tenant_name, |
720 | -+ 'X-Role': roles, |
721 | -+ } |
722 | -+ |
723 | -+ def _header_to_env_var(self, key): |
724 | -+ """Convert header to wsgi env variable. |
725 | -+ |
726 | -+ :param key: http header name (ex. 'X-Auth-Token') |
727 | -+ :return wsgi env variable name (ex. 'HTTP_X_AUTH_TOKEN') |
728 | -+ |
729 | -+ """ |
730 | -+ return 'HTTP_%s' % key.replace('-', '_').upper() |
731 | -+ |
732 | -+ def _add_headers(self, env, headers): |
733 | -+ """Add http headers to environment.""" |
734 | -+ for (k, v) in headers.iteritems(): |
735 | -+ env_key = self._header_to_env_var(k) |
736 | -+ env[env_key] = v |
737 | -+ |
738 | -+ def _remove_headers(self, env, keys): |
739 | -+ """Remove http headers from environment.""" |
740 | -+ for k in keys: |
741 | -+ env_key = self._header_to_env_var(k) |
742 | -+ try: |
743 | -+ del env[env_key] |
744 | -+ except KeyError: |
745 | -+ pass |
746 | -+ |
747 | -+ def _get_header(self, env, key, default=None): |
748 | -+ """Get http header from environment.""" |
749 | -+ env_key = self._header_to_env_var(key) |
750 | -+ return env.get(env_key, default) |
751 | - |
752 | - |
753 | - def filter_factory(global_conf, **local_conf): |
754 | -@@ -438,12 +440,3 @@ def app_factory(global_conf, **local_con |
755 | - conf = global_conf.copy() |
756 | - conf.update(local_conf) |
757 | - return AuthProtocol(None, conf) |
758 | -- |
759 | --if __name__ == '__main__': |
760 | -- app_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), |
761 | -- os.pardir, |
762 | -- os.pardir, |
763 | -- 'examples/paste/auth_token.ini') |
764 | -- app = deploy.loadapp('config:%s' % app_path, |
765 | -- global_conf={'log_name': 'auth_token.log'}) |
766 | -- wsgi.server(eventlet.listen(('', 8090)), app) |
767 | |
768 | === modified file 'debian/patches/series' |
769 | --- debian/patches/series 2012-03-02 14:54:39 +0000 |
770 | +++ debian/patches/series 2012-03-07 02:13:21 +0000 |
771 | @@ -1,2 +1,1 @@ |
772 | sql_connection.patch |
773 | -keystone-auth.patch |
774 | |
775 | === modified file 'debian/patches/sql_connection.patch' |
776 | --- debian/patches/sql_connection.patch 2012-03-01 01:57:57 +0000 |
777 | +++ debian/patches/sql_connection.patch 2012-03-07 02:13:21 +0000 |
778 | @@ -18,7 +18,7 @@ |
779 | # syslog_log_facility = LOG_LOCAL0 |
780 | |
781 | [sql] |
782 | --connection = sqlite:///bla.db |
783 | +-connection = sqlite:///keystone.db |
784 | +connection = sqlite:////var/lib/keystone/keystone.db |
785 | idle_timeout = 200 |
786 | min_pool_size = 5 |