1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import argparse
14import functools
15import hashlib
16import logging
17import os
18import socket
19import time
20import warnings
21
22from debtcollector import removals
23from oslo_config import cfg
24from oslo_serialization import jsonutils
25from oslo_utils import encodeutils
26from oslo_utils import importutils
27from oslo_utils import strutils
28import requests
29import six
30from six.moves import urllib
31
32from keystoneclient import exceptions
33from keystoneclient.i18n import _
34
35osprofiler_web = importutils.try_import("osprofiler.web")
36
37USER_AGENT = 'python-keystoneclient'
38
39# NOTE(jamielennox): Clients will likely want to print more than json. Please
40# propose a patch if you have a content type you think is reasonable to print
41# here and we'll add it to the list as required.
42_LOG_CONTENT_TYPES = set(['application/json'])
43
44_logger = logging.getLogger(__name__)
45
46
47def _positive_non_zero_float(argument_value):
48    if argument_value is None:
49        return None
50    try:
51        value = float(argument_value)
52    except ValueError:
53        msg = _("%s must be a float") % argument_value
54        raise argparse.ArgumentTypeError(msg)
55    if value <= 0:
56        msg = _("%s must be greater than 0") % argument_value
57        raise argparse.ArgumentTypeError(msg)
58    return value
59
60
61def request(url, method='GET', **kwargs):
62    return Session().request(url, method=method, **kwargs)
63
64
65def _remove_service_catalog(body):
66    try:
67        data = jsonutils.loads(body)
68
69        # V3 token
70        if 'token' in data and 'catalog' in data['token']:
71            data['token']['catalog'] = '<removed>'
72            return jsonutils.dumps(data)
73
74        # V2 token
75        if 'serviceCatalog' in data['access']:
76            data['access']['serviceCatalog'] = '<removed>'
77            return jsonutils.dumps(data)
78
79    except Exception:  # nosec(cjschaef): multiple exceptions can be raised
80        # Don't fail trying to clean up the request body.
81        pass
82    return body
83
84
85class Session(object):
86    """Maintains client communication state and common functionality.
87
88    As much as possible the parameters to this class reflect and are passed
89    directly to the requests library.
90
91    :param auth: An authentication plugin to authenticate the session with.
92                 (optional, defaults to None)
93    :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
94    :param requests.Session session: A requests session object that can be used
95                                     for issuing requests. (optional)
96    :param string original_ip: The original IP of the requesting user which
97                               will be sent to identity service in a
98                               'Forwarded' header. (optional)
99    :param verify: The verification arguments to pass to requests. These are of
100                   the same form as requests expects, so True or False to
101                   verify (or not) against system certificates or a path to a
102                   bundle or CA certs to check against or None for requests to
103                   attempt to locate and use certificates. (optional, defaults
104                   to True)
105    :param cert: A client certificate to pass to requests. These are of the
106                 same form as requests expects. Either a single filename
107                 containing both the certificate and key or a tuple containing
108                 the path to the certificate then a path to the key. (optional)
109    :param float timeout: A timeout to pass to requests. This should be a
110                          numerical value indicating some amount (or fraction)
111                          of seconds or 0 for no timeout. (optional, defaults
112                          to 0)
113    :param string user_agent: A User-Agent header string to use for the
114                              request. If not provided a default is used.
115                              (optional, defaults to 'python-keystoneclient')
116    :param int/bool redirect: Controls the maximum number of redirections that
117                              can be followed by a request. Either an integer
118                              for a specific count or True/False for
119                              forever/never. (optional, default to 30)
120    """
121
122    user_agent = None
123
124    _REDIRECT_STATUSES = (301, 302, 303, 305, 307)
125
126    REDIRECT_STATUSES = _REDIRECT_STATUSES
127    """This property is deprecated as of the 1.7.0 release and may be removed
128       in the 2.0.0 release."""
129
130    _DEFAULT_REDIRECT_LIMIT = 30
131
132    DEFAULT_REDIRECT_LIMIT = _DEFAULT_REDIRECT_LIMIT
133    """This property is deprecated as of the 1.7.0 release and may be removed
134       in the 2.0.0 release."""
135
136    def __init__(self, auth=None, session=None, original_ip=None, verify=True,
137                 cert=None, timeout=None, user_agent=None,
138                 redirect=_DEFAULT_REDIRECT_LIMIT):
139        warnings.warn(
140            'keystoneclient.session.Session is deprecated as of the 2.1.0 '
141            'release in favor of keystoneauth1.session.Session. It will be '
142            'removed in future releases.',
143            DeprecationWarning)
144
145        if not session:
146            session = requests.Session()
147            # Use TCPKeepAliveAdapter to fix bug 1323862
148            for scheme in list(session.adapters):
149                session.mount(scheme, TCPKeepAliveAdapter())
150
151        self.auth = auth
152        self.session = session
153        self.original_ip = original_ip
154        self.verify = verify
155        self.cert = cert
156        self.timeout = None
157        self.redirect = redirect
158
159        if timeout is not None:
160            self.timeout = float(timeout)
161
162        # don't override the class variable if none provided
163        if user_agent is not None:
164            self.user_agent = user_agent
165
166    @staticmethod
167    def _process_header(header):
168        """Redact the secure headers to be logged."""
169        secure_headers = ('authorization', 'x-auth-token',
170                          'x-subject-token', 'x-service-token')
171        if header[0].lower() in secure_headers:
172            # hashlib.sha1() bandit nosec, as it is HMAC-SHA1 in
173            # keystone, which is considered secure (unlike just sha1)
174            token_hasher = hashlib.sha1()  # nosec(lhinds)
175            token_hasher.update(header[1].encode('utf-8'))
176            token_hash = token_hasher.hexdigest()
177            return (header[0], '{SHA1}%s' % token_hash)
178        return header
179
180    def _http_log_request(self, url, method=None, data=None,
181                          headers=None, logger=_logger):
182        if not logger.isEnabledFor(logging.DEBUG):
183            # NOTE(morganfainberg): This whole debug section is expensive,
184            # there is no need to do the work if we're not going to emit a
185            # debug log.
186            return
187
188        string_parts = ['REQ: curl -g -i']
189
190        # NOTE(jamielennox): None means let requests do its default validation
191        # so we need to actually check that this is False.
192        if self.verify is False:
193            string_parts.append('--insecure')
194        elif isinstance(self.verify, six.string_types):
195            string_parts.append('--cacert "%s"' % self.verify)
196
197        if method:
198            string_parts.extend(['-X', method])
199
200        string_parts.append(url)
201
202        if headers:
203            for header in headers.items():
204                string_parts.append('-H "%s: %s"'
205                                    % self._process_header(header))
206
207        if data:
208            if isinstance(data, six.binary_type):
209                try:
210                    data = data.decode("ascii")
211                except UnicodeDecodeError:
212                    data = "<binary_data>"
213            string_parts.append("-d '%s'" % data)
214        try:
215            logger.debug(' '.join(string_parts))
216        except UnicodeDecodeError:
217            logger.debug("Replaced characters that could not be decoded"
218                         " in log output, original caused UnicodeDecodeError")
219            string_parts = [
220                encodeutils.safe_decode(
221                    part, errors='replace') for part in string_parts]
222            logger.debug(' '.join(string_parts))
223
224    def _http_log_response(self, response, logger):
225        if not logger.isEnabledFor(logging.DEBUG):
226            return
227
228        # NOTE(samueldmq): If the response does not provide enough info about
229        # the content type to decide whether it is useful and safe to log it
230        # or not, just do not log the body. Trying to# read the response body
231        # anyways may result on reading a long stream of bytes and getting an
232        # unexpected MemoryError. See bug 1616105 for further details.
233        content_type = response.headers.get('content-type', None)
234
235        # NOTE(lamt): Per [1], the Content-Type header can be of the form
236        # Content-Type := type "/" subtype *[";" parameter]
237        # [1] https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
238        for log_type in _LOG_CONTENT_TYPES:
239            if content_type is not None and content_type.startswith(log_type):
240                text = _remove_service_catalog(response.text)
241                break
242        else:
243            text = ('Omitted, Content-Type is set to %s. Only '
244                    '%s responses have their bodies logged.')
245            text = text % (content_type, ', '.join(_LOG_CONTENT_TYPES))
246
247        string_parts = [
248            'RESP:',
249            '[%s]' % response.status_code
250        ]
251        for header in response.headers.items():
252            string_parts.append('%s: %s' % self._process_header(header))
253        string_parts.append('\nRESP BODY: %s\n' % strutils.mask_password(text))
254
255        logger.debug(' '.join(string_parts))
256
257    # NOTE(artmr): parameter 'original_ip' value is never used
258    def request(self, url, method, json=None, original_ip=None,
259                user_agent=None, redirect=None, authenticated=None,
260                endpoint_filter=None, auth=None, requests_auth=None,
261                raise_exc=True, allow_reauth=True, log=True,
262                endpoint_override=None, connect_retries=0, logger=_logger,
263                **kwargs):
264        """Send an HTTP request with the specified characteristics.
265
266        Wrapper around `requests.Session.request` to handle tasks such as
267        setting headers, JSON encoding/decoding, and error handling.
268
269        Arguments that are not handled are passed through to the requests
270        library.
271
272        :param string url: Path or fully qualified URL of HTTP request. If only
273                           a path is provided then endpoint_filter must also be
274                           provided such that the base URL can be determined.
275                           If a fully qualified URL is provided then
276                           endpoint_filter will be ignored.
277        :param string method: The http method to use. (e.g. 'GET', 'POST')
278        :param string original_ip: Mark this request as forwarded for this ip.
279                                   (optional)
280        :param dict headers: Headers to be included in the request. (optional)
281        :param json: Some data to be represented as JSON. (optional)
282        :param string user_agent: A user_agent to use for the request. If
283                                  present will override one present in headers.
284                                  (optional)
285        :param int/bool redirect: the maximum number of redirections that
286                                  can be followed by a request. Either an
287                                  integer for a specific count or True/False
288                                  for forever/never. (optional)
289        :param int connect_retries: the maximum number of retries that should
290                                    be attempted for connection errors.
291                                    (optional, defaults to 0 - never retry).
292        :param bool authenticated: True if a token should be attached to this
293                                   request, False if not or None for attach if
294                                   an auth_plugin is available.
295                                   (optional, defaults to None)
296        :param dict endpoint_filter: Data to be provided to an auth plugin with
297                                     which it should be able to determine an
298                                     endpoint to use for this request. If not
299                                     provided then URL is expected to be a
300                                     fully qualified URL. (optional)
301        :param str endpoint_override: The URL to use instead of looking up the
302                                      endpoint in the auth plugin. This will be
303                                      ignored if a fully qualified URL is
304                                      provided but take priority over an
305                                      endpoint_filter. (optional)
306        :param auth: The auth plugin to use when authenticating this request.
307                     This will override the plugin that is attached to the
308                     session (if any). (optional)
309        :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
310        :param requests_auth: A requests library auth plugin that cannot be
311                              passed via kwarg because the `auth` kwarg
312                              collides with our own auth plugins. (optional)
313        :type requests_auth: :py:class:`requests.auth.AuthBase`
314        :param bool raise_exc: If True then raise an appropriate exception for
315                               failed HTTP requests. If False then return the
316                               request object. (optional, default True)
317        :param bool allow_reauth: Allow fetching a new token and retrying the
318                                  request on receiving a 401 Unauthorized
319                                  response. (optional, default True)
320        :param bool log: If True then log the request and response data to the
321                         debug log. (optional, default True)
322        :param logger: The logger object to use to log request and responses.
323                       If not provided the keystoneclient.session default
324                       logger will be used.
325        :type logger: logging.Logger
326        :param kwargs: any other parameter that can be passed to
327                       requests.Session.request (such as `headers`). Except:
328                       'data' will be overwritten by the data in 'json' param.
329                       'allow_redirects' is ignored as redirects are handled
330                       by the session.
331
332        :raises keystoneclient.exceptions.ClientException: For connection
333            failure, or to indicate an error response code.
334
335        :returns: The response to the request.
336        """
337        headers = kwargs.setdefault('headers', dict())
338
339        if authenticated is None:
340            authenticated = bool(auth or self.auth)
341
342        if authenticated:
343            auth_headers = self.get_auth_headers(auth)
344
345            if auth_headers is None:
346                msg = _('No valid authentication is available')
347                raise exceptions.AuthorizationFailure(msg)
348
349            headers.update(auth_headers)
350
351        if osprofiler_web:
352            headers.update(osprofiler_web.get_trace_id_headers())
353
354        # if we are passed a fully qualified URL and an endpoint_filter we
355        # should ignore the filter. This will make it easier for clients who
356        # want to overrule the default endpoint_filter data added to all client
357        # requests. We check fully qualified here by the presence of a host.
358        if not urllib.parse.urlparse(url).netloc:
359            base_url = None
360
361            if endpoint_override:
362                base_url = endpoint_override
363            elif endpoint_filter:
364                base_url = self.get_endpoint(auth, **endpoint_filter)
365
366            if not base_url:
367                service_type = (endpoint_filter or {}).get('service_type',
368                                                           'unknown')
369                msg = _('Endpoint for %s service') % service_type
370                raise exceptions.EndpointNotFound(msg)
371
372            url = '%s/%s' % (base_url.rstrip('/'), url.lstrip('/'))
373
374        if self.cert:
375            kwargs.setdefault('cert', self.cert)
376
377        if self.timeout is not None:
378            kwargs.setdefault('timeout', self.timeout)
379
380        if user_agent:
381            headers['User-Agent'] = user_agent
382        elif self.user_agent:
383            user_agent = headers.setdefault('User-Agent', self.user_agent)
384        else:
385            user_agent = headers.setdefault('User-Agent', USER_AGENT)
386
387        if self.original_ip:
388            headers.setdefault('Forwarded',
389                               'for=%s;by=%s' % (self.original_ip, user_agent))
390
391        if json is not None:
392            headers['Content-Type'] = 'application/json'
393            kwargs['data'] = jsonutils.dumps(json)
394
395        kwargs.setdefault('verify', self.verify)
396
397        if requests_auth:
398            kwargs['auth'] = requests_auth
399
400        if log:
401            self._http_log_request(url, method=method,
402                                   data=kwargs.get('data'),
403                                   headers=headers,
404                                   logger=logger)
405
406        # Force disable requests redirect handling. We will manage this below.
407        kwargs['allow_redirects'] = False
408
409        if redirect is None:
410            redirect = self.redirect
411
412        send = functools.partial(self._send_request,
413                                 url, method, redirect, log, logger,
414                                 connect_retries)
415
416        try:
417            connection_params = self.get_auth_connection_params(auth=auth)
418        except exceptions.MissingAuthPlugin:  # nosec(cjschaef)
419            # NOTE(jamielennox): If we've gotten this far without an auth
420            # plugin then we should be happy with allowing no additional
421            # connection params. This will be the typical case for plugins
422            # anyway.
423            pass
424        else:
425            if connection_params:
426                kwargs.update(connection_params)
427
428        resp = send(**kwargs)
429
430        # handle getting a 401 Unauthorized response by invalidating the plugin
431        # and then retrying the request. This is only tried once.
432        if resp.status_code == 401 and authenticated and allow_reauth:
433            if self.invalidate(auth):
434                auth_headers = self.get_auth_headers(auth)
435
436                if auth_headers is not None:
437                    headers.update(auth_headers)
438                    resp = send(**kwargs)
439
440        if raise_exc and resp.status_code >= 400:
441            logger.debug('Request returned failure status: %s',
442                         resp.status_code)
443            raise exceptions.from_response(resp, method, url)
444
445        return resp
446
447    def _send_request(self, url, method, redirect, log, logger,
448                      connect_retries, connect_retry_delay=0.5, **kwargs):
449        # NOTE(jamielennox): We handle redirection manually because the
450        # requests lib follows some browser patterns where it will redirect
451        # POSTs as GETs for certain statuses which is not want we want for an
452        # API. See: https://en.wikipedia.org/wiki/Post/Redirect/Get
453
454        # NOTE(jamielennox): The interaction between retries and redirects are
455        # handled naively. We will attempt only a maximum number of retries and
456        # redirects rather than per request limits. Otherwise the extreme case
457        # could be redirects * retries requests. This will be sufficient in
458        # most cases and can be fixed properly if there's ever a need.
459
460        try:
461            try:
462                resp = self.session.request(method, url, **kwargs)
463            except requests.exceptions.SSLError as e:
464                msg = _('SSL exception connecting to %(url)s: '
465                        '%(error)s') % {'url': url, 'error': e}
466                raise exceptions.SSLError(msg)
467            except requests.exceptions.Timeout:
468                msg = _('Request to %s timed out') % url
469                raise exceptions.RequestTimeout(msg)
470            except requests.exceptions.ConnectionError:
471                msg = _('Unable to establish connection to %s') % url
472                raise exceptions.ConnectionRefused(msg)
473        except (exceptions.RequestTimeout, exceptions.ConnectionRefused) as e:
474            if connect_retries <= 0:
475                raise
476
477            logger.info('Failure: %(e)s. Retrying in %(delay).1fs.',
478                        {'e': e, 'delay': connect_retry_delay})
479            time.sleep(connect_retry_delay)
480
481            return self._send_request(
482                url, method, redirect, log, logger,
483                connect_retries=connect_retries - 1,
484                connect_retry_delay=connect_retry_delay * 2,
485                **kwargs)
486
487        if log:
488            self._http_log_response(resp, logger)
489
490        if resp.status_code in self._REDIRECT_STATUSES:
491            # be careful here in python True == 1 and False == 0
492            if isinstance(redirect, bool):
493                redirect_allowed = redirect
494            else:
495                redirect -= 1
496                redirect_allowed = redirect >= 0
497
498            if not redirect_allowed:
499                return resp
500
501            try:
502                location = resp.headers['location']
503            except KeyError:
504                logger.warning("Failed to redirect request to %s as new "
505                               "location was not provided.", resp.url)
506            else:
507                # NOTE(jamielennox): We don't pass through connect_retry_delay.
508                # This request actually worked so we can reset the delay count.
509                new_resp = self._send_request(
510                    location, method, redirect, log, logger,
511                    connect_retries=connect_retries,
512                    **kwargs)
513
514                if not isinstance(new_resp.history, list):
515                    new_resp.history = list(new_resp.history)
516                new_resp.history.insert(0, resp)
517                resp = new_resp
518
519        return resp
520
521    def head(self, url, **kwargs):
522        """Perform a HEAD request.
523
524        This calls :py:meth:`.request()` with ``method`` set to ``HEAD``.
525
526        """
527        return self.request(url, 'HEAD', **kwargs)
528
529    def get(self, url, **kwargs):
530        """Perform a GET request.
531
532        This calls :py:meth:`.request()` with ``method`` set to ``GET``.
533
534        """
535        return self.request(url, 'GET', **kwargs)
536
537    def post(self, url, **kwargs):
538        """Perform a POST request.
539
540        This calls :py:meth:`.request()` with ``method`` set to ``POST``.
541
542        """
543        return self.request(url, 'POST', **kwargs)
544
545    def put(self, url, **kwargs):
546        """Perform a PUT request.
547
548        This calls :py:meth:`.request()` with ``method`` set to ``PUT``.
549
550        """
551        return self.request(url, 'PUT', **kwargs)
552
553    def delete(self, url, **kwargs):
554        """Perform a DELETE request.
555
556        This calls :py:meth:`.request()` with ``method`` set to ``DELETE``.
557
558        """
559        return self.request(url, 'DELETE', **kwargs)
560
561    def patch(self, url, **kwargs):
562        """Perform a PATCH request.
563
564        This calls :py:meth:`.request()` with ``method`` set to ``PATCH``.
565
566        """
567        return self.request(url, 'PATCH', **kwargs)
568
569    @classmethod
570    def construct(cls, kwargs):
571        """Handle constructing a session from both old and new arguments.
572
573        Support constructing a session from the old
574        :py:class:`~keystoneclient.httpclient.HTTPClient` args as well as the
575        new request-style arguments.
576
577        .. warning::
578
579            *DEPRECATED as of 1.7.0*: This function is purely for bridging the
580            gap between older client arguments and the session arguments that
581            they relate to. It is not intended to be used as a generic Session
582            Factory. This function may be removed in the 2.0.0 release.
583
584        This function purposefully modifies the input kwargs dictionary so that
585        the remaining kwargs dict can be reused and passed on to other
586        functions without session arguments.
587
588        """
589        warnings.warn(
590            'Session.construct() is deprecated as of the 1.7.0 release  in '
591            'favor of using session constructor and may be removed in the '
592            '2.0.0 release.', DeprecationWarning)
593        return cls._construct(kwargs)
594
595    @classmethod
596    def _construct(cls, kwargs):
597        params = {}
598
599        for attr in ('verify', 'cacert', 'cert', 'key', 'insecure',
600                     'timeout', 'session', 'original_ip', 'user_agent'):
601            try:
602                params[attr] = kwargs.pop(attr)
603            except KeyError:  # nosec(cjschaef): we are brute force
604                # identifying possible attributes for kwargs
605                pass
606
607        return cls._make(**params)
608
609    @classmethod
610    def _make(cls, insecure=False, verify=None, cacert=None, cert=None,
611              key=None, **kwargs):
612        """Create a session with individual certificate parameters.
613
614        Some parameters used to create a session don't lend themselves to be
615        loaded from config/CLI etc. Create a session by converting those
616        parameters into session __init__ parameters.
617        """
618        if verify is None:
619            if insecure:
620                verify = False
621            else:
622                verify = cacert or True
623
624        if cert and key:
625            warnings.warn(
626                'Passing cert and key together is deprecated as of the 1.7.0 '
627                'release in favor of the requests library form of having the '
628                'cert and key as a tuple and may be removed in the 2.0.0 '
629                'release.', DeprecationWarning)
630            cert = (cert, key)
631
632        return cls(verify=verify, cert=cert, **kwargs)
633
634    def _auth_required(self, auth, msg):
635        if not auth:
636            auth = self.auth
637
638        if not auth:
639            raise exceptions.MissingAuthPlugin(msg)
640
641        return auth
642
643    def get_auth_headers(self, auth=None, **kwargs):
644        """Return auth headers as provided by the auth plugin.
645
646        :param auth: The auth plugin to use for token. Overrides the plugin
647                     on the session. (optional)
648        :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
649
650        :raises keystoneclient.exceptions.AuthorizationFailure: if a new token
651                                                                fetch fails.
652        :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
653                                                             available.
654
655        :returns: Authentication headers or None for failure.
656        :rtype: dict
657        """
658        msg = _('An auth plugin is required to fetch a token')
659        auth = self._auth_required(auth, msg)
660        return auth.get_headers(self, **kwargs)
661
662    @removals.remove(message='Use get_auth_headers instead.', version='1.7.0',
663                     removal_version='2.0.0')
664    def get_token(self, auth=None):
665        """Return a token as provided by the auth plugin.
666
667        :param auth: The auth plugin to use for token. Overrides the plugin
668                     on the session. (optional)
669        :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
670
671        :raises keystoneclient.exceptions.AuthorizationFailure: if a new token
672                                                                fetch fails.
673        :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
674                                                             available.
675
676        .. warning::
677
678             This method is deprecated as of the 1.7.0 release in favor of
679             :meth:`get_auth_headers` and may be removed in the 2.0.0 release.
680             This method assumes that the only header that is used to
681             authenticate a message is 'X-Auth-Token' which may not be correct.
682
683        :returns: A valid token.
684        :rtype: string
685        """
686        return (self.get_auth_headers(auth) or {}).get('X-Auth-Token')
687
688    def get_endpoint(self, auth=None, **kwargs):
689        """Get an endpoint as provided by the auth plugin.
690
691        :param auth: The auth plugin to use for token. Overrides the plugin on
692                     the session. (optional)
693        :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
694
695        :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
696                                                             available.
697
698        :returns: An endpoint if available or None.
699        :rtype: string
700        """
701        msg = _('An auth plugin is required to determine endpoint URL')
702        auth = self._auth_required(auth, msg)
703        return auth.get_endpoint(self, **kwargs)
704
705    def get_auth_connection_params(self, auth=None, **kwargs):
706        """Return auth connection params as provided by the auth plugin.
707
708        An auth plugin may specify connection parameters to the request like
709        providing a client certificate for communication.
710
711        We restrict the values that may be returned from this function to
712        prevent an auth plugin overriding values unrelated to connection
713        parameters. The values that are currently accepted are:
714
715        - `cert`: a path to a client certificate, or tuple of client
716          certificate and key pair that are used with this request.
717        - `verify`: a boolean value to indicate verifying SSL certificates
718          against the system CAs or a path to a CA file to verify with.
719
720        These values are passed to the requests library and further information
721        on accepted values may be found there.
722
723        :param auth: The auth plugin to use for tokens. Overrides the plugin
724                     on the session. (optional)
725        :type auth: keystoneclient.auth.base.BaseAuthPlugin
726
727        :raises keystoneclient.exceptions.AuthorizationFailure: if a new token
728                                                                fetch fails.
729        :raises keystoneclient.exceptions.MissingAuthPlugin: if a plugin is not
730                                                             available.
731        :raises keystoneclient.exceptions.UnsupportedParameters: if the plugin
732            returns a parameter that is not supported by this session.
733
734        :returns: Authentication headers or None for failure.
735        :rtype: dict
736        """
737        msg = _('An auth plugin is required to fetch connection params')
738        auth = self._auth_required(auth, msg)
739        params = auth.get_connection_params(self, **kwargs)
740
741        # NOTE(jamielennox): There needs to be some consensus on what
742        # parameters are allowed to be modified by the auth plugin here.
743        # Ideally I think it would be only the send() parts of the request
744        # flow. For now lets just allow certain elements.
745        params_copy = params.copy()
746
747        for arg in ('cert', 'verify'):
748            try:
749                kwargs[arg] = params_copy.pop(arg)
750            except KeyError:  # nosec(cjschaef): we are brute force
751                # identifying and removing values in params_copy
752                pass
753
754        if params_copy:
755            raise exceptions.UnsupportedParameters(list(params_copy))
756
757        return params
758
759    def invalidate(self, auth=None):
760        """Invalidate an authentication plugin.
761
762        :param auth: The auth plugin to invalidate. Overrides the plugin on the
763                     session. (optional)
764        :type auth: :py:class:`keystoneclient.auth.base.BaseAuthPlugin`
765
766        """
767        msg = _('An auth plugin is required to validate')
768        auth = self._auth_required(auth, msg)
769        return auth.invalidate()
770
771    def get_user_id(self, auth=None):
772        """Return the authenticated user_id as provided by the auth plugin.
773
774        :param auth: The auth plugin to use for token. Overrides the plugin
775                     on the session. (optional)
776        :type auth: keystoneclient.auth.base.BaseAuthPlugin
777
778        :raises keystoneclient.exceptions.AuthorizationFailure:
779            if a new token fetch fails.
780        :raises keystoneclient.exceptions.MissingAuthPlugin:
781            if a plugin is not available.
782
783        :returns string: Current user_id or None if not supported by plugin.
784        """
785        msg = _('An auth plugin is required to get user_id')
786        auth = self._auth_required(auth, msg)
787        return auth.get_user_id(self)
788
789    def get_project_id(self, auth=None):
790        """Return the authenticated project_id as provided by the auth plugin.
791
792        :param auth: The auth plugin to use for token. Overrides the plugin
793                     on the session. (optional)
794        :type auth: keystoneclient.auth.base.BaseAuthPlugin
795
796        :raises keystoneclient.exceptions.AuthorizationFailure:
797            if a new token fetch fails.
798        :raises keystoneclient.exceptions.MissingAuthPlugin:
799            if a plugin is not available.
800
801        :returns string: Current project_id or None if not supported by plugin.
802        """
803        msg = _('An auth plugin is required to get project_id')
804        auth = self._auth_required(auth, msg)
805        return auth.get_project_id(self)
806
807    @classmethod
808    def get_conf_options(cls, deprecated_opts=None):
809        """Get oslo_config options that are needed for a :py:class:`.Session`.
810
811        These may be useful without being registered for config file generation
812        or to manipulate the options before registering them yourself.
813
814        The options that are set are:
815            :cafile: The certificate authority filename.
816            :certfile: The client certificate file to present.
817            :keyfile: The key for the client certificate.
818            :insecure: Whether to ignore SSL verification.
819            :timeout: The max time to wait for HTTP connections.
820
821        :param dict deprecated_opts: Deprecated options that should be included
822             in the definition of new options. This should be a dict from the
823             name of the new option to a list of oslo.DeprecatedOpts that
824             correspond to the new option. (optional)
825
826             For example, to support the ``ca_file`` option pointing to the new
827             ``cafile`` option name::
828
829                 old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group')
830                 deprecated_opts={'cafile': [old_opt]}
831
832        :returns: A list of oslo_config options.
833        """
834        if deprecated_opts is None:
835            deprecated_opts = {}
836
837        return [cfg.StrOpt('cafile',
838                           deprecated_opts=deprecated_opts.get('cafile'),
839                           help='PEM encoded Certificate Authority to use '
840                                'when verifying HTTPs connections.'),
841                cfg.StrOpt('certfile',
842                           deprecated_opts=deprecated_opts.get('certfile'),
843                           help='PEM encoded client certificate cert file'),
844                cfg.StrOpt('keyfile',
845                           deprecated_opts=deprecated_opts.get('keyfile'),
846                           help='PEM encoded client certificate key file'),
847                cfg.BoolOpt('insecure',
848                            default=False,
849                            deprecated_opts=deprecated_opts.get('insecure'),
850                            help='Verify HTTPS connections.'),
851                cfg.IntOpt('timeout',
852                           deprecated_opts=deprecated_opts.get('timeout'),
853                           help='Timeout value for http requests'),
854                ]
855
856    @classmethod
857    def register_conf_options(cls, conf, group, deprecated_opts=None):
858        """Register the oslo_config options that are needed for a session.
859
860        The options that are set are:
861            :cafile: The certificate authority filename.
862            :certfile: The client certificate file to present.
863            :keyfile: The key for the client certificate.
864            :insecure: Whether to ignore SSL verification.
865            :timeout: The max time to wait for HTTP connections.
866
867        :param oslo_config.Cfg conf: config object to register with.
868        :param string group: The ini group to register options in.
869        :param dict deprecated_opts: Deprecated options that should be included
870             in the definition of new options. This should be a dict from the
871             name of the new option to a list of oslo.DeprecatedOpts that
872             correspond to the new option. (optional)
873
874             For example, to support the ``ca_file`` option pointing to the new
875             ``cafile`` option name::
876
877                 old_opt = oslo_cfg.DeprecatedOpt('ca_file', 'old_group')
878                 deprecated_opts={'cafile': [old_opt]}
879
880        :returns: The list of options that was registered.
881        """
882        opts = cls.get_conf_options(deprecated_opts=deprecated_opts)
883        conf.register_group(cfg.OptGroup(group))
884        conf.register_opts(opts, group=group)
885        return opts
886
887    @classmethod
888    def load_from_conf_options(cls, conf, group, **kwargs):
889        """Create a session object from an oslo_config object.
890
891        The options must have been previously registered with
892        register_conf_options.
893
894        :param oslo_config.Cfg conf: config object to register with.
895        :param string group: The ini group to register options in.
896        :param dict kwargs: Additional parameters to pass to session
897                            construction.
898        :returns: A new session object.
899        :rtype: :py:class:`.Session`
900        """
901        c = conf[group]
902
903        kwargs['insecure'] = c.insecure
904        kwargs['cacert'] = c.cafile
905        if c.certfile and c.keyfile:
906            kwargs['cert'] = (c.certfile, c.keyfile)
907        kwargs['timeout'] = c.timeout
908
909        return cls._make(**kwargs)
910
911    @staticmethod
912    def register_cli_options(parser):
913        """Register the argparse arguments that are needed for a session.
914
915        :param argparse.ArgumentParser parser: parser to add to.
916        """
917        parser.add_argument('--insecure',
918                            default=False,
919                            action='store_true',
920                            help='Explicitly allow client to perform '
921                                 '"insecure" TLS (https) requests. The '
922                                 'server\'s certificate will not be verified '
923                                 'against any certificate authorities. This '
924                                 'option should be used with caution.')
925
926        parser.add_argument('--os-cacert',
927                            metavar='<ca-certificate>',
928                            default=os.environ.get('OS_CACERT'),
929                            help='Specify a CA bundle file to use in '
930                                 'verifying a TLS (https) server certificate. '
931                                 'Defaults to env[OS_CACERT].')
932
933        parser.add_argument('--os-cert',
934                            metavar='<certificate>',
935                            default=os.environ.get('OS_CERT'),
936                            help='Defaults to env[OS_CERT].')
937
938        parser.add_argument('--os-key',
939                            metavar='<key>',
940                            default=os.environ.get('OS_KEY'),
941                            help='Defaults to env[OS_KEY].')
942
943        parser.add_argument('--timeout',
944                            default=600,
945                            type=_positive_non_zero_float,
946                            metavar='<seconds>',
947                            help='Set request timeout (in seconds).')
948
949    @classmethod
950    def load_from_cli_options(cls, args, **kwargs):
951        """Create a :py:class:`.Session` object from CLI arguments.
952
953        The CLI arguments must have been registered with
954        :py:meth:`.register_cli_options`.
955
956        :param Namespace args: result of parsed arguments.
957
958        :returns: A new session object.
959        :rtype: :py:class:`.Session`
960        """
961        kwargs['insecure'] = args.insecure
962        kwargs['cacert'] = args.os_cacert
963        if args.os_cert and args.os_key:
964            kwargs['cert'] = (args.os_cert, args.os_key)
965        kwargs['timeout'] = args.timeout
966
967        return cls._make(**kwargs)
968
969
970class TCPKeepAliveAdapter(requests.adapters.HTTPAdapter):
971    """The custom adapter used to set TCP Keep-Alive on all connections.
972
973    This Adapter also preserves the default behaviour of Requests which
974    disables Nagle's Algorithm. See also:
975    http://blogs.msdn.com/b/windowsazurestorage/archive/2010/06/25/nagle-s-algorithm-is-not-friendly-towards-small-requests.aspx
976    """
977
978    def init_poolmanager(self, *args, **kwargs):
979        if 'socket_options' not in kwargs:
980            socket_options = [
981                # Keep Nagle's algorithm off
982                (socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
983                # Turn on TCP Keep-Alive
984                (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
985            ]
986
987            # Some operating systems (e.g., OSX) do not support setting
988            # keepidle
989            if hasattr(socket, 'TCP_KEEPIDLE'):
990                socket_options += [
991                    # Wait 60 seconds before sending keep-alive probes
992                    (socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
993                ]
994
995            # TODO(claudiub): Windows does not contain the TCP_KEEPCNT and
996            # TCP_KEEPINTVL socket attributes. Instead, it contains
997            # SIO_KEEPALIVE_VALS, which can be set via ioctl, which should be
998            # set once it is available in requests.
999            # https://msdn.microsoft.com/en-us/library/dd877220%28VS.85%29.aspx
1000            if hasattr(socket, 'TCP_KEEPCNT'):
1001                socket_options += [
1002                    # Set the maximum number of keep-alive probes
1003                    (socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)
1004                ]
1005
1006            if hasattr(socket, 'TCP_KEEPINTVL'):
1007                socket_options += [
1008                    # Send keep-alive probes every 15 seconds
1009                    (socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 15)
1010                ]
1011
1012            # After waiting 60 seconds, and then sending a probe once every 15
1013            # seconds 4 times, these options should ensure that a connection
1014            # hands for no longer than 2 minutes before a ConnectionError is
1015            # raised.
1016
1017            kwargs['socket_options'] = socket_options
1018        super(TCPKeepAliveAdapter, self).init_poolmanager(*args, **kwargs)
1019