~ubuntu-branches/ubuntu/trusty/python-keystoneclient/trusty-proposed

« back to all changes in this revision

Viewing changes to keystoneclient/middleware/auth_token.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2014-03-27 12:08:28 UTC
  • mfrom: (1.1.26)
  • Revision ID: package-import@ubuntu.com-20140327120828-yu3vm5g1v1pkl93w
Tags: 1:0.7.1-ubuntu1
New upstream release. (LP: #1298453)

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
* Collects and forwards identity information based on a valid token
27
27
  such as user name, tenant, etc
28
28
 
29
 
Refer to: http://keystone.openstack.org/middlewarearchitecture.html
 
29
Refer to: http://docs.openstack.org/developer/python-keystoneclient/
 
30
middlewarearchitecture.html
30
31
 
31
32
HEADERS
32
33
-------
142
143
 
143
144
"""
144
145
 
 
146
import contextlib
145
147
import datetime
146
148
import logging
147
149
import os
151
153
import time
152
154
 
153
155
import netaddr
 
156
from oslo.config import cfg
154
157
import six
155
158
from six.moves import urllib
156
159
 
163
166
from keystoneclient import utils
164
167
 
165
168
 
166
 
CONF = None
167
 
# to pass gate before oslo-config is deployed everywhere,
168
 
# try application copies first
169
 
for app in 'nova', 'glance', 'quantum', 'cinder':
170
 
    try:
171
 
        cfg = __import__('%s.openstack.common.cfg' % app,
172
 
                         fromlist=['%s.openstack.common' % app])
173
 
        # test which application middleware is running in
174
 
        if hasattr(cfg, 'CONF') and 'config_file' in cfg.CONF:
175
 
            CONF = cfg.CONF
176
 
            break
177
 
    except ImportError:
178
 
        pass
179
 
if not CONF:
180
 
    from oslo.config import cfg
181
 
    CONF = cfg.CONF
182
 
 
183
169
# alternative middleware configuration in the main application's
184
170
# configuration file e.g. in nova.conf
185
171
# [keystone_authtoken]
196
182
# 'swift.cache' key. However it could be different, depending on deployment.
197
183
# To use Swift memcache, you must set the 'cache' option to the environment
198
184
# key where the Swift cache object is stored.
 
185
 
199
186
opts = [
200
187
    cfg.StrOpt('auth_admin_prefix',
201
188
               default='',
232
219
               default=3,
233
220
               help='How many times are we trying to reconnect when'
234
221
               ' communicating with Identity API Server.'),
235
 
    cfg.StrOpt('http_handler',
236
 
               default=None,
237
 
               help='Allows to pass in the name of a fake http_handler'
238
 
               ' callback function used instead of httplib.HTTPConnection or'
239
 
               ' httplib.HTTPSConnection. Useful for unit testing where'
240
 
               ' network is not available.'),
241
222
    cfg.StrOpt('admin_token',
242
223
               secret=True,
243
224
               help='Single shared secret with the Keystone configuration'
267
248
               help='Directory used to cache files related to PKI tokens'),
268
249
    cfg.ListOpt('memcached_servers',
269
250
                deprecated_name='memcache_servers',
270
 
                help='If defined, the memcache server(s) to use for'
271
 
                ' caching'),
 
251
                help='Optionally specify a list of memcached server(s) to'
 
252
                ' use for caching. If left undefined, tokens will instead be'
 
253
                ' cached in-process.'),
272
254
    cfg.IntOpt('token_cache_time',
273
255
               default=300,
274
 
               help='In order to prevent excessive requests and validations,'
275
 
               ' the middleware uses an in-memory cache for the tokens the'
276
 
               ' Keystone API returns. This is only valid if memcache_servers'
277
 
               ' is defined. Set to -1 to disable caching completely.'),
 
256
               help='In order to prevent excessive effort spent validating'
 
257
               ' tokens, the middleware caches previously-seen tokens for a'
 
258
               ' configurable duration (in seconds). Set to -1 to disable'
 
259
               ' caching completely.'),
278
260
    cfg.IntOpt('revocation_cache_time',
279
 
               default=1,
280
 
               help='Value only used for unit testing'),
 
261
               default=300,
 
262
               help='Determines the frequency at which the list of revoked'
 
263
               ' tokens is retrieved from the Identity service (in seconds). A'
 
264
               ' high number of revocation events combined with a low cache'
 
265
               ' duration may significantly reduce performance.'),
281
266
    cfg.StrOpt('memcache_security_strategy',
282
267
               default=None,
283
268
               help='(optional) if defined, indicate whether token data'
309
294
               ' token binding is needed to be allowed. Finally the name of a'
310
295
               ' binding method that must be present in tokens.'),
311
296
]
 
297
 
 
298
CONF = cfg.CONF
312
299
CONF.register_opts(opts, group='keystone_authtoken')
313
300
 
314
301
LIST_OF_VERSIONS_TO_ATTEMPT = ['v2.0', 'v3.0']
410
397
        auth_host = self._conf_get('auth_host')
411
398
        auth_port = int(self._conf_get('auth_port'))
412
399
        auth_protocol = self._conf_get('auth_protocol')
413
 
        self.auth_admin_prefix = self._conf_get('auth_admin_prefix')
 
400
        auth_admin_prefix = self._conf_get('auth_admin_prefix')
414
401
        self.auth_uri = self._conf_get('auth_uri')
415
402
 
416
403
        if netaddr.valid_ipv6(auth_host):
431
418
            # documented in bug 1207517
432
419
            self.auth_uri = self.request_uri
433
420
 
 
421
        if auth_admin_prefix:
 
422
            self.request_uri = "%s/%s" % (self.request_uri,
 
423
                                          auth_admin_prefix.strip('/'))
 
424
 
434
425
        # SSL
435
426
        self.cert_file = self._conf_get('certfile')
436
427
        self.key_file = self._conf_get('keyfile')
460
451
        self.admin_password = self._conf_get('admin_password')
461
452
        self.admin_tenant_name = self._conf_get('admin_tenant_name')
462
453
 
463
 
        # Token caching via memcache
464
 
        self._cache = None
465
 
        self._cache_initialized = False    # cache already initialized?
 
454
        # Token caching
 
455
        self._cache_pool = None
 
456
        self._cache_initialized = False
466
457
        # memcache value treatment, ENCRYPT or MAC
467
458
        self._memcache_security_strategy = \
468
459
            self._conf_get('memcache_security_strategy')
494
485
                raise ConfigurationError('memcache_security_strategy must be '
495
486
                                         'ENCRYPT or MAC')
496
487
            if not self._memcache_secret_key:
497
 
                raise ConfigurationError('mecmache_secret_key must be defined '
 
488
                raise ConfigurationError('memcache_secret_key must be defined '
498
489
                                         'when a memcache_security_strategy '
499
490
                                         'is defined')
500
491
 
501
492
    def _init_cache(self, env):
502
 
        cache = self._conf_get('cache')
503
 
        memcache_servers = self._conf_get('memcached_servers')
504
 
 
505
 
        if cache and env.get(cache, None) is not None:
506
 
            # use the cache from the upstream filter
507
 
            self.LOG.info('Using %s memcache for caching token', cache)
508
 
            self._cache = env.get(cache)
509
 
        else:
510
 
            # use Keystone memcache
511
 
            self._cache = memorycache.get_client(memcache_servers)
 
493
        self._cache_pool = CachePool(
 
494
            env.get(self._conf_get('cache')),
 
495
            self._conf_get('memcached_servers'))
512
496
        self._cache_initialized = True
513
497
 
514
498
    def _conf_get(self, name):
567
551
                    versions.append(version['id'])
568
552
            except KeyError:
569
553
                self.LOG.error(
570
 
                    'Invalid version response format from server', data)
 
554
                    'Invalid version response format from server')
571
555
                raise ServiceError('Unable to parse version response '
572
556
                                   'from keystone')
573
557
 
761
745
        if body:
762
746
            kwargs['data'] = jsonutils.dumps(body)
763
747
 
764
 
        path = self.auth_admin_prefix + path
765
 
 
766
748
        response = self._http_request(method, path, **kwargs)
767
749
 
768
750
        try:
810
792
                "Unexpected response from keystone service: %s", data)
811
793
            raise ServiceError('invalid json response')
812
794
        except (ValueError):
 
795
            data['access']['token']['id'] = '<SANITIZED>'
813
796
            self.LOG.warn(
814
797
                "Unable to parse expiration time from token: %s", data)
815
798
            raise ServiceError('invalid json response')
842
825
            return data
843
826
        except NetworkError:
844
827
            self.LOG.debug('Token validation failure.', exc_info=True)
845
 
            self.LOG.warn("Authorization failed for token %s", token_id)
 
828
            self.LOG.warn("Authorization failed for token")
846
829
            raise InvalidUserToken('Token authorization failed')
847
830
        except Exception:
848
831
            self.LOG.debug('Token validation failure.', exc_info=True)
849
832
            if token_id:
850
833
                self._cache_store_invalid(token_id)
851
 
            self.LOG.warn("Authorization failed for token %s", token_id)
 
834
            self.LOG.warn("Authorization failed for token")
852
835
            raise InvalidUserToken('Token authorization failed')
853
836
 
854
837
    def _build_user_headers(self, token_info):
991
974
        return token only if fresh (not expired).
992
975
        """
993
976
 
994
 
        if self._cache and token_id:
 
977
        if token_id:
995
978
            if self._memcache_security_strategy is None:
996
979
                key = CACHE_KEY_TEMPLATE % token_id
997
 
                serialized = self._cache.get(key)
 
980
                with self._cache_pool.reserve() as cache:
 
981
                    serialized = cache.get(key)
998
982
            else:
999
983
                secret_key = self._memcache_secret_key
1000
984
                if isinstance(secret_key, six.string_types):
1008
992
                    security_strategy)
