Example #1
0
def validate_auth_cookie_is_correctly_configured() -> bool:
    """
    Function which verifies that SameCookie config option value is correctly configured.

    This method should be called in the api init phase so we catch any misconfiguration issues
    before startup.
    """
    if cfg.CONF.api.auth_cookie_same_site not in [
            "strict", "lax", "none", "unset"
    ]:
        raise ValueError(
            'Got invalid value "%s" (type %s) for cfg.CONF.api.auth_cookie_same_site config '
            "option. Valid values are: strict, lax, none, unset." % (
                cfg.CONF.api.auth_cookie_same_site,
                type(cfg.CONF.api.auth_cookie_same_site),
            ))

    # Now we try to make a dummy cookie to verify all the options are configured correctly. Some
    # Options are mutually exclusive - e.g. SameSite none and Secure false.
    try:
        # NOTE: none and unset don't mean the same thing - unset implies not setting this attribute
        # (backward compatibility) and none implies setting this attribute value to none
        same_site = cfg.CONF.api.auth_cookie_same_site

        kwargs = {}
        if same_site != "unset":
            kwargs["samesite"] = same_site

        cookies.make_cookie(
            "test_cookie",
            "dummyvalue",
            httponly=True,
            secure=cfg.CONF.api.auth_cookie_secure,
            **kwargs,
        )
    except Exception as e:
        raise ValueError(
            "Failed to validate api.auth_cookie config options: %s" % (str(e)))

    return True
