1# Copyright 2010-2012 OpenStack Foundation 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12# implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16r""" 17Token-based Authentication Middleware. 18 19This WSGI component: 20 21* Verifies that incoming client requests have valid tokens by validating 22 tokens with the auth service. 23* Rejects unauthenticated requests unless the auth_token middleware is in 24 ``delay_auth_decision`` mode, which means the final decision is delegated to 25 the downstream WSGI component (usually the OpenStack service). 26* Collects and forwards identity information based on a valid token 27 such as user name, domain, project, etc. 28 29Refer to: https://docs.openstack.org/keystonemiddleware/latest/\ 30middlewarearchitecture.html 31 32 33Headers 34------- 35 36The auth_token middleware uses headers sent in by the client on the request 37and sets headers and environment variables for the downstream WSGI component. 38 39Coming in from initial call from client or customer 40^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 41 42HTTP_X_AUTH_TOKEN 43 The client token being passed in. 44 45HTTP_X_SERVICE_TOKEN 46 A service token being passed in. 47 48Used for communication between components 49^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 50 51WWW-Authenticate 52 HTTP header returned to a user indicating which endpoint to use 53 to retrieve a new token. 54 55What auth_token adds to the request for use by the OpenStack service 56^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 57 58When using composite authentication (a user and service token are 59present) additional service headers relating to the service user 60will be added. They take the same form as the standard headers but add 61``_SERVICE_``. These headers will not exist in the environment if no 62service token is present. 63 64HTTP_X_IDENTITY_STATUS, HTTP_X_SERVICE_IDENTITY_STATUS 65 Will be set to either ``Confirmed`` or ``Invalid``. 66 67 The underlying service will only see a value of 'Invalid' if the middleware 68 is configured to run in ``delay_auth_decision`` mode. As with all such 69 headers, ``HTTP_X_SERVICE_IDENTITY_STATUS`` will only exist in the 70 environment if a service token is presented. This is different than 71 ``HTTP_X_IDENTITY_STATUS`` which is always set even if no user token is 72 presented. This allows the underlying service to determine if a 73 denial should use ``401 Unauthenticated`` or ``403 Forbidden``. 74 75HTTP_OPENSTACK_SYSTEM_SCOPE 76 A string relaying system information about the token's scope. This 77 attribute is only present if the token is system-scoped. The string ``all`` 78 means the token is scoped to the entire deployment system. 79 80HTTP_X_DOMAIN_ID, HTTP_X_SERVICE_DOMAIN_ID 81 Identity service managed unique identifier, string. Only present if 82 this is a domain-scoped token. 83 84HTTP_X_DOMAIN_NAME, HTTP_X_SERVICE_DOMAIN_NAME 85 Unique domain name, string. Only present if this is a domain-scoped 86 token. 87 88HTTP_X_PROJECT_ID, HTTP_X_SERVICE_PROJECT_ID 89 Identity service managed unique identifier, string. Only present if 90 this is a project-scoped token. 91 92HTTP_X_PROJECT_NAME, HTTP_X_SERVICE_PROJECT_NAME 93 Project name, unique within owning domain, string. Only present if 94 this is a project-scoped token. 95 96HTTP_X_PROJECT_DOMAIN_ID, HTTP_X_SERVICE_PROJECT_DOMAIN_ID 97 Identity service managed unique identifier of owning domain of 98 project, string. Only present if this is a project-scoped v3 token. If 99 this variable is set, this indicates that the PROJECT_NAME can only 100 be assumed to be unique within this domain. 101 102HTTP_X_PROJECT_DOMAIN_NAME, HTTP_X_SERVICE_PROJECT_DOMAIN_NAME 103 Name of owning domain of project, string. Only present if this is a 104 project-scoped v3 token. If this variable is set, this indicates that 105 the PROJECT_NAME can only be assumed to be unique within this domain. 106 107HTTP_X_USER_ID, HTTP_X_SERVICE_USER_ID 108 Identity-service managed unique identifier, string. 109 110HTTP_X_USER_NAME, HTTP_X_SERVICE_USER_NAME 111 User identifier, unique within owning domain, string. 112 113HTTP_X_USER_DOMAIN_ID, HTTP_X_SERVICE_USER_DOMAIN_ID 114 Identity service managed unique identifier of owning domain of 115 user, string. If this variable is set, this indicates that the USER_NAME 116 can only be assumed to be unique within this domain. 117 118HTTP_X_USER_DOMAIN_NAME, HTTP_X_SERVICE_USER_DOMAIN_NAME 119 Name of owning domain of user, string. If this variable is set, this 120 indicates that the USER_NAME can only be assumed to be unique within 121 this domain. 122 123HTTP_X_ROLES, HTTP_X_SERVICE_ROLES 124 Comma delimited list of case-sensitive role names. 125 126HTTP_X_IS_ADMIN_PROJECT 127 The string value 'True' or 'False' representing whether the user's token is 128 scoped to the admin project. As historically there was no admin project 129 this will default to True for tokens without this information to be 130 backwards with existing policy files. 131 132HTTP_X_SERVICE_CATALOG 133 service catalog (optional, JSON string). 134 135 For compatibility reasons this catalog will always be in the V2 catalog 136 format even if it is a v3 token. 137 138 .. note:: This is an exception in that it contains 'SERVICE' but relates to 139 a user token, not a service token. The existing user's catalog can be 140 very large; it was decided not to present a catalog relating to the 141 service token to avoid using more HTTP header space. 142 143HTTP_X_TENANT_ID 144 *Deprecated* in favor of HTTP_X_PROJECT_ID. 145 146 Identity service managed unique identifier, string. For v3 tokens, this 147 will be set to the same value as HTTP_X_PROJECT_ID. 148 149HTTP_X_TENANT_NAME 150 *Deprecated* in favor of HTTP_X_PROJECT_NAME. 151 152 Project identifier, unique within owning domain, string. For v3 tokens, 153 this will be set to the same value as HTTP_X_PROJECT_NAME. 154 155HTTP_X_TENANT 156 *Deprecated* in favor of HTTP_X_TENANT_ID and HTTP_X_TENANT_NAME. 157 158 Identity server-assigned unique identifier, string. For v3 tokens, this 159 will be set to the same value as HTTP_X_PROJECT_ID. 160 161HTTP_X_USER 162 *Deprecated* in favor of HTTP_X_USER_ID and HTTP_X_USER_NAME. 163 164 User name, unique within owning domain, string. 165 166HTTP_X_ROLE 167 *Deprecated* in favor of HTTP_X_ROLES. 168 169 Will contain the same values as HTTP_X_ROLES. 170 171Environment Variables 172^^^^^^^^^^^^^^^^^^^^^ 173 174These variables are set in the request environment for use by the downstream 175WSGI component. 176 177keystone.token_info 178 Information about the token discovered in the process of validation. This 179 may include extended information returned by the token validation call, as 180 well as basic information about the project and user. 181 182keystone.token_auth 183 A keystoneauth1 auth plugin that may be used with a 184 :py:class:`keystoneauth1.session.Session`. This plugin will load the 185 authentication data provided to auth_token middleware. 186 187 188Configuration 189------------- 190 191auth_token middleware configuration can be in the main application's 192configuration file, e.g. in ``nova.conf``: 193 194.. code-block:: ini 195 196 [keystone_authtoken] 197 auth_plugin = password 198 auth_url = http://keystone:5000/ 199 username = nova 200 user_domain_id = default 201 password = whyarewestillusingpasswords 202 project_name = service 203 project_domain_id = default 204 205Configuration can also be in the ``api-paste.ini`` file with the same options, 206but this is discouraged. 207 208Swift 209----- 210 211When deploy auth_token middleware with Swift, user may elect to use Swift 212memcache instead of the local auth_token memcache. Swift memcache is passed in 213from the request environment and it's identified by the ``swift.cache`` key. 214However it could be different, depending on deployment. To use Swift memcache, 215you must set the ``cache`` option to the environment key where the Swift cache 216object is stored. 217 218""" 219 220import copy 221import re 222 223from keystoneauth1 import access 224from keystoneauth1 import adapter 225from keystoneauth1 import discover 226from keystoneauth1 import exceptions as ksa_exceptions 227from keystoneauth1 import loading 228from keystoneauth1.loading import session as session_loading 229import oslo_cache 230from oslo_config import cfg 231from oslo_log import log as logging 232from oslo_serialization import jsonutils 233import webob.dec 234 235from keystonemiddleware._common import config 236from keystonemiddleware.auth_token import _auth 237from keystonemiddleware.auth_token import _base 238from keystonemiddleware.auth_token import _cache 239from keystonemiddleware.auth_token import _exceptions as ksm_exceptions 240from keystonemiddleware.auth_token import _identity 241from keystonemiddleware.auth_token import _opts 242from keystonemiddleware.auth_token import _request 243from keystonemiddleware.auth_token import _user_plugin 244from keystonemiddleware.i18n import _ 245 246 247_LOG = logging.getLogger(__name__) 248_CACHE_INVALID_INDICATOR = 'invalid' 249oslo_cache.configure(cfg.CONF) 250 251AUTH_TOKEN_OPTS = [ 252 (_base.AUTHTOKEN_GROUP, 253 _opts._OPTS + _auth.OPTS + loading.get_auth_common_conf_options()) 254] 255 256 257def list_opts(): 258 """Return a list of oslo_config options available in auth_token middleware. 259 260 The returned list includes all oslo_config options which may be registered 261 at runtime by the project. 262 263 Each element of the list is a tuple. The first element is the name of the 264 group under which the list of elements in the second element will be 265 registered. A group name of None corresponds to the [DEFAULT] group in 266 config files. 267 268 NOTE: This function is no longer used for oslo_config sample generation. 269 Some services rely on this function for listing ALL (including deprecated) 270 options and registering them into their own config objects which we do not 271 want for sample config files. 272 273 See: :py:func:`keystonemiddleware.auth_token._opts.list_opts` for sample 274 config files. 275 276 :returns: a list of (group_name, opts) tuples 277 """ 278 return [(g, copy.deepcopy(o)) for g, o in AUTH_TOKEN_OPTS] 279 280 281def _path_matches(request_path, path_pattern): 282 # The fnmatch module doesn't provide the ability to match * versus **, 283 # so convert to regex. 284 token_regex = (r'(?P<tag>{[^}]*})|' # {tag} # nosec 285 r'(?P<wild>\*(?=$|[^\*]))|' # * 286 r'(?P<rec_wild>\*\*)|' # ** 287 r'(?P<literal>[^{}\*])') # anything else 288 path_regex = '' 289 for match in re.finditer(token_regex, path_pattern): 290 token = match.groupdict() 291 if token['tag'] or token['wild']: 292 path_regex += r'[^\/]+' 293 if token['rec_wild']: 294 path_regex += '.*' 295 if token['literal']: 296 path_regex += token['literal'] 297 path_regex = r'^%s$' % path_regex 298 return re.match(path_regex, request_path) 299 300 301class _BIND_MODE(object): 302 DISABLED = 'disabled' 303 PERMISSIVE = 'permissive' 304 STRICT = 'strict' 305 REQUIRED = 'required' 306 KERBEROS = 'kerberos' 307 308 309class BaseAuthProtocol(object): 310 """A base class for AuthProtocol token checking implementations. 311 312 :param Callable app: The next application to call after middleware. 313 :param logging.Logger log: The logging object to use for output. By default 314 it will use a logger in the 315 keystonemiddleware.auth_token namespace. 316 :param str enforce_token_bind: The style of token binding enforcement to 317 perform. 318 """ 319 320 def __init__(self, 321 app, 322 log=_LOG, 323 enforce_token_bind=_BIND_MODE.PERMISSIVE, 324 service_token_roles=None, 325 service_token_roles_required=False, 326 service_type=None): 327 self.log = log 328 self._app = app 329 self._enforce_token_bind = enforce_token_bind 330 self._service_token_roles = set(service_token_roles or []) 331 self._service_token_roles_required = service_token_roles_required 332 self._service_token_warning_emitted = False 333 self._service_type = service_type 334 335 @webob.dec.wsgify(RequestClass=_request._AuthTokenRequest) 336 def __call__(self, req): 337 """Handle incoming request.""" 338 response = self.process_request(req) 339 if response: 340 return response 341 response = req.get_response(self._app) 342 return self.process_response(response) 343 344 def process_request(self, request): 345 """Process request. 346 347 If this method returns a value then that value will be used as the 348 response. The next application down the stack will not be executed and 349 process_response will not be called. 350 351 Otherwise, the next application down the stack will be executed and 352 process_response will be called with the generated response. 353 354 By default this method does not return a value. 355 356 :param request: Incoming request 357 :type request: _request.AuthTokenRequest 358 359 """ 360 user_auth_ref = None 361 serv_auth_ref = None 362 allow_expired = False 363 364 if request.service_token: 365 self.log.debug('Authenticating service token') 366 try: 367 _, serv_auth_ref = self._do_fetch_token(request.service_token) 368 self._validate_token(serv_auth_ref) 369 self._confirm_token_bind(serv_auth_ref, request) 370 except ksm_exceptions.InvalidToken: 371 self.log.info('Invalid service token') 372 request.service_token_valid = False 373 else: 374 # FIXME(jamielennox): The new behaviour for service tokens is 375 # that they have to pass the policy check to be allowed. 376 # Previously any token was accepted here. For now we will 377 # continue to mark service tokens as valid if they are valid 378 # but we will only allow service role tokens to do 379 # allow_expired. In future we should reject any token that 380 # isn't a service token here. 381 role_names = set(serv_auth_ref.role_names) 382 check = self._service_token_roles.intersection(role_names) 383 role_check_passed = bool(check) 384 385 # if service_token_roles_required then the service token is 386 # only valid if the roles check out. Otherwise at this point it 387 # is true because keystone has already validated it. 388 if self._service_token_roles_required: 389 request.service_token_valid = role_check_passed 390 else: 391 if not self._service_token_warning_emitted: 392 self.log.warning('A valid token was submitted as ' 393 'a service token, but it was not ' 394 'a valid service token. This is ' 395 'incorrect but backwards ' 396 'compatible behaviour. This will ' 397 'be removed in future releases.') 398 # prevent log spam on every single request 399 self._service_token_warning_emitted = True 400 401 request.service_token_valid = True 402 403 # allow_expired always requires passing the role check. 404 allow_expired = role_check_passed 405 406 if request.user_token: 407 self.log.debug('Authenticating user token') 408 try: 409 data, user_auth_ref = self._do_fetch_token( 410 request.user_token, 411 allow_expired=allow_expired) 412 self._validate_token(user_auth_ref, 413 allow_expired=allow_expired) 414 if user_auth_ref.version != 'v2.0': 415 self.validate_allowed_request(request, data['token']) 416 if not request.service_token: 417 self._confirm_token_bind(user_auth_ref, request) 418 except ksm_exceptions.InvalidToken: 419 self.log.info('Invalid user token') 420 request.user_token_valid = False 421 else: 422 request.user_token_valid = True 423 request.token_info = data 424 425 request.token_auth = _user_plugin.UserAuthPlugin(user_auth_ref, 426 serv_auth_ref) 427 428 def _validate_token(self, auth_ref, allow_expired=False): 429 """Perform the validation steps on the token. 430 431 :param auth_ref: The token data 432 :type auth_ref: keystoneauth1.access.AccessInfo 433 434 :raises exc.InvalidToken: if token is rejected 435 """ 436 # 0 seconds of validity means it is invalid right now 437 if (not allow_expired) and auth_ref.will_expire_soon(stale_duration=0): 438 raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 439 440 def _do_fetch_token(self, token, **kwargs): 441 """Helper method to fetch a token and convert it into an AccessInfo.""" 442 # NOTE(edmondsw): strip the token to remove any whitespace that may 443 # have been passed along in the header per bug 1689468 444 token = token.strip() 445 data = self.fetch_token(token, **kwargs) 446 447 try: 448 return data, access.create(body=data, auth_token=token) 449 except Exception: 450 self.log.warning('Invalid token contents.', exc_info=True) 451 raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 452 453 def fetch_token(self, token, **kwargs): 454 """Fetch the token data based on the value in the header. 455 456 Retrieve the data associated with the token value that was in the 457 header. This can be from PKI, contacting the identity server or 458 whatever is required. 459 460 :param str token: The token present in the request header. 461 :param dict kwargs: Additional keyword arguments may be passed through 462 here to support new features. If an implementation 463 is not aware of how to use these arguments it 464 should ignore them. 465 466 :raises exc.InvalidToken: if token is invalid. 467 468 :returns: The token data 469 :rtype: dict 470 """ 471 raise NotImplementedError() 472 473 def process_response(self, response): 474 """Do whatever you'd like to the response. 475 476 By default the response is returned unmodified. 477 478 :param response: Response object 479 :type response: ._request._AuthTokenResponse 480 """ 481 return response 482 483 def _invalid_user_token(self, msg=False): 484 # NOTE(jamielennox): use False as the default so that None is valid 485 if msg is False: 486 msg = _('Token authorization failed') 487 488 raise ksm_exceptions.InvalidToken(msg) 489 490 def _confirm_token_bind(self, auth_ref, req): 491 if self._enforce_token_bind == _BIND_MODE.DISABLED: 492 return 493 494 # permissive and strict modes don't require there to be a bind 495 permissive = self._enforce_token_bind in (_BIND_MODE.PERMISSIVE, 496 _BIND_MODE.STRICT) 497 498 if not auth_ref.bind: 499 if permissive: 500 # no bind provided and none required 501 return 502 else: 503 self.log.info('No bind information present in token.') 504 self._invalid_user_token() 505 506 # get the named mode if bind_mode is not one of the predefined 507 if permissive or self._enforce_token_bind == _BIND_MODE.REQUIRED: 508 name = None 509 else: 510 name = self._enforce_token_bind 511 512 if name and name not in auth_ref.bind: 513 self.log.info('Named bind mode %s not in bind information', 514 name) 515 self._invalid_user_token() 516 517 for bind_type, identifier in auth_ref.bind.items(): 518 if bind_type == _BIND_MODE.KERBEROS: 519 if req.auth_type != 'negotiate': 520 self.log.info('Kerberos credentials required and ' 521 'not present.') 522 self._invalid_user_token() 523 524 if req.remote_user != identifier: 525 self.log.info('Kerberos credentials do not match ' 526 'those in bind.') 527 self._invalid_user_token() 528 529 self.log.debug('Kerberos bind authentication successful.') 530 531 elif self._enforce_token_bind == _BIND_MODE.PERMISSIVE: 532 self.log.debug('Ignoring Unknown bind for permissive mode: ' 533 '%(bind_type)s: %(identifier)s.', 534 {'bind_type': bind_type, 535 'identifier': identifier}) 536 537 else: 538 self.log.info( 539 'Couldn`t verify unknown bind: %(bind_type)s: ' 540 '%(identifier)s.', 541 {'bind_type': bind_type, 'identifier': identifier}) 542 self._invalid_user_token() 543 544 def validate_allowed_request(self, request, token): 545 self.log.debug("Validating token access rules against request") 546 app_cred = token.get('application_credential') 547 if not app_cred: 548 return 549 access_rules = app_cred.get('access_rules') 550 if access_rules is None: 551 return 552 if hasattr(self, '_conf'): 553 my_service_type = self._conf.get('service_type') 554 else: 555 my_service_type = self._service_type 556 if not my_service_type: 557 self.log.warning('Cannot validate request with restricted' 558 ' access rules. Set service_type in' 559 ' [keystone_authtoken] to allow access rule' 560 ' validation.') 561 raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 562 # token can always be validated regardless of access rules 563 if (my_service_type == 'identity' and 564 request.method == 'GET' and 565 request.path.endswith('/v3/auth/tokens')): 566 return 567 catalog = token['catalog'] 568 # validate service type is in catalog 569 catalog_svcs = [s for s in catalog if s['type'] == my_service_type] 570 if len(catalog_svcs) == 0: 571 self.log.warning('Cannot validate request with restricted' 572 ' access rules. service_type in' 573 ' [keystone_authtoken] is not a valid service' 574 ' type in the catalog.') 575 raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 576 if request.service_token: 577 # The request may not match an allowed request, but the presence 578 # of the service token indicates this is a chain of requests and 579 # hence this request was not user-facing 580 return 581 for access_rule in access_rules: 582 method = access_rule['method'] 583 path = access_rule['path'] 584 service = access_rule['service'] 585 if request.method == method and \ 586 service == my_service_type and \ 587 _path_matches(request.path, path): 588 return 589 raise ksm_exceptions.InvalidToken(_('Token authorization failed')) 590 591 592class AuthProtocol(BaseAuthProtocol): 593 """Middleware that handles authenticating client calls.""" 594 595 def __init__(self, app, conf): 596 log = logging.getLogger(conf.get('log_name', __name__)) 597 log.info('Starting Keystone auth_token middleware') 598 599 self._conf = config.Config('auth_token', 600 _base.AUTHTOKEN_GROUP, 601 list_opts(), 602 conf) 603 if self._conf.oslo_conf_obj is not cfg.CONF: 604 oslo_cache.configure(self._conf.oslo_conf_obj) 605 606 token_roles_required = self._conf.get('service_token_roles_required') 607 608 if not token_roles_required: 609 log.warning('AuthToken middleware is set with ' 610 'keystone_authtoken.service_token_roles_required ' 611 'set to False. This is backwards compatible but ' 612 'deprecated behaviour. Please set this to True.') 613 614 super(AuthProtocol, self).__init__( 615 app, 616 log=log, 617 enforce_token_bind=self._conf.get('enforce_token_bind'), 618 service_token_roles=self._conf.get('service_token_roles'), 619 service_token_roles_required=token_roles_required) 620 621 # delay_auth_decision means we still allow unauthenticated requests 622 # through and we let the downstream service make the final decision 623 self._delay_auth_decision = self._conf.get('delay_auth_decision') 624 self._include_service_catalog = self._conf.get( 625 'include_service_catalog') 626 self._interface = self._conf.get('interface') 627 self._auth = self._create_auth_plugin() 628 self._session = self._create_session() 629 self._identity_server = self._create_identity_server() 630 631 self._www_authenticate_uri = self._conf.get('www_authenticate_uri') 632 if not self._www_authenticate_uri: 633 self._www_authenticate_uri = self._conf.get('auth_uri') 634 if not self._www_authenticate_uri: 635 self.log.warning( 636 'Configuring www_authenticate_uri to point to the public ' 637 'identity endpoint is required; clients may not be able to ' 638 'authenticate against an admin endpoint') 639 640 # FIXME(dolph): drop support for this fallback behavior as 641 # documented in bug 1207517. 642 643 self._www_authenticate_uri = \ 644 self._identity_server.www_authenticate_uri 645 646 self._token_cache = self._token_cache_factory() 647 648 def process_request(self, request): 649 """Process request. 650 651 Evaluate the headers in a request and attempt to authenticate the 652 request. If authenticated then additional headers are added to the 653 request for use by applications. If not authenticated the request will 654 be rejected or marked unauthenticated depending on configuration. 655 """ 656 request.remove_auth_headers() 657 self._token_cache.initialize(request.environ) 658 659 resp = super(AuthProtocol, self).process_request(request) 660 if resp: 661 return resp 662 663 if not request.user_token: 664 # if no user token is present then that's an invalid request 665 request.user_token_valid = False 666 667 # NOTE(jamielennox): The service status is allowed to be missing if a 668 # service token is not passed. If the service status is missing that's 669 # a valid request. We should find a better way to expose this from the 670 # request object. 671 user_status = request.user_token and request.user_token_valid 672 service_status = request.headers.get('X-Service-Identity-Status', 673 'Confirmed') 674 675 if not (user_status and service_status == 'Confirmed'): 676 if self._delay_auth_decision: 677 self.log.debug('Deferring reject downstream') 678 else: 679 self.log.info('Rejecting request') 680 message = _('The request you have made requires ' 681 'authentication.') 682 body = {'error': { 683 'code': 401, 684 'title': 'Unauthorized', 685 'message': message, 686 }} 687 raise webob.exc.HTTPUnauthorized( 688 body=jsonutils.dumps(body), 689 headers=self._reject_auth_headers, 690 charset='UTF-8', 691 content_type='application/json') 692 693 if request.user_token_valid: 694 request.set_user_headers(request.token_auth.user) 695 696 if self._include_service_catalog: 697 request.set_service_catalog_headers(request.token_auth.user) 698 699 if request.token_auth: 700 request.token_auth._auth = self._auth 701 request.token_auth._session = self._session 702 703 if request.service_token and request.service_token_valid: 704 request.set_service_headers(request.token_auth.service) 705 706 if self.log.isEnabledFor(logging.DEBUG): 707 self.log.debug('Received request from %s', 708 request.token_auth._log_format) 709 710 def process_response(self, response): 711 """Process Response. 712 713 Add ``WWW-Authenticate`` headers to requests that failed with 714 ``401 Unauthenticated`` so users know where to authenticate for future 715 requests. 716 """ 717 if response.status_int == 401: 718 response.headers.extend(self._reject_auth_headers) 719 720 return response 721 722 @property 723 def _reject_auth_headers(self): 724 header_val = 'Keystone uri="%s"' % self._www_authenticate_uri 725 return [('WWW-Authenticate', header_val)] 726 727 def fetch_token(self, token, allow_expired=False): 728 """Retrieve a token from either a PKI bundle or the identity server. 729 730 :param str token: token id 731 732 :raises exc.InvalidToken: if token is rejected 733 """ 734 data = None 735 try: 736 cached = self._token_cache.get(token) 737 738 if cached: 739 if cached == _CACHE_INVALID_INDICATOR: 740 self.log.debug('Cached token is marked unauthorized') 741 raise ksm_exceptions.InvalidToken() 742 743 # NOTE(jamielennox): Cached values used to be stored as a tuple 744 # of data and expiry time. They no longer are but we have to 745 # allow some time to transition the old format so if it's a 746 # tuple just use the data. 747 if len(cached) == 2: 748 cached = cached[0] 749 750 data = cached 751 else: 752 data = self._identity_server.verify_token( 753 token, 754 allow_expired=allow_expired) 755 756 self._token_cache.set(token, data) 757 758 except (ksa_exceptions.ConnectFailure, 759 ksa_exceptions.DiscoveryFailure, 760 ksa_exceptions.RequestTimeout, 761 ksm_exceptions.ServiceError) as e: 762 self.log.critical('Unable to validate token: %s', e) 763 if self._delay_auth_decision: 764 self.log.debug('Keystone unavailable; marking token as ' 765 'invalid and deferring auth decision.') 766 raise ksm_exceptions.InvalidToken( 767 'Keystone unavailable: %s' % e) 768 raise webob.exc.HTTPServiceUnavailable( 769 'The Keystone service is temporarily unavailable.') 770 except ksm_exceptions.InvalidToken: 771 self.log.debug('Token validation failure.', exc_info=True) 772 self._token_cache.set(token, _CACHE_INVALID_INDICATOR) 773 self.log.warning('Authorization failed for token') 774 raise 775 except ksa_exceptions.EndpointNotFound: 776 # Invalidate auth in adapter for identity endpoint update 777 self._identity_server.invalidate() 778 raise 779 780 return data 781 782 def _validate_token(self, auth_ref, **kwargs): 783 super(AuthProtocol, self)._validate_token(auth_ref, **kwargs) 784 785 if auth_ref.version == 'v2.0' and not auth_ref.project_id: 786 msg = _('Unable to determine service tenancy.') 787 raise ksm_exceptions.InvalidToken(msg) 788 789 def _create_auth_plugin(self): 790 # NOTE(jamielennox): Ideally this would use load_from_conf_options 791 # however that is not possible because we have to support the override 792 # pattern we use in _conf.get. This function therefore does a manual 793 # version of load_from_conf_options with the fallback plugin inline. 794 795 group = self._conf.get('auth_section') or _base.AUTHTOKEN_GROUP 796 797 # NOTE(jamielennox): auth_plugin was deprecated to auth_type. _conf.get 798 # doesn't handle that deprecation in the case of conf dict options so 799 # we have to manually check the value 800 plugin_name = (self._conf.get('auth_type', group=group) 801 or self._conf.paste_overrides.get('auth_plugin')) 802 803 if not plugin_name: 804 return _auth.AuthTokenPlugin( 805 log=self.log, 806 auth_admin_prefix=self._conf.get('auth_admin_prefix', 807 group=group), 808 auth_host=self._conf.get('auth_host', group=group), 809 auth_port=self._conf.get('auth_port', group=group), 810 auth_protocol=self._conf.get('auth_protocol', group=group), 811 identity_uri=self._conf.get('identity_uri', group=group), 812 admin_token=self._conf.get('admin_token', group=group), 813 admin_user=self._conf.get('admin_user', group=group), 814 admin_password=self._conf.get('admin_password', group=group), 815 admin_tenant_name=self._conf.get('admin_tenant_name', 816 group=group) 817 ) 818 819 # Plugin option registration is normally done as part of the load_from 820 # function rather than the register function so copy here. 821 plugin_loader = loading.get_plugin_loader(plugin_name) 822 plugin_opts = loading.get_auth_plugin_conf_options(plugin_loader) 823 824 self._conf.oslo_conf_obj.register_opts(plugin_opts, group=group) 825 getter = lambda opt: self._conf.get(opt.dest, group=group) # noqa 826 return plugin_loader.load_from_options_getter(getter) 827 828 def _create_session(self, **kwargs): 829 # NOTE(jamielennox): Loading Session here should be exactly the 830 # same as calling Session.load_from_conf_options(CONF, GROUP) 831 # however we can't do that because we have to use _conf.get to 832 # support the paste.ini options. 833 kwargs.setdefault('cert', self._conf.get('certfile')) 834 kwargs.setdefault('key', self._conf.get('keyfile')) 835 kwargs.setdefault('cacert', self._conf.get('cafile')) 836 kwargs.setdefault('insecure', self._conf.get('insecure')) 837 kwargs.setdefault('timeout', self._conf.get('http_connect_timeout')) 838 kwargs.setdefault('user_agent', self._conf.user_agent) 839 840 return session_loading.Session().load_from_options(**kwargs) 841 842 def _create_identity_server(self): 843 adap = adapter.Adapter( 844 self._session, 845 auth=self._auth, 846 service_type='identity', 847 interface=self._interface, 848 region_name=self._conf.get('region_name'), 849 connect_retries=self._conf.get('http_request_max_retries')) 850 851 auth_version = self._conf.get('auth_version') 852 if auth_version is not None: 853 auth_version = discover.normalize_version_number(auth_version) 854 return _identity.IdentityServer( 855 self.log, 856 adap, 857 include_service_catalog=self._include_service_catalog, 858 requested_auth_version=auth_version, 859 requested_auth_interface=self._interface) 860 861 def _create_oslo_cache(self): 862 # having this as a function makes test mocking easier 863 region = oslo_cache.create_region() 864 oslo_cache.configure_cache_region(self._conf.oslo_conf_obj, region) 865 return region 866 867 def _token_cache_factory(self): 868 869 security_strategy = self._conf.get('memcache_security_strategy') 870 871 cache_kwargs = dict( 872 cache_time=int(self._conf.get('token_cache_time')), 873 env_cache_name=self._conf.get('cache'), 874 memcached_servers=self._conf.get('memcached_servers'), 875 use_advanced_pool=self._conf.get('memcache_use_advanced_pool'), 876 dead_retry=self._conf.get('memcache_pool_dead_retry'), 877 maxsize=self._conf.get('memcache_pool_maxsize'), 878 unused_timeout=self._conf.get('memcache_pool_unused_timeout'), 879 conn_get_timeout=self._conf.get('memcache_pool_conn_get_timeout'), 880 socket_timeout=self._conf.get('memcache_pool_socket_timeout'), 881 ) 882 883 if security_strategy.lower() != 'none': 884 secret_key = self._conf.get('memcache_secret_key') 885 return _cache.SecureTokenCache(self.log, 886 security_strategy, 887 secret_key, 888 **cache_kwargs) 889 else: 890 return _cache.TokenCache(self.log, **cache_kwargs) 891 892 893def filter_factory(global_conf, **local_conf): 894 """Return a WSGI filter app for use with paste.deploy.""" 895 conf = global_conf.copy() 896 conf.update(local_conf) 897 898 def auth_filter(app): 899 return AuthProtocol(app, conf) 900 return auth_filter 901 902 903def app_factory(global_conf, **local_conf): 904 conf = global_conf.copy() 905 conf.update(local_conf) 906 return AuthProtocol(None, conf) 907 908 909# NOTE(jamielennox): Maintained here for public API compatibility. 910InvalidToken = ksm_exceptions.InvalidToken 911ServiceError = ksm_exceptions.ServiceError 912ConfigurationError = ksm_exceptions.ConfigurationError 913