1009
993
                cache_key = CACHE_KEY_TEMPLATE % (
1010
994
                    memcache_crypt.get_cache_key(keys))
1011
 
                raw_cached = self._cache.get(cache_key)
 
995
                with self._cache_pool.reserve() as cache:
 
996
                    raw_cached = cache.get(cache_key)
1012
997
                try:
1013
998
                    # unprotect_data will return None if raw_cached is None
1014
999
                    serialized = memcache_crypt.unprotect_data(keys,
1030
1015
                serialized = serialized.decode('utf-8')
1031
1016
            cached = jsonutils.loads(serialized)
1032
1017
            if cached == 'invalid':
1033
 
                self.LOG.debug('Cached Token %s is marked unauthorized',
1034
 
                               token_id)
 
1018
                self.LOG.debug('Cached Token is marked unauthorized')
1035
1019
                raise InvalidUserToken('Token authorization failed')
1036
1020
 
1037
1021
            data, expires = cached
1047
1031
            expires = timeutils.normalize_time(expires)
1048
1032
            utcnow = timeutils.utcnow()
1049
1033
            if ignore_expires or utcnow < expires:
1050
 
                self.LOG.debug('Returning cached token %s', token_id)
 
1034
                self.LOG.debug('Returning cached token')
1051
1035
                return data
1052
1036
            else:
1053
 
                self.LOG.debug('Cached Token %s seems expired', token_id)
 
1037
                self.LOG.debug('Cached Token seems expired')
1054
1038
 
1055
1039
    def _cache_store(self, token_id, data):
1056
1040
        """Store value into memcache.
1076
1060
            cache_key = CACHE_KEY_TEMPLATE % memcache_crypt.get_cache_key(keys)
1077
1061
            data_to_store = memcache_crypt.protect_data(keys, serialized_data)
1078
1062
 
1079
 
        self._cache.set(cache_key,
1080
 
                        data_to_store,
1081
 
                        time=self.token_cache_time)
 
1063
        with self._cache_pool.reserve() as cache:
 
1064
            cache.set(cache_key, data_to_store, time=self.token_cache_time)
1082
1065
 
1083
1066
    def _invalid_user_token(self, msg=False):
1084
1067
        # NOTE(jamielennox): use False as the default so that None is valid
1158
1141
        quick check of token freshness on retrieval.
1159
1142
 
1160
1143
        """
1161
 
        if self._cache:
1162
 
                self.LOG.debug('Storing %s token in memcache', token_id)
1163
 
                self._cache_store(token_id, (data, expires))
 
1144
        self.LOG.debug('Storing token in cache')
 
1145
        self._cache_store(token_id, (data, expires))
1164
1146
 
1165
1147
    def _cache_store_invalid(self, token_id):
1166
1148
        """Store invalid token in cache."""
1167
 
        if self._cache:
1168
 
            self.LOG.debug(
1169
 
                'Marking token %s as unauthorized in memcache', token_id)
1170
 
            self._cache_store(token_id, 'invalid')
 
1149
        self.LOG.debug('Marking token as unauthorized in cache')
 
1150
        self._cache_store(token_id, 'invalid')
1171
1151
 
1172
1152
    def cert_file_missing(self, proc_output, file_name):
1173
1153
        return (file_name in proc_output and not os.path.exists(file_name))
1179
1159
        :param retry: flag that forces the middleware to retry
1180
1160
                      user authentication when an indeterminate
1181
1161
                      response is received. Optional.
1182
 
        :return token object received from keystone on success
1183
 
        :raise InvalidUserToken if token is rejected
1184
 
        :raise ServiceError if unable to authenticate token
 
1162
        :return: token object received from keystone on success
 
1163
        :raise InvalidUserToken: if token is rejected
 
1164
        :raise ServiceError: if unable to authenticate token
1185
1165
 
1186
1166
        """
1187
1167
        # Determine the highest api version we can use.
1209
1189
        if response.status_code == 200:
1210
1190
            return data
1211
1191
        if response.status_code == 404:
1212
 
            self.LOG.warn("Authorization failed for token %s", user_token)
 
1192
            self.LOG.warn("Authorization failed for token")
1213
1193
            raise InvalidUserToken('Token authorization failed')
1214
1194
        if response.status_code == 401:
1215
1195
            self.LOG.info(
1216
 
                'Keystone rejected admin token %s, resetting', headers)
 
1196
                'Keystone rejected admin token, resetting')
1217
1197
            self.admin_token = None
1218
1198
        else:
1219
1199
            self.LOG.error('Bad response code while validating token: %s',
1220
1200
                           response.status_code)
1221
1201
        if retry:
1222
1202
            self.LOG.info('Retrying validation')
1223
 
            return self._validate_user_token(user_token, env, False)
 
1203
            return self.verify_uuid_token(user_token, False)
1224
1204
        else:
1225
 
            self.LOG.warn("Invalid user token: %s. Keystone response: %s.",
1226
 
                          user_token, data)
 
1205
            self.LOG.warn("Invalid user token. Keystone response: %s", data)
1227
1206
 
1228
1207
            raise InvalidUserToken()
1229
1208
 
1239
1218
        token_id = utils.hash_signed_token(signed_text)
1240
1219
        for revoked_id in revoked_ids:
1241
1220
            if token_id == revoked_id:
1242
 
                self.LOG.debug('Token %s is marked as having been revoked',
1243
 
                               token_id)
 
1221
                self.LOG.debug('Token is marked as having been revoked')
1244
1222
                return True
1245
1223
        return False
1246
1224
 
1263
1241
                                          self.signing_ca_file_name):
1264
1242
                    self.fetch_ca_cert()
1265
1243
                    continue
 
1244
                self.LOG.error('CMS Verify output: %s', err.output)
1266
1245
                raise
1267
1246
            except cms.subprocess.CalledProcessError as err:
1268
1247
                self.LOG.warning('Verify error: %s', err)
1328
1307
            self.token_revocation_list = self.fetch_revocation_list()
1329
1308
        return self._token_revocation_list
1330
1309
 
 
1310
    def _atomic_write_to_signing_dir(self, file_name, value):
 
1311
        # In Python2, encoding is slow so the following check avoids it if it
 
1312
        # is not absolutely necessary.
 
1313
        if isinstance(value, six.text_type):
 
1314
            value = value.encode('utf-8')
 
1315
 
 
1316
        def _atomic_write(destination, data):
 
1317
            with tempfile.NamedTemporaryFile(dir=self.signing_dirname,
 
1318
                                             delete=False) as f:
 
1319
                f.write(data)
 
1320
            os.rename(f.name, destination)
 
1321
 
 
1322
        try:
 
1323
            _atomic_write(file_name, value)
 
1324
        except (OSError, IOError):
 
1325
            self.verify_signing_dir()
 
1326
            _atomic_write(file_name, value)
 
1327
 
1331
1328
    @token_revocation_list.setter
1332
1329
    def token_revocation_list(self, value):
1333
1330
        """Save a revocation list to memory and to disk.
1337
1334
        """
1338
1335
        self._token_revocation_list = jsonutils.loads(value)
1339
1336
        self.token_revocation_list_fetched_time = timeutils.utcnow()
1340
 
 
1341
 
        with tempfile.NamedTemporaryFile(dir=self.signing_dirname,
1342
 
                                         delete=False) as f:
1343
 
            # In Python2, encoding is slow so the following check avoids it if
1344
 
            # it is not absolutely necessary.
1345
 
            if isinstance(value, six.text_type):
1346
 
                value = value.encode('utf-8')
1347
 
            f.write(value)
1348
 
        os.rename(f.name, self.revoked_file_name)
 
1337
        self._atomic_write_to_signing_dir(self.revoked_file_name, value)
1349
1338
 
1350
1339
    def fetch_revocation_list(self, retry=True):
1351
1340
        headers = {'X-Auth-Token': self.get_admin_token()}
1354
1343
        if response.status_code == 401:
1355
1344
            if retry:
1356
1345
                self.LOG.info(
1357
 
                    'Keystone rejected admin token %s, resetting admin token',
1358
 
                    headers)
 
1346
                    'Keystone rejected admin token, resetting admin token')
1359
1347
                self.admin_token = None
1360
1348
                return self.fetch_revocation_list(retry=False)
1361
1349
        if response.status_code != 200:
1364
1352
            raise ServiceError('Revocation list improperly formatted.')
1365
1353
        return self.cms_verify(data['signed'])
1366
1354
 
 
1355
    def _fetch_cert_file(self, cert_file_name, cert_type):
 
1356
        path = '/v2.0/certificates/' + cert_type
 
1357
        response = self._http_request('GET', path)
 
1358
        if response.status_code != 200:
 
1359
            raise exceptions.CertificateConfigError(response.text)
 
1360
        self._atomic_write_to_signing_dir(cert_file_name, response.text)
 
1361
 
1367
1362
    def fetch_signing_cert(self):
1368
 
        path = self.auth_admin_prefix.rstrip('/')
1369
 
        path += '/v2.0/certificates/signing'
1370
 
        response = self._http_request('GET', path)
1371
 
 
1372
 
        def write_cert_file(data):
1373
 
            with open(self.signing_cert_file_name, 'w') as certfile:
1374
 
                certfile.write(data)
1375
 
 
1376
 
        if response.status_code != 200:
1377
 
            raise exceptions.CertificateConfigError(response.text)
1378
 
 
1379
 
        try:
1380
 
            try:
1381
 
                write_cert_file(response.text)
1382
 
            except IOError:
1383
 
                self.verify_signing_dir()
1384
 
                write_cert_file(response.text)
1385
 
        except (AssertionError, KeyError):
1386
 
            self.LOG.warn(
1387
 
                "Unexpected response from keystone service: %s", response.text)
1388
 
            raise ServiceError('invalid json response')
 
1363
        self._fetch_cert_file(self.signing_cert_file_name, 'signing')
1389
1364
 
1390
1365
    def fetch_ca_cert(self):
1391
 
        path = self.auth_admin_prefix.rstrip('/') + '/v2.0/certificates/ca'
1392
 
        response = self._http_request('GET', path)
1393
 
 
1394
 
        if response.status_code != 200:
1395
 
            raise exceptions.CertificateConfigError(response.text)
1396
 
 
1397
 
        try:
1398
 
            with open(self.signing_ca_file_name, 'w') as certfile:
1399
 
                certfile.write(response.text)
1400
 
        except (AssertionError, KeyError):
1401
 
            self.LOG.warn(
1402
 
                "Unexpected response from keystone service: %s", response.text)
1403
 
            raise ServiceError('invalid json response')
 
1366
        self._fetch_cert_file(self.signing_ca_file_name, 'ca')
 
1367
 
 
1368
 
 
1369
class CachePool(list):
 
1370
    """A lazy pool of cache references."""
 
1371
 
 
1372
    def __init__(self, cache, memcached_servers):
 
1373
        self._environment_cache = cache
 
1374
        self._memcached_servers = memcached_servers
 
1375
 
 
1376
    @contextlib.contextmanager
 
1377
    def reserve(self):
 
1378
        """Context manager to manage a pooled cache reference."""
 
1379
        if self._environment_cache is not None:
 
1380
            # skip pooling and just use the cache from the upstream filter
 
1381
            yield self._environment_cache
 
1382
            return  # otherwise the context manager will continue!
 
1383
 
 
1384
        try:
 
1385
            c = self.pop()
 
1386
        except IndexError:
 
1387
            # the pool is empty, so we need to create a new client
 
1388
            c = memorycache.get_client(self._memcached_servers)
 
1389
 
 
1390
        try:
 
1391
            yield c
 
1392
        finally:
 
1393
            self.append(c)
1404
1394
 
1405
1395
 
1406
1396
def filter_factory(global_conf, **local_conf):
1417
1407
    conf = global_conf.copy()
1418
1408
    conf.update(local_conf)
1419
1409
    return AuthProtocol(None, conf)
 
1410
 
 
1411
 
 
1412
if __name__ == '__main__':
 
1413
    """Run this module directly to start a protected echo service::
 
1414
 
 
1415
        $ python -m keystoneclient.middleware.auth_token
 
1416
 
 
1417
    When the ``auth_token`` module authenticates a request, the echo service
 
1418
    will respond with all the environment variables presented to it by this
 
1419
    module.
 
1420
 
 
1421
    """
 
1422
    def echo_app(environ, start_response):
 
1423
        """A WSGI application that echoes the CGI environment to the user."""
 
1424
        start_response('200 OK', [('Content-Type', 'application/json')])
 
1425
        environment = dict((k, v) for k, v in six.iteritems(environ)
 
1426
                           if k.startswith('HTTP_X_'))
 
1427
        yield jsonutils.dumps(environment)
 
1428
 
 
1429
    from wsgiref import simple_server
 
1430
 
 
1431
    # hardcode any non-default configuration here
 
1432
    conf = {'auth_protocol': 'http', 'admin_token': 'ADMIN'}
 
1433
    app = AuthProtocol(echo_app, conf)
 
1434
    server = simple_server.make_server('', 8000, app)
 
1435
    print('Serving on port 8000 (Ctrl+C to end)...')
 
1436
    server.serve_forever()