예제 #1
0
    def test_parse_content_type_header(self):
        values = [
            'application/json',
            'foo/bar',
            'application/json; charset=utf-8',
            'application/json; charset=utf-8; foo=bar',
        ]
        expected_results = [
            ('application/json', {}),
            ('foo/bar', {}),
            ('application/json', {'charset': 'utf-8'}),
            ('application/json', {'charset': 'utf-8', 'foo': 'bar'})
        ]

        for value, expected_result in zip(values, expected_results):
            result = parse_content_type_header(content_type=value)
            self.assertEqual(result, expected_result)
예제 #2
0
    def post(self, *args, **kwargs):
        hook = '/'.join(args)  # TODO: There must be a better way to do this.

        # Note: For backward compatibility reasons we default to application/json if content
        # type is not explicitly provided
        content_type = pecan.request.headers.get('Content-Type',
                                                 'application/json')
        content_type = parse_content_type_header(content_type=content_type)[0]
        body = pecan.request.body

        try:
            body = self._parse_request_body(content_type=content_type,
                                            body=body)
        except Exception as e:
            self._log_request('Failed to parse request body: %s.' % (str(e)),
                              pecan.request)
            msg = 'Failed to parse request body "%s": %s' % (body, str(e))
            return pecan.abort(http_client.BAD_REQUEST, msg)

        headers = self._get_headers_as_dict(pecan.request.headers)
        # If webhook contains a trace-tag use that else create create a unique trace-tag.
        trace_context = self._create_trace_context(trace_tag=headers.pop(
            TRACE_TAG_HEADER, None),
                                                   hook=hook)

        if hook == 'st2' or hook == 'st2/':
            return self._handle_st2_webhook(body, trace_context=trace_context)

        if not self._is_valid_hook(hook):
            self._log_request('Invalid hook.', pecan.request)
            msg = 'Webhook %s not registered with st2' % hook
            return pecan.abort(http_client.NOT_FOUND, msg)

        triggers = self._hooks.get_triggers_for_hook(hook)
        payload = {}

        payload['headers'] = headers
        payload['body'] = body
        # Dispatch trigger instance for each of the trigger found
        for trigger in triggers:
            self._trigger_dispatcher.dispatch(trigger,
                                              payload=payload,
                                              trace_context=trace_context)

        return body
예제 #3
0
    def test_parse_content_type_header(self):
        values = [
            'application/json',
            'foo/bar',
            'application/json; charset=utf-8',
            'application/json; charset=utf-8; foo=bar',
        ]
        expected_results = [('application/json', {}), ('foo/bar', {}),
                            ('application/json', {
                                'charset': 'utf-8'
                            }),
                            ('application/json', {
                                'charset': 'utf-8',
                                'foo': 'bar'
                            })]

        for value, expected_result in zip(values, expected_results):
            result = parse_content_type_header(content_type=value)
            self.assertEqual(result, expected_result)
예제 #4
0
파일: webhooks.py 프로젝트: Bala96/st2
    def post(self, *args, **kwargs):
        hook = '/'.join(args)  # TODO: There must be a better way to do this.

        # Note: For backward compatibility reasons we default to application/json if content
        # type is not explicitly provided
        content_type = pecan.request.headers.get('Content-Type', 'application/json')
        content_type = parse_content_type_header(content_type=content_type)[0]
        body = pecan.request.body

        try:
            body = self._parse_request_body(content_type=content_type, body=body)
        except Exception as e:
            self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)
            msg = 'Failed to parse request body "%s": %s' % (body, str(e))
            return pecan.abort(http_client.BAD_REQUEST, msg)

        headers = self._get_headers_as_dict(pecan.request.headers)
        # If webhook contains a trace-tag use that else create create a unique trace-tag.
        trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),
                                                   hook=hook)

        if hook == 'st2' or hook == 'st2/':
            return self._handle_st2_webhook(body, trace_context=trace_context)

        if not self._is_valid_hook(hook):
            self._log_request('Invalid hook.', pecan.request)
            msg = 'Webhook %s not registered with st2' % hook
            return pecan.abort(http_client.NOT_FOUND, msg)

        triggers = self._hooks.get_triggers_for_hook(hook)
        payload = {}

        payload['headers'] = headers
        payload['body'] = body
        # Dispatch trigger instance for each of the trigger found
        for trigger in triggers:
            self._trigger_dispatcher.dispatch(trigger, payload=payload,
                trace_context=trace_context)

        return body
예제 #5
0
    def test_parse_content_type_header(self):
        values = [
            "application/json",
            "foo/bar",
            "application/json; charset=utf-8",
            "application/json; charset=utf-8; foo=bar",
        ]
        expected_results = [
            ("application/json", {}),
            ("foo/bar", {}),
            ("application/json", {
                "charset": "utf-8"
            }),
            ("application/json", {
                "charset": "utf-8",
                "foo": "bar"
            }),
        ]

        for value, expected_result in zip(values, expected_results):
            result = parse_content_type_header(content_type=value)
            self.assertEqual(result, expected_result)
예제 #6
0
파일: router.py 프로젝트: jamal-jiang/st2
    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
예제 #7
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`.
        """
        endpoint, path_vars = self.match(req)

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

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

        if self.auth and security:
            try:
                auth_resp = None
                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'])
                        else:
                            token = None

                        if token:
                            if auth_resp:
                                raise auth_exc.MultipleAuthSourcesError(
                                    'Only one of Token or API key expected.')

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

                            context['user'] = User.get_by_name(auth_resp.user)

                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':
                if req.body:
                    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)

                    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:
                            Model = op_resolver(schema['x-api-model'])
                        else:
                            Model = Body

                        kw[argument_name] = Model(**data)
                else:
                    kw[argument_name] = None

            # 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
            type = param.get('type', None)
            if kw[argument_name] is not None:
                if 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 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 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])

        # Call the controller
        func = op_resolver(endpoint['operationId'])
        resp = func(**kw)

        # 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),
                                      responses.get('default', None))

        if response_spec and 'schema' in response_spec:
            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"')

        return resp
예제 #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()
                                )
                                # 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
예제 #9
0
파일: router.py 프로젝트: lyandut/st2
    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