163
166
from keystoneclient import utils
167
# to pass gate before oslo-config is deployed everywhere,
168
# try application copies first
169
for app in 'nova', 'glance', 'quantum', 'cinder':
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:
180
from oslo.config import cfg
183
169
# alternative middleware configuration in the main application's
184
170
# configuration file e.g. in nova.conf
185
171
# [keystone_authtoken]
233
220
help='How many times are we trying to reconnect when'
234
221
' communicating with Identity API Server.'),
235
cfg.StrOpt('http_handler',
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',
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'
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',
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',
280
help='Value only used for unit testing'),
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',
283
268
help='(optional) if defined, indicate whether token data'
460
451
self.admin_password = self._conf_get('admin_password')
461
452
self.admin_tenant_name = self._conf_get('admin_tenant_name')
463
# Token caching via memcache
465
self._cache_initialized = False # cache already initialized?
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 '
501
492
def _init_cache(self, env):
502
cache = self._conf_get('cache')
503
memcache_servers = self._conf_get('memcached_servers')
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)
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
514
498
def _conf_get(self, name):
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)
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')
854
837
def _build_user_headers(self, token_info):
991
974
return token only if fresh (not expired).
994
if self._cache and 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)
999
983
secret_key = self._memcache_secret_key
1000
984
if isinstance(secret_key, six.string_types):
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')
1053
self.LOG.debug('Cached Token %s seems expired', token_id)
1037
self.LOG.debug('Cached Token seems expired')
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)
1079
self._cache.set(cache_key,
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)
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.
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))
1165
1147
def _cache_store_invalid(self, token_id):
1166
1148
"""Store invalid token in cache."""
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')
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
1187
1167
# Determine the highest api version we can use.
1209
1189
if response.status_code == 200:
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:
1216
'Keystone rejected admin token %s, resetting', headers)
1196
'Keystone rejected admin token, resetting')
1217
1197
self.admin_token = None
1219
1199
self.LOG.error('Bad response code while validating token: %s',
1220
1200
response.status_code)
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)
1225
self.LOG.warn("Invalid user token: %s. Keystone response: %s.",
1205
self.LOG.warn("Invalid user token. Keystone response: %s", data)
1228
1207
raise InvalidUserToken()
1328
1307
self.token_revocation_list = self.fetch_revocation_list()
1329
1308
return self._token_revocation_list
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')
1316
def _atomic_write(destination, data):
1317
with tempfile.NamedTemporaryFile(dir=self.signing_dirname,
1320
os.rename(f.name, destination)
1323
_atomic_write(file_name, value)
1324
except (OSError, IOError):
1325
self.verify_signing_dir()
1326
_atomic_write(file_name, value)
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.
1338
1335
self._token_revocation_list = jsonutils.loads(value)
1339
1336
self.token_revocation_list_fetched_time = timeutils.utcnow()
1341
with tempfile.NamedTemporaryFile(dir=self.signing_dirname,
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')
1348
os.rename(f.name, self.revoked_file_name)
1337
self._atomic_write_to_signing_dir(self.revoked_file_name, value)
1350
1339
def fetch_revocation_list(self, retry=True):
1351
1340
headers = {'X-Auth-Token': self.get_admin_token()}
1364
1352
raise ServiceError('Revocation list improperly formatted.')
1365
1353
return self.cms_verify(data['signed'])
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)
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)
1372
def write_cert_file(data):
1373
with open(self.signing_cert_file_name, 'w') as certfile:
1374
certfile.write(data)
1376
if response.status_code != 200:
1377
raise exceptions.CertificateConfigError(response.text)
1381
write_cert_file(response.text)
1383
self.verify_signing_dir()
1384
write_cert_file(response.text)
1385
except (AssertionError, KeyError):
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')
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)
1394
if response.status_code != 200:
1395
raise exceptions.CertificateConfigError(response.text)
1398
with open(self.signing_ca_file_name, 'w') as certfile:
1399
certfile.write(response.text)
1400
except (AssertionError, KeyError):
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')
1369
class CachePool(list):
1370
"""A lazy pool of cache references."""
1372
def __init__(self, cache, memcached_servers):
1373
self._environment_cache = cache
1374
self._memcached_servers = memcached_servers
1376
@contextlib.contextmanager
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!
1387
# the pool is empty, so we need to create a new client
1388
c = memorycache.get_client(self._memcached_servers)
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)
1412
if __name__ == '__main__':
1413
"""Run this module directly to start a protected echo service::
1415
$ python -m keystoneclient.middleware.auth_token
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
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)
1429
from wsgiref import simple_server
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()