I'm creating an API gateway using fastAPI for some microservices, which in I authenticate user via Azure AD via fastapi-azure-auth.
So I'm trying to take out the user info from the request object (request.state.user) and inject my own token in it to later pass to other microservices.
I tried using app.middleware,
#app.middleware("http")
async def add_process_time_header(request: Request, call_next):
new_token = generate_token(request.state.user)
request["new_token"] = new_token
response = await call_next(request)
return response
but I'm getting following error at the initialization of the project:
AttributeError: 'State' object has no attribute 'user'
Also I tried creating a decorator:
from functools import wraps
def token_injector(function):
#wraps(function)
def wrap_function(*args, **kwargs):
user: User = kwargs['request'].state.user
new_token = generate_token(user)
kwargs["new_token"] = new_token # or kwargs['request']["new_token"] = new_token
return function(*args, **kwargs)
return wrap_function
and for this approach, I'm getting these errors:
ValueError: [TypeError("'coroutine' object is not iterable"),
TypeError('vars() argument must have dict attribute')]
and
TypeError: 'Request' object does not support item assignment
So does anyone have a solution for this?
UPDATE
Seems like it can't be done in middleware, cause the user info is added after it
I'm not familiar enough with fastapi-azure-auth to know if this would work exactly as you'd need, but you could do something like:
async def get_new_token(request: Request, user_info=Security(azure_scheme, scopes='user_impersonation'):
return 'my_freshly_generated_token:' + request.state.user
app.include_router(api_router, prefix=settings.API_V1_STR, dependencies=[Depends(get_new_token)])
#app.get('/foo')
async def actual_route(new_token: str = Depends(get_new_token)):
# do magic
pass
The actual dependency will only be evaluated once (so the router dependency will evaluate it, the view will get passed the returned value). Make sure to test that I haven't borked user authentication for some reason.
Thanks to #MatsLindh. With a slightly change of his answer, I could find a working solution:
async def get_new_token(request: Request):
# Generate new token
request.state.new_token = 'NEW_TOKEN_HERE'
app.include_router(
api_router,
prefix=settings.API_V1_STR,
dependencies=[Security(azure_scheme, scopes=['user_login'])],
)
#app.get('/foo')
async def actual_route(request: Request = Depends(get_new_token)):
# Find new token in request.state.new_token
pass
Related
I'm working on a project which uses FastAPI. My router file looks like the following:
# GET API Endpoint 1
#router.get("/project/{project_id}/{employee_id}")
async def method_one(
project_id: str, organization_id: str, session: AsyncSession = Depends(get_db)
):
try:
return await CustomController.method_one(
session, project_id, employee_id
)
except Exception as e:
return custom_exception_handler(e)
# GET API Endpoint 2
#router.get("/project/details/{project_id}")
async def method_two(
project_id: str, session: AsyncSession = Depends(get_db)
):
try:
return await CustomController.method_two(
session=session, project_id=project_id
)
except Exception as e:
return custom_exception_handler(e)
# GET API Endpoint 3
#router.get("/project/metadata/{project_id}")
async def method_three(
project_id: str, session: AsyncSession = Depends(get_db)
):
try:
return await CustomController.method_three(
session=session, project_id=project_id
)
except Exception as e:
return custom_exception_handler(e)
The obvious expectation of workflow here is: when each of these API endpoints are triggered with their required path parameters, the controller method is executed, as defined in their body.
However, for some strange reason, when API endpoints 2 and 3 are triggered, they are executing the controller method in endpoint 1, i.e., CustomController.method_one().
Upon adding some print() statements in the method method_one() of the router, I've observed that method_one() is being called when API endpoint 2 is called, while it is actually supposed to call method_two() in the router. Same is the case with API endpoint 3.
I'm unable to understand why the method body of method_one() is getting executed, when API endpoints 2 and 3 are triggered. Am I missing out something on configuration, or something - can someone please correct me? Thanks!
In FastAPI, as described in this answer, because endpoints are evaluated in order (see FastAPI's about how order matters), it makes sure that the endpoint you defined first in your app—in this case, that is, /project/{project_id}/...—will be evaluated first. Hence, every time you call one of the other two endpoints, i.e., /project/details/... and /project/metadata/..., the first endpoint is triggered, using details or metadata as the project_id parameter.
Solution
Thus, you need to make sure that the other two endpoints are declared before the one for /project/{project_id}/.... For example:
# GET API Endpoint 1
#router.get("/project/details/{project_id}")
# ...
# GET API Endpoint 2
#router.get("/project/metadata/{project_id}")
# ...
# GET API Endpoint 3
#router.get("/project/{project_id}/{employee_id}")
# ...
This might be a newbie question, but I can't get dependency_overrides to work for testing.
Following the docs this should be simple to implement but I'm missing something…
Consider the following code:
In main.py:
from fastapi import FastAPI
from routes import router
app = FastAPI()
app.include_router(router)
In routes.py:
from fastapi import APIRouter, status
from fastapi.param_functions import Depends
from fastapi.responses import JSONResponse
from authentication import Authentication
router = APIRouter()
#router.get("/list/", dependencies=[Depends(Authentication(role=user))])
async def return_all():
response = JSONResponse(
status_code=status.HTTP_200_OK,
content="Here is all the objects!"
)
return response
In test_list.py:
from unittest import TestCase
from fastapi.testclient import TestClient
from main import app
from authentication import Authentication
def override_dependencies():
return {"Some": "Thing"}
client = TestClient(app)
app.dependency_overrides[Authentication] = override_dependencies
class ListTestCase(TestCase):
def test_list_get(self):
response = client.get("/list/")
self.assertEqual(200, response.status_code)
Gives the following error:
self.assertEqual(200, response.status_code)
AssertionError: 200 != 403
i.e., it tried to authenticate but was denied. Hence, it doesn't seem that it overrides my dependency.
Note that Depends is used in the path operation decorator (#router.get), and not the function as in the docs…
I see that you are using Authentication(role=user) as a dependency, but then you are trying to override Authentication and these are two different callables, the former being actually Authentication(role=user).__call__; thus I guess FastAPI is not able to match the correct override.
The problem with this approach is that Authentication(role=user).__call__ is an instance method, but in order to override a dependency in dependency_overrides you must be able to adress the callable statically, so that it is always the same every time you call it.
I had to implement a similar thing, and i solved this by injecting the authentication logic in like this:
class RestIdentityValidator:
methods: List[AuthMethod]
def __init__(self, *methods: AuthMethod):
self.methods = list(dict.fromkeys([e for e in AuthMethod] if methods is None else methods))
def __call__(
self,
bearer_token_identity: IdentityInfo | None = Depends(get_bearer_token_identity),
basic_token_identity: IdentityInfo | None = Depends(get_basic_token_identity)
) -> IdentityInfo | None:
identity = None
for auth_method in self.methods:
match auth_method:
case AuthMethod.BEARER:
identity = bearer_token_identity
case AuthMethod.BASIC:
identity = basic_token_identity
case _:
pass
if identity is not None:
break
return identity
and then used it like this:
#router.get("/users", response_model=Response[List[UserOut]])
async def get_all_users(
identity: IdentityInfo = Depends(RestIdentityValidator(AuthMethod.BEARER)),
...
Then you can inject and override this normally. The fact that you are using it in the decorator shouldn't make any difference in this case.
Now this sample is not implementing the same functionality as yours, but I hope it can give you a hint on how to solve your problem.
Usage of async/await was presented in Flask 2.0. (https://flask.palletsprojects.com/en/2.0.x/async-await/)
I am using Flask-RestX so is it possible to use async/await in RestX requests handlers?
Something like:
#api.route('/try-async')
class MyResource(Resource):
#api.expect(some_schema)
async def get(self):
result = await async_function()
return result
is not working and when I try to reach this endpoint I'm getting error:
TypeError: Object of type coroutine is not JSON serializable
Is there any info on that?
Package versions:
flask==2.0.1
flask-restx==0.4.0
and I've also installed flask[async] as documentation suggests.
I've gotten around this by using an internal redirect
#api.route('/try-async')
class MyResource(Resource):
#api.expect(some_schema)
def get(self):
return redirect(url_for('.hidden_async'), code=307)
#api.route('/hidden-async', methods=['GET'])
async def hidden_async():
result = await async_function()
return result
Redirecting with code=307 will ensure any method and body are unchanged after the redirect (Link). So passing data to the async function is possible as well.
#api.route('/try-async')
class MyResource(Resource):
#api.expect(some_schema)
def post(self):
return redirect(url_for('.hidden_async'), code=307)
#api.route('/hidden-async', methods=['POST'])
async def hidden_async():
data = request.get_json()
tasks = [async_function(d) for d in data]
result = await asyncio.gather(tasks)
return result
I'm using the swagger editor (OpenApi 2) for creating flask apis in python. When you define a model in swagger and use it as a schema for the body of a request, swagger validates the body before handing the control to you in the X_controller.py files.
I want to add some code before that validation happens (for printing logs for debugging purposes). Swagger just prints to stdout errors like the following and they are not useful when you have a lot of fields (I need the key that isn't valid).
https://host/path validation error: False is not of type 'string'
10.255.0.2 - - [20/May/2020:20:20:20 +0000] "POST /path HTTP/1.1" 400 116 "-" "GuzzleHttp/7"
I know tecnically you can remove the validations in swagger and do them manually in your code but I want to keep using this feature, when it works it's awesome.
Any ideas on how to do this or any alternative to be able to log the requests are welcome.
After some time studying the matter this is what I learnt.
First let's take a look at how a python-flask server made with Swagger Editor works.
Swagger Editor generates the server stub through Swagger Codegen using the definition written in Swagger Editor. This server stub returned by codegen uses the framework Connexion on top of flask to handle all the HTTP requests and responses, including the validation against the swagger definition (swagger.yaml).
Connexion is a framework that makes it easy to develop python-flask servers because it has a lot of functionality you'd have to make yourself already built in, like parameter validation. All we need to do is replace (in this case modify) these connexion validators.
There are three validators:
ParameterValidator
RequestBodyValidator
ResponseValidator
They get mapped to flask by default but we can replace them easily in the __main__.py file as we'll see.
Our goal is to replace the default logs and default error response to some custom ones. I'm using a custom Error model and a function called error_response() for preparing error responses, and Loguru for logging the errors (not mandatory, you can keep the original one).
To make the changes needed, looking at the connexion validators code, we can see that most of it can be reused, we only need to modify:
RequestBodyValidator: __call__() and validate_schema()
ParameterValidator: __call__()
So we only need to create two new classes that extend the original ones, and copy and modify those functions.
Be careful when copying and pasting. This code is based on connexion==1.1.15. If your are on a different version you should base your classes on it.
In a new file custom_validators.py we need:
import json
import functools
from flask import Flask
from loguru import logger
from requests import Response
from jsonschema import ValidationError
from connexion.utils import all_json, is_null
from connexion.exceptions import ExtraParameterProblem
from swagger_server.models import Error
from connexion.decorators.validation import ParameterValidator, RequestBodyValidator
app = Flask(__name__)
def error_response(response: Error) -> Response:
return app.response_class(
response=json.dumps(response.to_dict(), default=str),
status=response.status,
mimetype='application/json')
class CustomParameterValidator(ParameterValidator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
#functools.wraps(function)
def wrapper(request):
if self.strict_validation:
query_errors = self.validate_query_parameter_list(request)
formdata_errors = self.validate_formdata_parameter_list(request)
if formdata_errors or query_errors:
raise ExtraParameterProblem(formdata_errors, query_errors)
for param in self.parameters.get('query', []):
error = self.validate_query_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('path', []):
error = self.validate_path_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('header', []):
error = self.validate_header_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('formData', []):
error = self.validate_formdata_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
return function(request)
return wrapper
class CustomRequestBodyValidator(RequestBodyValidator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
#functools.wraps(function)
def wrapper(request):
if all_json(self.consumes):
data = request.json
if data is None and len(request.body) > 0 and not self.is_null_value_valid:
# the body has contents that were not parsed as JSON
return error_response(Error(
status=415,
description="Invalid Content-type ({content_type}), JSON data was expected".format(content_type=request.headers.get("Content-Type", ""))
))
error = self.validate_schema(data, request.url)
if error and not self.has_default:
return error
response = function(request)
return response
return wrapper
def validate_schema(self, data, url):
if self.is_null_value_valid and is_null(data):
return None
try:
self.validator.validate(data)
except ValidationError as exception:
description = f'Validation error. Attribute "{exception.validator_value}" return this error: "{exception.message}"'
logger.error(description)
return error_response(Error(
status=400,
description=description
))
return None
Once we have our validators, we have to map them to the flask app (__main__.py) using validator_map:
validator_map = {
'parameter': CustomParameterValidator,
'body': CustomRequestBodyValidator,
'response': ResponseValidator,
}
app = connexion.App(__name__, specification_dir='./swagger/', validator_map=validator_map)
app.app.json_encoder = encoder.JSONEncoder
app.add_api(Path('swagger.yaml'), arguments={'title': 'MyApp'})
If you also need to replace the validator I didn't use in this example, just create a custom child class of ResponseValidator and replace it on the validator_map dictionary in __main__.py.
Connexion docs:
https://connexion.readthedocs.io/en/latest/request.html
Forgive me for repeating an answer first posted at https://stackoverflow.com/a/73051652/1630244
Have you tried the Connexion before_request feature? Here's an example that logs the headers and content before Connexion validates the body:
import connexion
import logging
from flask import request
logger = logging.getLogger(__name__)
conn_app = connexion.FlaskApp(__name__)
#conn_app.app.before_request
def before_request():
for h in request.headers:
logger.debug('header %s', h)
logger.debug('data %s', request.get_data())
For our requirements, I made the following changes
I updated the input validation exception code from 422 to 400.
I also modified the default Json error output.
My issue
My FastAPI generated automatic documentation is sill showing default error code and error message format.
My Question
Is it possible to update the API documentation to reflect my change like the correct error code and the right error output format?
Currently there is no simple way. You have to modify the OpenAPI file as described here. Meaning you have to load the dictionary and remove the references to the error 422. Here is a minimal example:
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.exceptions import RequestValidationError
app = FastAPI()
#app.get("/items/")
async def read_items():
return [{"name": "Foo"}]
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
"""Your custom validation exception."""
message =
return JSONResponse(
status_code=400,
content={"message": f"Validation error: {exc}"}
)
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi()
# look for the error 422 and removes it
for method in openapi_schema["paths"]:
try:
del openapi_schema["paths"][method]["post"]["responses"]["422"]
except KeyError:
pass
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
The current way to do it is modifying the generated OpenAPI https://fastapi.tiangolo.com/advanced/extending-openapi/