|
30 | 30 | access key.
|
31 | 31 | * Validates s3 token with Keystone.
|
32 | 32 | * Transforms the account name to AUTH_%(tenant_name).
|
| 33 | +* Optionally can retrieve and cache secret from keystone |
| 34 | + to validate signature locally |
33 | 35 |
|
34 | 36 | """
|
35 | 37 |
|
36 | 38 | import base64
|
37 | 39 | import json
|
38 | 40 |
|
| 41 | +from keystoneclient.v3 import client as keystone_client |
| 42 | +from keystoneauth1 import session as keystone_session |
| 43 | +from keystoneauth1 import loading as keystone_loading |
39 | 44 | import requests
|
40 | 45 | import six
|
41 | 46 | from six.moves import urllib
|
42 | 47 |
|
43 | 48 | from swift.common.swob import Request, HTTPBadRequest, HTTPUnauthorized, \
|
44 | 49 | HTTPException
|
45 |
| -from swift.common.utils import config_true_value, split_path, get_logger |
| 50 | +from swift.common.utils import config_true_value, split_path, get_logger, \ |
| 51 | + cache_from_env |
46 | 52 | from swift.common.wsgi import ConfigFileError
|
47 | 53 |
|
48 | 54 |
|
@@ -155,6 +161,31 @@ def __init__(self, app, conf):
|
155 | 161 | else:
|
156 | 162 | self._verify = None
|
157 | 163 |
|
| 164 | + self._secret_cache_duration = int(conf.get('secret_cache_duration', 0)) |
| 165 | + if self._secret_cache_duration > 0: |
| 166 | + try: |
| 167 | + auth_plugin = keystone_loading.get_plugin_loader( |
| 168 | + conf.get('auth_type')) |
| 169 | + available_auth_options = auth_plugin.get_options() |
| 170 | + auth_options = {} |
| 171 | + for option in available_auth_options: |
| 172 | + name = option.name.replace('-', '_') |
| 173 | + value = conf.get(name) |
| 174 | + if value: |
| 175 | + auth_options[name] = value |
| 176 | + |
| 177 | + auth = auth_plugin.load_from_options(**auth_options) |
| 178 | + session = keystone_session.Session(auth=auth) |
| 179 | + self.keystoneclient = keystone_client.Client(session=session) |
| 180 | + self._logger.info("Caching s3tokens for %s seconds", |
| 181 | + self._secret_cache_duration) |
| 182 | + except Exception: |
| 183 | + self._logger.warning("Unable to load keystone auth_plugin. " |
| 184 | + "Secret caching will be unavailable.", |
| 185 | + exc_info=True) |
| 186 | + self.keystoneclient = None |
| 187 | + self._secret_cache_duration = 0 |
| 188 | + |
158 | 189 | def _deny_request(self, code):
|
159 | 190 | error_cls, message = {
|
160 | 191 | 'AccessDenied': (HTTPUnauthorized, 'Access denied'),
|
@@ -245,64 +276,99 @@ def __call__(self, environ, start_response):
|
245 | 276 | creds = {'credentials': {'access': access,
|
246 | 277 | 'token': token,
|
247 | 278 | 'signature': signature}}
|
248 |
| - creds_json = json.dumps(creds) |
249 |
| - self._logger.debug('Connecting to Keystone sending this JSON: %s', |
250 |
| - creds_json) |
251 |
| - # NOTE(vish): We could save a call to keystone by having |
252 |
| - # keystone return token, tenant, user, and roles |
253 |
| - # from this call. |
254 |
| - # |
255 |
| - # NOTE(chmou): We still have the same problem we would need to |
256 |
| - # change token_auth to detect if we already |
257 |
| - # identified and not doing a second query and just |
258 |
| - # pass it through to swiftauth in this case. |
259 |
| - try: |
260 |
| - # NB: requests.Response, not swob.Response |
261 |
| - resp = self._json_request(creds_json) |
262 |
| - except HTTPException as e_resp: |
263 |
| - if self._delay_auth_decision: |
264 |
| - msg = 'Received error, deferring rejection based on error: %s' |
265 |
| - self._logger.debug(msg, e_resp.status) |
266 |
| - return self._app(environ, start_response) |
267 |
| - else: |
268 |
| - msg = 'Received error, rejecting request with error: %s' |
269 |
| - self._logger.debug(msg, e_resp.status) |
270 |
| - # NB: swob.Response, not requests.Response |
271 |
| - return e_resp(environ, start_response) |
272 |
| - |
273 |
| - self._logger.debug('Keystone Reply: Status: %d, Output: %s', |
274 |
| - resp.status_code, resp.content) |
275 | 279 |
|
276 |
| - try: |
277 |
| - token = resp.json() |
278 |
| - if 'access' in token: |
279 |
| - headers, token_id, tenant = parse_v2_response(token) |
280 |
| - elif 'token' in token: |
281 |
| - headers, token_id, tenant = parse_v3_response(token) |
282 |
| - else: |
283 |
| - raise ValueError |
284 |
| - |
285 |
| - # Populate the environment similar to auth_token, |
286 |
| - # so we don't have to contact Keystone again. |
| 280 | + memcache_client = None |
| 281 | + memcache_token_key = 's3secret/%s' % access |
| 282 | + if self._secret_cache_duration > 0: |
| 283 | + memcache_client = cache_from_env(environ) |
| 284 | + cached_auth_data = None |
| 285 | + |
| 286 | + if memcache_client: |
| 287 | + cached_auth_data = memcache_client.get(memcache_token_key) |
| 288 | + if cached_auth_data: |
| 289 | + headers, token_id, tenant, secret = cached_auth_data |
| 290 | + if s3_auth_details['check_signature'](secret): |
| 291 | + self._logger.debug("Cached creds valid") |
| 292 | + else: |
| 293 | + self._logger.debug("Cached creds invalid") |
| 294 | + cached_auth_data = None |
| 295 | + |
| 296 | + if not cached_auth_data: |
| 297 | + creds_json = json.dumps(creds) |
| 298 | + self._logger.debug('Connecting to Keystone sending this JSON: %s', |
| 299 | + creds_json) |
| 300 | + # NOTE(vish): We could save a call to keystone by having |
| 301 | + # keystone return token, tenant, user, and roles |
| 302 | + # from this call. |
287 | 303 | #
|
288 |
| - # Note that although the strings are unicode following json |
289 |
| - # deserialization, Swift's HeaderEnvironProxy handles ensuring |
290 |
| - # they're stored as native strings |
291 |
| - req.headers.update(headers) |
292 |
| - req.environ['keystone.token_info'] = token |
293 |
| - except (ValueError, KeyError, TypeError): |
294 |
| - if self._delay_auth_decision: |
295 |
| - error = ('Error on keystone reply: %d %s - ' |
296 |
| - 'deferring rejection downstream') |
297 |
| - self._logger.debug(error, resp.status_code, resp.content) |
298 |
| - return self._app(environ, start_response) |
299 |
| - else: |
300 |
| - error = ('Error on keystone reply: %d %s - ' |
301 |
| - 'rejecting request') |
302 |
| - self._logger.debug(error, resp.status_code, resp.content) |
303 |
| - return self._deny_request('InvalidURI')( |
304 |
| - environ, start_response) |
305 |
| - |
| 304 | + # NOTE(chmou): We still have the same problem we would need to |
| 305 | + # change token_auth to detect if we already |
| 306 | + # identified and not doing a second query and just |
| 307 | + # pass it through to swiftauth in this case. |
| 308 | + try: |
| 309 | + # NB: requests.Response, not swob.Response |
| 310 | + resp = self._json_request(creds_json) |
| 311 | + except HTTPException as e_resp: |
| 312 | + if self._delay_auth_decision: |
| 313 | + msg = ('Received error, deferring rejection based on ' |
| 314 | + 'error: %s') |
| 315 | + self._logger.debug(msg, e_resp.status) |
| 316 | + return self._app(environ, start_response) |
| 317 | + else: |
| 318 | + msg = 'Received error, rejecting request with error: %s' |
| 319 | + self._logger.debug(msg, e_resp.status) |
| 320 | + # NB: swob.Response, not requests.Response |
| 321 | + return e_resp(environ, start_response) |
| 322 | + |
| 323 | + self._logger.debug('Keystone Reply: Status: %d, Output: %s', |
| 324 | + resp.status_code, resp.content) |
| 325 | + |
| 326 | + try: |
| 327 | + token = resp.json() |
| 328 | + if 'access' in token: |
| 329 | + headers, token_id, tenant = parse_v2_response(token) |
| 330 | + elif 'token' in token: |
| 331 | + headers, token_id, tenant = parse_v3_response(token) |
| 332 | + else: |
| 333 | + raise ValueError |
| 334 | + if memcache_client: |
| 335 | + user_id = headers.get('X-User-Id') |
| 336 | + if not user_id: |
| 337 | + raise ValueError |
| 338 | + try: |
| 339 | + cred_ref = self.keystoneclient.ec2.get( |
| 340 | + user_id=user_id, |
| 341 | + access=access) |
| 342 | + memcache_client.set( |
| 343 | + memcache_token_key, |
| 344 | + (headers, token_id, tenant, cred_ref.secret), |
| 345 | + time=self._secret_cache_duration) |
| 346 | + self._logger.debug("Cached keystone credentials") |
| 347 | + except Exception: |
| 348 | + self._logger.warning("Unable to cache secret", |
| 349 | + exc_info=True) |
| 350 | + |
| 351 | + # Populate the environment similar to auth_token, |
| 352 | + # so we don't have to contact Keystone again. |
| 353 | + # |
| 354 | + # Note that although the strings are unicode following json |
| 355 | + # deserialization, Swift's HeaderEnvironProxy handles ensuring |
| 356 | + # they're stored as native strings |
| 357 | + req.environ['keystone.token_info'] = token |
| 358 | + except (ValueError, KeyError, TypeError): |
| 359 | + if self._delay_auth_decision: |
| 360 | + error = ('Error on keystone reply: %d %s - ' |
| 361 | + 'deferring rejection downstream') |
| 362 | + self._logger.debug(error, resp.status_code, resp.content) |
| 363 | + return self._app(environ, start_response) |
| 364 | + else: |
| 365 | + error = ('Error on keystone reply: %d %s - ' |
| 366 | + 'rejecting request') |
| 367 | + self._logger.debug(error, resp.status_code, resp.content) |
| 368 | + return self._deny_request('InvalidURI')( |
| 369 | + environ, start_response) |
| 370 | + |
| 371 | + req.headers.update(headers) |
306 | 372 | req.headers['X-Auth-Token'] = token_id
|
307 | 373 | tenant_to_connect = force_tenant or tenant['id']
|
308 | 374 | if six.PY2 and isinstance(tenant_to_connect, six.text_type):
|
|
0 commit comments