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