Example #2
0
    def __call__(self, req):
        """
        The method is invoked on every request and shows the lifecycle of the request received from
        the middleware.

        Although some middleware may use parts of the API spec, it is safe to assume that if you're
        looking for the particular spec property handler, it's most likely a part of this method.

        At the time of writing, the only property being utilized by middleware was `x-log-result`.
        """
        LOG.debug("Received call with WebOb: %s", req)
        endpoint, path_vars = self.match(req)
        LOG.debug("Parsed endpoint: %s", endpoint)
        LOG.debug("Parsed path_vars: %s", path_vars)

        context = copy.copy(getattr(self, 'mock_context', {}))
        cookie_token = None

        # Handle security
        if 'security' in endpoint:
            security = endpoint.get('security')
        else:
            security = self.spec.get('security', [])

        if self.auth and security:
            try:
                security_definitions = self.spec.get('securityDefinitions', {})
                for statement in security:
                    declaration, options = statement.copy().popitem()
                    definition = security_definitions[declaration]

                    if definition['type'] == 'apiKey':
                        if definition['in'] == 'header':
                            token = req.headers.get(definition['name'])
                        elif definition['in'] == 'query':
                            token = req.GET.get(definition['name'])
                        elif definition['in'] == 'cookie':
                            token = req.cookies.get(definition['name'])
                        else:
                            token = None

                        if token:
                            auth_func = op_resolver(
                                definition['x-operationId'])
                            auth_resp = auth_func(token)

                            # Include information on how user authenticated inside the context
                            if 'auth-token' in definition['name'].lower():
                                auth_method = 'authentication token'
                            elif 'api-key' in definition['name'].lower():
                                auth_method = 'API key'

                            context['user'] = User.get_by_name(auth_resp.user)
                            context['auth_info'] = {
                                'method': auth_method,
                                'location': definition['in']
                            }

                            # Also include token expiration time when authenticated via auth token
                            if 'auth-token' in definition['name'].lower():
                                context['auth_info'][
                                    'token_expire'] = auth_resp.expiry

                            if 'x-set-cookie' in definition:
                                max_age = auth_resp.expiry - date_utils.get_datetime_utc_now(
                                )
                                cookie_token = cookies.make_cookie(
                                    definition['x-set-cookie'],
                                    token,
                                    max_age=max_age,
                                    httponly=True)

                            break

                if 'user' not in context:
                    raise auth_exc.NoAuthSourceProvidedError(
                        'One of Token or API key required.')
            except (auth_exc.NoAuthSourceProvidedError,
                    auth_exc.MultipleAuthSourcesError) as e:
                LOG.error(str(e))
                return abort_unauthorized(str(e))
            except auth_exc.TokenNotProvidedError as e:
                LOG.exception('Token is not provided.')
                return abort_unauthorized(str(e))
            except auth_exc.TokenNotFoundError as e:
                LOG.exception('Token is not found.')
                return abort_unauthorized(str(e))
            except auth_exc.TokenExpiredError as e:
                LOG.exception('Token has expired.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyNotProvidedError as e:
                LOG.exception('API key is not provided.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyNotFoundError as e:
                LOG.exception('API key is not found.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyDisabledError as e:
                LOG.exception('API key is disabled.')
                return abort_unauthorized(str(e))

            if cfg.CONF.rbac.enable:
                user_db = context['user']

                permission_type = endpoint.get('x-permissions', None)
                if permission_type:
                    resolver = resolvers.get_resolver_for_permission_type(
                        permission_type)
                    has_permission = resolver.user_has_permission(
                        user_db, permission_type)

                    if not has_permission:
                        raise rbac_exc.ResourceTypeAccessDeniedError(
                            user_db, permission_type)

        # Collect parameters
        kw = {}
        for param in endpoint.get('parameters', []) + endpoint.get(
                'x-parameters', []):
            name = param['name']
            argument_name = param.get('x-as', None) or name
            source = param['in']
            default = param.get('default', None)

            # Collecting params from different sources
            if source == 'query':
                kw[argument_name] = req.GET.get(name, default)
            elif source == 'path':
                kw[argument_name] = path_vars[name]
            elif source == 'header':
                kw[argument_name] = req.headers.get(name, default)
            elif source == 'formData':
                kw[argument_name] = req.POST.get(name, default)
            elif source == 'environ':
                kw[argument_name] = req.environ.get(name.upper(), default)
            elif source == 'context':
                kw[argument_name] = context.get(name, default)
            elif source == 'request':
                kw[argument_name] = getattr(req, name)
            elif source == 'body':
                content_type = req.headers.get('Content-Type',
                                               'application/json')
                content_type = parse_content_type_header(
                    content_type=content_type)[0]
                schema = param['schema']

                # NOTE: HACK: Workaround for eventlet wsgi server which sets Content-Type to
                # text/plain if Content-Type is not provided in the request.
                # All ouf our API endpoints except /v1/workflows/inspection and
                # /exp/validation/mistral expect application/json so we explicitly set it to that
                # if not provided (set to text/plain by the base http server) and if it's not
                # /v1/workflows/inspection and /exp/validation/mistral API endpoints.
                if not self.is_gunicorn and content_type == 'text/plain':
                    operation_id = endpoint['operationId']

                    if ('workflow_inspection_controller' not in operation_id
                            and 'mistral_validation_controller'
                            not in operation_id):
                        content_type = 'application/json'

                # Note: We also want to perform validation if no body is explicitly provided - in a
                # lot of POST, PUT scenarios, body is mandatory
                if not req.body and content_type == 'application/json':
                    req.body = b'{}'

                try:
                    if content_type == 'application/json':
                        data = req.json
                    elif content_type == 'text/plain':
                        data = req.body
                    elif content_type in [
                            'application/x-www-form-urlencoded',
                            'multipart/form-data'
                    ]:
                        data = urlparse.parse_qs(req.body)
                    else:
                        raise ValueError('Unsupported Content-Type: "%s"' %
                                         (content_type))
                except Exception as e:
                    detail = 'Failed to parse request body: %s' % str(e)
                    raise exc.HTTPBadRequest(detail=detail)

                # Special case for Python 3
                if six.PY3 and content_type == 'text/plain' and isinstance(
                        data, six.binary_type):
                    # Convert bytes to text type (string / unicode)
                    data = data.decode('utf-8')

                try:
                    CustomValidator(schema,
                                    resolver=self.spec_resolver).validate(data)
                except (jsonschema.ValidationError, ValueError) as e:
                    raise exc.HTTPBadRequest(detail=e.message,
                                             comment=traceback.format_exc())

                if content_type == 'text/plain':
                    kw[argument_name] = data
                else:

                    class Body(object):
                        def __init__(self, **entries):
                            self.__dict__.update(entries)

                    ref = schema.get('$ref', None)
                    if ref:
                        with self.spec_resolver.resolving(ref) as resolved:
                            schema = resolved

                    if 'x-api-model' in schema:
                        input_type = schema.get('type', [])
                        Model = op_resolver(schema['x-api-model'])

                        if input_type and not isinstance(
                                input_type, (list, tuple)):
                            input_type = [input_type]

                        # root attribute is not an object, we need to use wrapper attribute to
                        # make it work with **kwarg expansion
                        if input_type and 'array' in input_type:
                            data = {'data': data}

                        instance = self._get_model_instance(model_cls=Model,
                                                            data=data)

                        # Call validate on the API model - note we should eventually move all
                        # those model schema definitions into openapi.yaml
                        try:
                            instance = instance.validate()
                        except (jsonschema.ValidationError, ValueError) as e:
                            raise exc.HTTPBadRequest(
                                detail=e.message,
                                comment=traceback.format_exc())
                    else:
                        LOG.debug(
                            'Missing x-api-model definition for %s, using generic Body '
                            'model.' % (endpoint['operationId']))
                        model = Body
                        instance = self._get_model_instance(model_cls=model,
                                                            data=data)

                    kw[argument_name] = instance

            # Making sure all required params are present
            required = param.get('required', False)
            if required and kw[argument_name] is None:
                detail = 'Required parameter "%s" is missing' % name
                raise exc.HTTPBadRequest(detail=detail)

            # Validating and casting param types
            param_type = param.get('type', None)
            if kw[argument_name] is not None:
                if param_type == 'boolean':
                    positive = ('true', '1', 'yes', 'y')
                    negative = ('false', '0', 'no', 'n')

                    if str(kw[argument_name]).lower(
                    ) not in positive + negative:
                        detail = 'Parameter "%s" is not of type boolean' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = str(
                        kw[argument_name]).lower() in positive
                elif param_type == 'integer':
                    regex = r'^-?[0-9]+$'

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type integer' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = int(kw[argument_name])
                elif param_type == 'number':
                    regex = r'^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$'

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type float' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = float(kw[argument_name])
                elif param_type == 'array' and param.get('items', {}).get(
                        'type', None) == 'string':
                    if kw[argument_name] is None:
                        kw[argument_name] = []
                    elif isinstance(kw[argument_name], (list, tuple)):
                        # argument is already an array
                        pass
                    else:
                        kw[argument_name] = kw[argument_name].split(',')

        # Call the controller
        try:
            func = op_resolver(endpoint['operationId'])
        except Exception as e:
            LOG.exception('Failed to load controller for operation "%s": %s' %
                          (endpoint['operationId'], str(e)))
            raise e

        try:
            resp = func(**kw)
        except Exception as e:
            LOG.exception(
                'Failed to call controller function "%s" for operation "%s": %s'
                % (func.__name__, endpoint['operationId'], str(e)))
            raise e

        # Handle response
        if resp is None:
            resp = Response()

        if not hasattr(resp, '__call__'):
            resp = Response(json=resp)

        responses = endpoint.get('responses', {})
        response_spec = responses.get(str(resp.status_code), None)
        default_response_spec = responses.get('default', None)

        if not response_spec and default_response_spec:
            LOG.debug(
                'No custom response spec found for endpoint "%s", using a default one'
                % (endpoint['operationId']))
            response_spec_name = 'default'
        else:
            response_spec_name = str(resp.status_code)

        response_spec = response_spec or default_response_spec

        if response_spec and 'schema' in response_spec:
            LOG.debug(
                'Using response spec "%s" for endpoint %s and status code %s' %
                (response_spec_name, endpoint['operationId'],
                 resp.status_code))

            try:
                validator = CustomValidator(response_spec['schema'],
                                            resolver=self.spec_resolver)
                validator.validate(resp.json)
            except (jsonschema.ValidationError, ValueError):
                LOG.exception('Response validation failed.')
                resp.headers.add('Warning',
                                 '199 OpenAPI "Response validation failed"')
        else:
            LOG.debug('No response spec found for endpoint "%s"' %
                      (endpoint['operationId']))

        if cookie_token:
            resp.headerlist.append(('Set-Cookie', cookie_token))

        return resp
Example #3
0
 def makeOne(self, name, value, **kw):
     from webob.cookies import make_cookie
     return make_cookie(name, value, **kw)
Example #4
0
    def set_cookie(self, name=None, value='', max_age=None,
                   path='/', domain=None, secure=False, httponly=False,
                   comment=None, expires=None, overwrite=False, key=None):
        """
        Set (add) a cookie for the response.

        Arguments are:

        ``name``

           The cookie name.

        ``value``

           The cookie value, which should be a string or ``None``.  If
           ``value`` is ``None``, it's equivalent to calling the
           :meth:`webob.response.Response.unset_cookie` method for this
           cookie key (it effectively deletes the cookie on the client).

        ``max_age``

           An integer representing a number of seconds, ``datetime.timedelta``,
           or ``None``. This value is used as the ``Max-Age`` of the generated
           cookie.  If ``expires`` is not passed and this value is not
           ``None``, the ``max_age`` value will also influence the ``Expires``
           value of the cookie (``Expires`` will be set to now + max_age).  If
           this value is ``None``, the cookie will not have a ``Max-Age`` value
           (unless ``expires`` is set). If both ``max_age`` and ``expires`` are
           set, this value takes precedence.

        ``path``

           A string representing the cookie ``Path`` value.  It defaults to
           ``/``.

        ``domain``

           A string representing the cookie ``Domain``, or ``None``.  If
           domain is ``None``, no ``Domain`` value will be sent in the
           cookie.

        ``secure``

           A boolean.  If it's ``True``, the ``secure`` flag will be sent in
           the cookie, if it's ``False``, the ``secure`` flag will not be
           sent in the cookie.

        ``httponly``

           A boolean.  If it's ``True``, the ``HttpOnly`` flag will be sent
           in the cookie, if it's ``False``, the ``HttpOnly`` flag will not
           be sent in the cookie.

        ``comment``

           A string representing the cookie ``Comment`` value, or ``None``.
           If ``comment`` is ``None``, no ``Comment`` value will be sent in
           the cookie.

        ``expires``

           A ``datetime.timedelta`` object representing an amount of time,
           ``datetime.datetime`` or ``None``. A non-``None`` value is used to
           generate the ``Expires`` value of the generated cookie. If
           ``max_age`` is not passed, but this value is not ``None``, it will
           influence the ``Max-Age`` header. If this value is ``None``, the
           ``Expires`` cookie value will be unset (unless ``max_age`` is set).
           If ``max_age`` is set, it will be used to generate the ``expires``
           and this value is ignored.

        ``overwrite``

           If this key is ``True``, before setting the cookie, unset any
           existing cookie.

        """

        # Backwards compatibility for the old name "key", remove this in 1.7
        if name is None and key is not None:
            warn_deprecation('Argument "key" was renamed to "name".', 1.7, 1)
            name = key

        if name is None:
            raise TypeError('set_cookie() takes at least 1 argument')

        if overwrite:
            self.unset_cookie(name, strict=False)

        # If expires is set, but not max_age we set max_age to expires
        if not max_age and isinstance(expires, timedelta):
            max_age = expires

        # expires can also be a datetime
        if not max_age and isinstance(expires, datetime):
            max_age = expires - datetime.utcnow()

        value = bytes_(value, 'utf-8')

        cookie = make_cookie(name, value, max_age=max_age, path=path,
                domain=domain, secure=secure, httponly=httponly,
                comment=comment)

        self.headerlist.append(('Set-Cookie', cookie))
Example #5
0
    def set_cookie(self,
                   name=None,
                   value='',
                   max_age=None,
                   path='/',
                   domain=None,
                   secure=False,
                   httponly=False,
                   comment=None,
                   expires=None,
                   overwrite=False,
                   key=None):
        """
        Set (add) a cookie for the response.

        Arguments are:

        ``name``

           The cookie name.

        ``value``

           The cookie value, which should be a string or ``None``.  If
           ``value`` is ``None``, it's equivalent to calling the
           :meth:`webob.response.Response.unset_cookie` method for this
           cookie key (it effectively deletes the cookie on the client).

        ``max_age``

           An integer representing a number of seconds, ``datetime.timedelta``,
           or ``None``. This value is used as the ``Max-Age`` of the generated
           cookie.  If ``expires`` is not passed and this value is not
           ``None``, the ``max_age`` value will also influence the ``Expires``
           value of the cookie (``Expires`` will be set to now + max_age).  If
           this value is ``None``, the cookie will not have a ``Max-Age`` value
           (unless ``expires`` is set). If both ``max_age`` and ``expires`` are
           set, this value takes precedence.

        ``path``

           A string representing the cookie ``Path`` value.  It defaults to
           ``/``.

        ``domain``

           A string representing the cookie ``Domain``, or ``None``.  If
           domain is ``None``, no ``Domain`` value will be sent in the
           cookie.

        ``secure``

           A boolean.  If it's ``True``, the ``secure`` flag will be sent in
           the cookie, if it's ``False``, the ``secure`` flag will not be
           sent in the cookie.

        ``httponly``

           A boolean.  If it's ``True``, the ``HttpOnly`` flag will be sent
           in the cookie, if it's ``False``, the ``HttpOnly`` flag will not
           be sent in the cookie.

        ``comment``

           A string representing the cookie ``Comment`` value, or ``None``.
           If ``comment`` is ``None``, no ``Comment`` value will be sent in
           the cookie.

        ``expires``

           A ``datetime.timedelta`` object representing an amount of time,
           ``datetime.datetime`` or ``None``. A non-``None`` value is used to
           generate the ``Expires`` value of the generated cookie. If
           ``max_age`` is not passed, but this value is not ``None``, it will
           influence the ``Max-Age`` header. If this value is ``None``, the
           ``Expires`` cookie value will be unset (unless ``max_age`` is set).
           If ``max_age`` is set, it will be used to generate the ``expires``
           and this value is ignored.

        ``overwrite``

           If this key is ``True``, before setting the cookie, unset any
           existing cookie.

        """

        # Backwards compatibility for the old name "key", remove this in 1.7
        if name is None and key is not None:
            warn_deprecation('Argument "key" was renamed to "name".', 1.7, 1)
            name = key

        if name is None:
            raise TypeError('set_cookie() takes at least 1 argument')

        if overwrite:
            self.unset_cookie(name, strict=False)

        # If expires is set, but not max_age we set max_age to expires
        if not max_age and isinstance(expires, timedelta):
            max_age = expires

        # expires can also be a datetime
        if not max_age and isinstance(expires, datetime):
            max_age = expires - datetime.utcnow()

        value = bytes_(value, 'utf-8')

        cookie = make_cookie(name,
                             value,
                             max_age=max_age,
                             path=path,
                             domain=domain,
                             secure=secure,
                             httponly=httponly,
                             comment=comment)

        self.headerlist.append(('Set-Cookie', cookie))
Example #6
0
    def __call__(self, req):
        """
        The method is invoked on every request and shows the lifecycle of the request received from
        the middleware.

        Although some middleware may use parts of the API spec, it is safe to assume that if you're
        looking for the particular spec property handler, it's most likely a part of this method.

        At the time of writing, the only property being utilized by middleware was `x-log-result`.
        """
        LOG.debug("Received call with WebOb: %s", req)
        endpoint, path_vars = self.match(req)
        LOG.debug("Parsed endpoint: %s", endpoint)
        LOG.debug("Parsed path_vars: %s", path_vars)

        context = copy.copy(getattr(self, "mock_context", {}))
        cookie_token = None

        # Handle security
        if "security" in endpoint:
            security = endpoint.get("security")
        else:
            security = self.spec.get("security", [])

        if self.auth and security:
            try:
                security_definitions = self.spec.get("securityDefinitions", {})
                for statement in security:
                    declaration, options = statement.copy().popitem()
                    definition = security_definitions[declaration]

                    if definition["type"] == "apiKey":
                        if definition["in"] == "header":
                            token = req.headers.get(definition["name"])
                        elif definition["in"] == "query":
                            token = req.GET.get(definition["name"])
                        elif definition["in"] == "cookie":
                            token = req.cookies.get(definition["name"])
                        else:
                            token = None

                        if token:
                            _, auth_func = op_resolver(definition["x-operationId"])
                            auth_resp = auth_func(token)

                            # Include information on how user authenticated inside the context
                            if "auth-token" in definition["name"].lower():
                                auth_method = "authentication token"
                            elif "api-key" in definition["name"].lower():
                                auth_method = "API key"

                            context["user"] = User.get_by_name(auth_resp.user)
                            context["auth_info"] = {
                                "method": auth_method,
                                "location": definition["in"],
                            }

                            # Also include token expiration time when authenticated via auth token
                            if "auth-token" in definition["name"].lower():
                                context["auth_info"]["token_expire"] = auth_resp.expiry

                            if "x-set-cookie" in definition:
                                max_age = (
                                    auth_resp.expiry - date_utils.get_datetime_utc_now()
                                )
                                # NOTE: unset and none don't mean the same thing - unset implies
                                # not setting this attribute at all (backward compatibility) and
                                # none implies setting this attribute value to none
                                same_site = cfg.CONF.api.auth_cookie_same_site

                                kwargs = {}
                                if same_site != "unset":
                                    kwargs["samesite"] = same_site

                                cookie_token = cookies.make_cookie(
                                    definition["x-set-cookie"],
                                    token,
                                    max_age=max_age,
                                    httponly=True,
                                    secure=cfg.CONF.api.auth_cookie_secure,
                                    **kwargs,
                                )

                            break

                if "user" not in context:
                    raise auth_exc.NoAuthSourceProvidedError(
                        "One of Token or API key required."
                    )
            except (
                auth_exc.NoAuthSourceProvidedError,
                auth_exc.MultipleAuthSourcesError,
            ) as e:
                LOG.error(six.text_type(e))
                return abort_unauthorized(six.text_type(e))
            except auth_exc.TokenNotProvidedError as e:
                LOG.exception("Token is not provided.")
                return abort_unauthorized(six.text_type(e))
            except auth_exc.TokenNotFoundError as e:
                LOG.exception("Token is not found.")
                return abort_unauthorized(six.text_type(e))
            except auth_exc.TokenExpiredError as e:
                LOG.exception("Token has expired.")
                return abort_unauthorized(six.text_type(e))
            except auth_exc.ApiKeyNotProvidedError as e:
                LOG.exception("API key is not provided.")
                return abort_unauthorized(six.text_type(e))
            except auth_exc.ApiKeyNotFoundError as e:
                LOG.exception("API key is not found.")
                return abort_unauthorized(six.text_type(e))
            except auth_exc.ApiKeyDisabledError as e:
                LOG.exception("API key is disabled.")
                return abort_unauthorized(six.text_type(e))

            if cfg.CONF.rbac.enable:
                user_db = context["user"]

                permission_type = endpoint.get("x-permissions", None)
                if permission_type:
                    rbac_backend = get_rbac_backend()

                    resolver = rbac_backend.get_resolver_for_permission_type(
                        permission_type
                    )
                    has_permission = resolver.user_has_permission(
                        user_db, permission_type
                    )

                    if not has_permission:
                        raise rbac_exc.ResourceTypeAccessDeniedError(
                            user_db, permission_type
                        )

        # Collect parameters
        kw = {}
        for param in endpoint.get("parameters", []) + endpoint.get("x-parameters", []):
            name = param["name"]
            argument_name = param.get("x-as", None) or name
            source = param["in"]
            default = param.get("default", None)

            # Collecting params from different sources
            if source == "query":
                kw[argument_name] = req.GET.get(name, default)
            elif source == "path":
                kw[argument_name] = path_vars[name]
            elif source == "header":
                kw[argument_name] = req.headers.get(name, default)
            elif source == "formData":
                kw[argument_name] = req.POST.get(name, default)
            elif source == "environ":
                kw[argument_name] = req.environ.get(name.upper(), default)
            elif source == "context":
                kw[argument_name] = context.get(name, default)
            elif source == "request":
                kw[argument_name] = getattr(req, name)
            elif source == "body":
                content_type = req.headers.get("Content-Type", "application/json")
                content_type = parse_content_type_header(content_type=content_type)[0]
                schema = param["schema"]

                # NOTE: HACK: Workaround for eventlet wsgi server which sets Content-Type to
                # text/plain if Content-Type is not provided in the request.
                # All ouf our API endpoints except /v1/workflows/inspection
                # expect application/json so we explicitly set it to that
                # if not provided (set to text/plain by the base http server) and if it's not
                # /v1/workflows/inspection API endpoints.
                if not self.is_gunicorn and content_type == "text/plain":
                    operation_id = endpoint["operationId"]

                    if "workflow_inspection_controller" not in operation_id:
                        content_type = "application/json"

                # Note: We also want to perform validation if no body is explicitly provided - in a
                # lot of POST, PUT scenarios, body is mandatory
                if not req.body and content_type == "application/json":
                    req.body = b"{}"

                try:
                    if content_type == "application/json":
                        data = req.json
                    elif content_type == "text/plain":
                        data = req.body
                    elif content_type in [
                        "application/x-www-form-urlencoded",
                        "multipart/form-data",
                    ]:
                        data = urlparse.parse_qs(six.ensure_str(req.body))
                    else:
                        raise ValueError(
                            'Unsupported Content-Type: "%s"' % (content_type)
                        )
                except Exception as e:
                    detail = "Failed to parse request body: %s" % six.text_type(e)
                    raise exc.HTTPBadRequest(detail=detail)

                # Special case for Python 3
                if (
                    six.PY3
                    and content_type == "text/plain"
                    and isinstance(data, six.binary_type)
                ):
                    # Convert bytes to text type (string / unicode)
                    data = data.decode("utf-8")

                try:
                    CustomValidator(schema, resolver=self.spec_resolver).validate(data)
                except (jsonschema.ValidationError, ValueError) as e:
                    raise exc.HTTPBadRequest(
                        detail=getattr(e, "message", six.text_type(e)),
                        comment=traceback.format_exc(),
                    )

                if content_type == "text/plain":
                    kw[argument_name] = data
                else:
                    ref = schema.get("$ref", None)
                    if ref:
                        with self.spec_resolver.resolving(ref) as resolved:
                            schema = resolved

                    if "x-api-model" in schema:
                        input_type = schema.get("type", [])
                        _, Model = op_resolver(schema["x-api-model"])

                        if input_type and not isinstance(input_type, (list, tuple)):
                            input_type = [input_type]

                        # root attribute is not an object, we need to use wrapper attribute to
                        # make it work with **kwarg expansion
                        if input_type and "array" in input_type:
                            data = {"data": data}

                        instance = self._get_model_instance(model_cls=Model, data=data)

                        # Call validate on the API model - note we should eventually move all
                        # those model schema definitions into openapi.yaml
                        try:
                            instance = instance.validate()
                        except (jsonschema.ValidationError, ValueError) as e:
                            raise exc.HTTPBadRequest(
                                detail=getattr(e, "message", six.text_type(e)),
                                comment=traceback.format_exc(),
                            )
                    else:
                        LOG.debug(
                            "Missing x-api-model definition for %s, using GenericRequestParam "
                            "model." % (endpoint["operationId"])
                        )
                        model = GenericRequestParam
                        instance = self._get_model_instance(model_cls=model, data=data)

                    kw[argument_name] = instance

            # Making sure all required params are present
            required = param.get("required", False)
            if required and kw[argument_name] is None:
                detail = 'Required parameter "%s" is missing' % name
                raise exc.HTTPBadRequest(detail=detail)

            # Validating and casting param types
            param_type = param.get("type", None)
            if kw[argument_name] is not None:
                if param_type == "boolean":
                    positive = ("true", "1", "yes", "y")
                    negative = ("false", "0", "no", "n")

                    if str(kw[argument_name]).lower() not in positive + negative:
                        detail = 'Parameter "%s" is not of type boolean' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = str(kw[argument_name]).lower() in positive
                elif param_type == "integer":
                    regex = r"^-?[0-9]+$"

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type integer' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = int(kw[argument_name])
                elif param_type == "number":
                    regex = r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$"

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type float' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = float(kw[argument_name])
                elif (
                    param_type == "array"
                    and param.get("items", {}).get("type", None) == "string"
                ):
                    if kw[argument_name] is None:
                        kw[argument_name] = []
                    elif isinstance(kw[argument_name], (list, tuple)):
                        # argument is already an array
                        pass
                    else:
                        kw[argument_name] = kw[argument_name].split(",")

        # Call the controller
        try:
            controller_instance, func = op_resolver(endpoint["operationId"])
        except Exception as e:
            LOG.exception(
                'Failed to load controller for operation "%s": %s'
                % (endpoint["operationId"], six.text_type(e))
            )
            raise e

        try:
            resp = func(**kw)
        except DataStoreKeyNotFoundError as e:
            LOG.warning(
                'Failed to call controller function "%s" for operation "%s": %s'
                % (func.__name__, endpoint["operationId"], six.text_type(e))
            )
            raise e
        except Exception as e:
            LOG.exception(
                'Failed to call controller function "%s" for operation "%s": %s'
                % (func.__name__, endpoint["operationId"], six.text_type(e))
            )
            raise e

        # Handle response
        if resp is None:
            resp = Response()

        if not hasattr(resp, "__call__"):
            resp = Response(json=resp)

        operation_id = endpoint["operationId"]

        # Process the response removing attributes based on the exclude_attribute and
        # include_attributes query param filter values (if specified)
        include_attributes = kw.get("include_attributes", None)
        exclude_attributes = kw.get("exclude_attributes", None)
        has_include_or_exclude_attributes = bool(include_attributes) or bool(
            exclude_attributes
        )

        # NOTE: We do NOT want to process stream controller response
        is_streamming_controller = endpoint.get(
            "x-is-streaming-endpoint", bool("st2stream" in operation_id)
        )

        if (
            not is_streamming_controller
            and resp.body
            and has_include_or_exclude_attributes
        ):
            # NOTE: We need to check for response.body attribute since resp.json throws if JSON
            # response is not available
            mandatory_include_fields = getattr(
                controller_instance, "mandatory_include_fields_response", []
            )
            data = self._process_response(
                data=resp.json,
                mandatory_include_fields=mandatory_include_fields,
                include_attributes=include_attributes,
                exclude_attributes=exclude_attributes,
            )
            resp.json = data

        responses = endpoint.get("responses", {})
        response_spec = responses.get(str(resp.status_code), None)
        default_response_spec = responses.get("default", None)

        if not response_spec and default_response_spec:
            LOG.debug(
                'No custom response spec found for endpoint "%s", using a default one'
                % (endpoint["operationId"])
            )
            response_spec_name = "default"
        else:
            response_spec_name = str(resp.status_code)

        response_spec = response_spec or default_response_spec
        response_spec = response_spec or {}
        validate_response = response_spec.get("schema", {}).get(
            "validate_response", True
        )

        if (
            response_spec
            and "schema" in response_spec
            and not has_include_or_exclude_attributes
            and validate_response
        ):
            # NOTE: We don't perform response validation when include or exclude attributes are
            # provided because this means partial response which likely won't pass the validation
            LOG.debug(
                'Using response spec "%s" for endpoint %s and status code %s'
                % (response_spec_name, endpoint["operationId"], resp.status_code)
            )

            try:
                validator = CustomValidator(
                    response_spec["schema"], resolver=self.spec_resolver
                )

                response_type = response_spec["schema"].get("type", "json")
                if response_type == "string":
                    validator.validate(resp.text)
                else:
                    validator.validate(resp.json)
            except (jsonschema.ValidationError, ValueError):
                LOG.exception("Response validation failed.")
                resp.headers.add("Warning", '199 OpenAPI "Response validation failed"')
        else:
            LOG.debug(
                'No response spec found for endpoint "%s"' % (endpoint["operationId"])
            )

        if cookie_token:
            resp.headerlist.append(("Set-Cookie", cookie_token))

        return resp
Example #7
0
 def set_cookie(self, name, value="", **args):
     if isinstance(value, str):
         value = value.encode("utf-8")
     cookie = make_cookie(name, value, **args)
     self._response.AddHeader("Set-Cookie", cookie)
Example #8
0
    def __call__(self, req):
        """
        The method is invoked on every request and shows the lifecycle of the request received from
        the middleware.

        Although some middleware may use parts of the API spec, it is safe to assume that if you're
        looking for the particular spec property handler, it's most likely a part of this method.

        At the time of writing, the only property being utilized by middleware was `x-log-result`.
        """
        LOG.debug("Received call with WebOb: %s", req)
        endpoint, path_vars = self.match(req)
        LOG.debug("Parsed endpoint: %s", endpoint)
        LOG.debug("Parsed path_vars: %s", path_vars)

        context = copy.copy(getattr(self, 'mock_context', {}))
        cookie_token = None

        # Handle security
        if 'security' in endpoint:
            security = endpoint.get('security')
        else:
            security = self.spec.get('security', [])

        if self.auth and security:
            try:
                security_definitions = self.spec.get('securityDefinitions', {})
                for statement in security:
                    declaration, options = statement.copy().popitem()
                    definition = security_definitions[declaration]

                    if definition['type'] == 'apiKey':
                        if definition['in'] == 'header':
                            token = req.headers.get(definition['name'])
                        elif definition['in'] == 'query':
                            token = req.GET.get(definition['name'])
                        elif definition['in'] == 'cookie':
                            token = req.cookies.get(definition['name'])
                        else:
                            token = None

                        if token:
                            auth_func = op_resolver(definition['x-operationId'])
                            auth_resp = auth_func(token)

                            # Include information on how user authenticated inside the context
                            if 'auth-token' in definition['name'].lower():
                                auth_method = 'authentication token'
                            elif 'api-key' in definition['name'].lower():
                                auth_method = 'API key'

                            context['user'] = User.get_by_name(auth_resp.user)
                            context['auth_info'] = {
                                'method': auth_method,
                                'location': definition['in']
                            }

                            # Also include token expiration time when authenticated via auth token
                            if 'auth-token' in definition['name'].lower():
                                context['auth_info']['token_expire'] = auth_resp.expiry

                            if 'x-set-cookie' in definition:
                                max_age = auth_resp.expiry - date_utils.get_datetime_utc_now()
                                cookie_token = cookies.make_cookie(definition['x-set-cookie'],
                                                                   token,
                                                                   max_age=max_age,
                                                                   httponly=True)

                            break

                if 'user' not in context:
                    raise auth_exc.NoAuthSourceProvidedError('One of Token or API key required.')
            except (auth_exc.NoAuthSourceProvidedError,
                    auth_exc.MultipleAuthSourcesError) as e:
                LOG.error(str(e))
                return abort_unauthorized(str(e))
            except auth_exc.TokenNotProvidedError as e:
                LOG.exception('Token is not provided.')
                return abort_unauthorized(str(e))
            except auth_exc.TokenNotFoundError as e:
                LOG.exception('Token is not found.')
                return abort_unauthorized(str(e))
            except auth_exc.TokenExpiredError as e:
                LOG.exception('Token has expired.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyNotProvidedError as e:
                LOG.exception('API key is not provided.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyNotFoundError as e:
                LOG.exception('API key is not found.')
                return abort_unauthorized(str(e))
            except auth_exc.ApiKeyDisabledError as e:
                LOG.exception('API key is disabled.')
                return abort_unauthorized(str(e))

            if cfg.CONF.rbac.enable:
                user_db = context['user']

                permission_type = endpoint.get('x-permissions', None)
                if permission_type:
                    resolver = resolvers.get_resolver_for_permission_type(permission_type)
                    has_permission = resolver.user_has_permission(user_db, permission_type)

                    if not has_permission:
                        raise rbac_exc.ResourceTypeAccessDeniedError(user_db,
                                                                     permission_type)

        # Collect parameters
        kw = {}
        for param in endpoint.get('parameters', []) + endpoint.get('x-parameters', []):
            name = param['name']
            argument_name = param.get('x-as', None) or name
            source = param['in']
            default = param.get('default', None)

            # Collecting params from different sources
            if source == 'query':
                kw[argument_name] = req.GET.get(name, default)
            elif source == 'path':
                kw[argument_name] = path_vars[name]
            elif source == 'header':
                kw[argument_name] = req.headers.get(name, default)
            elif source == 'formData':
                kw[argument_name] = req.POST.get(name, default)
            elif source == 'environ':
                kw[argument_name] = req.environ.get(name.upper(), default)
            elif source == 'context':
                kw[argument_name] = context.get(name, default)
            elif source == 'request':
                kw[argument_name] = getattr(req, name)
            elif source == 'body':
                # Note: We also want to perform validation if no body is explicitly provided - in a
                # lot of POST, PUT scenarios, body is mandatory
                if not req.body:
                    req.body = b'{}'

                content_type = req.headers.get('Content-Type', 'application/json')
                content_type = parse_content_type_header(content_type=content_type)[0]
                schema = param['schema']

                try:
                    if content_type == 'application/json':
                        data = req.json
                    elif content_type == 'text/plain':
                        data = req.body
                    elif content_type in ['application/x-www-form-urlencoded',
                                          'multipart/form-data']:
                        data = urlparse.parse_qs(req.body)
                    else:
                        raise ValueError('Unsupported Content-Type: "%s"' % (content_type))
                except Exception as e:
                    detail = 'Failed to parse request body: %s' % str(e)
                    raise exc.HTTPBadRequest(detail=detail)

                # Special case for Python 3
                if six.PY3 and content_type == 'text/plain' and isinstance(data, six.binary_type):
                    # Convert bytes to text type (string / unicode)
                    data = data.decode('utf-8')

                try:
                    CustomValidator(schema, resolver=self.spec_resolver).validate(data)
                except (jsonschema.ValidationError, ValueError) as e:
                    raise exc.HTTPBadRequest(detail=e.message,
                                             comment=traceback.format_exc())

                if content_type == 'text/plain':
                    kw[argument_name] = data
                else:
                    class Body(object):
                        def __init__(self, **entries):
                            self.__dict__.update(entries)

                    ref = schema.get('$ref', None)
                    if ref:
                        with self.spec_resolver.resolving(ref) as resolved:
                            schema = resolved

                    if 'x-api-model' in schema:
                        input_type = schema.get('type', [])
                        Model = op_resolver(schema['x-api-model'])

                        if input_type and not isinstance(input_type, (list, tuple)):
                            input_type = [input_type]

                        # root attribute is not an object, we need to use wrapper attribute to
                        # make it work with **kwarg expansion
                        if input_type and 'array' in input_type:
                            data = {'data': data}

                        instance = self._get_model_instance(model_cls=Model, data=data)

                        # Call validate on the API model - note we should eventually move all
                        # those model schema definitions into openapi.yaml
                        try:
                            instance = instance.validate()
                        except (jsonschema.ValidationError, ValueError) as e:
                            raise exc.HTTPBadRequest(detail=e.message,
                                                     comment=traceback.format_exc())
                    else:
                        LOG.debug('Missing x-api-model definition for %s, using generic Body '
                                  'model.' % (endpoint['operationId']))
                        model = Body
                        instance = self._get_model_instance(model_cls=model, data=data)

                    kw[argument_name] = instance

            # Making sure all required params are present
            required = param.get('required', False)
            if required and kw[argument_name] is None:
                detail = 'Required parameter "%s" is missing' % name
                raise exc.HTTPBadRequest(detail=detail)

            # Validating and casting param types
            param_type = param.get('type', None)
            if kw[argument_name] is not None:
                if param_type == 'boolean':
                    positive = ('true', '1', 'yes', 'y')
                    negative = ('false', '0', 'no', 'n')

                    if str(kw[argument_name]).lower() not in positive + negative:
                        detail = 'Parameter "%s" is not of type boolean' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = str(kw[argument_name]).lower() in positive
                elif param_type == 'integer':
                    regex = r'^-?[0-9]+$'

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type integer' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = int(kw[argument_name])
                elif param_type == 'number':
                    regex = r'^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$'

                    if not re.search(regex, str(kw[argument_name])):
                        detail = 'Parameter "%s" is not of type float' % argument_name
                        raise exc.HTTPBadRequest(detail=detail)

                    kw[argument_name] = float(kw[argument_name])
                elif param_type == 'array' and param.get('items', {}).get('type', None) == 'string':
                    if kw[argument_name] is None:
                        kw[argument_name] = []
                    elif isinstance(kw[argument_name], (list, tuple)):
                        # argument is already an array
                        pass
                    else:
                        kw[argument_name] = kw[argument_name].split(',')

        # Call the controller
        try:
            func = op_resolver(endpoint['operationId'])
        except Exception as e:
            LOG.exception('Failed to load controller for operation "%s": %s' %
                          (endpoint['operationId'], str(e)))
            raise e

        try:
            resp = func(**kw)
        except Exception as e:
            LOG.exception('Failed to call controller function "%s" for operation "%s": %s' %
                          (func.__name__, endpoint['operationId'], str(e)))
            raise e

        # Handle response
        if resp is None:
            resp = Response()

        if not hasattr(resp, '__call__'):
            resp = Response(json=resp)

        responses = endpoint.get('responses', {})
        response_spec = responses.get(str(resp.status_code), None)
        default_response_spec = responses.get('default', None)

        if not response_spec and default_response_spec:
            LOG.debug('No custom response spec found for endpoint "%s", using a default one' %
                      (endpoint['operationId']))
            response_spec_name = 'default'
        else:
            response_spec_name = str(resp.status_code)

        response_spec = response_spec or default_response_spec

        if response_spec and 'schema' in response_spec:
            LOG.debug('Using response spec "%s" for endpoint %s and status code %s' %
                     (response_spec_name, endpoint['operationId'], resp.status_code))

            try:
                validator = CustomValidator(response_spec['schema'], resolver=self.spec_resolver)
                validator.validate(resp.json)
            except (jsonschema.ValidationError, ValueError):
                LOG.exception('Response validation failed.')
                resp.headers.add('Warning', '199 OpenAPI "Response validation failed"')
        else:
            LOG.debug('No response spec found for endpoint "%s"' % (endpoint['operationId']))

        if cookie_token:
            resp.headerlist.append(('Set-Cookie', cookie_token))

        return resp