~ubuntu-branches/ubuntu/precise/keystone/precise

« back to all changes in this revision

Viewing changes to debian/patches/keystone-auth.patch

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Adam Gandleman, Chuck Short
  • Date: 2012-03-09 12:26:12 UTC
  • mfrom: (1.1.12)
  • Revision ID: package-import@ubuntu.com-20120309122612-6tqkztfb1bgnswhe
Tags: 2012.1~rc1~20120308.2103-0ubuntu1
[ Adam Gandleman ]
* debian/patches/keystone-auth.patch: Drop, applied upstream at commit
  29337e66.
* debian/patches/sql_connection.patch: Refresh

[ Chuck Short ]
* New upstream release.
* debian/patches/sql_connection.patch: Refreshed.

Show diffs side-by-side

added added

removed removed

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