FASTApi authentication injection - python

My application has an AuthenticateService implemented as follows:
from domain.ports.repositories import ISalesmanRepository
from fastapi import HTTPException, status
from fastapi_jwt_auth import AuthJWT
from fastapi_jwt_auth.exceptions import JWTDecodeError
from shared.exceptions import EntityNotFound
from adapters.api.authentication.config import User
class AuthenticateService:
def __init__(self, user_repo: ISalesmanRepository):
self._repo = user_repo
def __call__(self, auth: AuthJWT) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
auth.jwt_required()
user_id = auth.get_jwt_subject()
except JWTDecodeError:
raise credentials_exception
try:
user = self._repo.get_by_id(user_id)
return user
except EntityNotFound:
raise credentials_exception
So the behavior is basically:
Is jwt is valid, get user from repository and returns
Raise 401 if jwt is invalid
The problem is that in every controller implemented I have to repeat the process. I tried to implement a decorator that injects the user into the controller in case of success but I couldn't. I'm sure that the best way to implement this is to use fastAPI's Depends dependency injector.
Today, a controller looks something like this:
from typing import Optional
from adapters.api.services import authenticate_service, create_sale_service
from fastapi import APIRouter, Depends
from fastapi_jwt_auth import AuthJWT
from pydantic import BaseModel
router = APIRouter()
class Request(BaseModel):
code: str
value: float
date: str
status: Optional[str] = None
#router.post('/sale')
def create_sale(request: Request, auth: AuthJWT = Depends()):
user = authenticate_service(auth)
result = create_sale_service.handle(
{"salesman": user, "sale": request.dict()}
)
return result.dict()
How can I abstract my authentication so that my controllers look like any of the versions below:
# Option 1: decorator
#router.post('/sale')
#authentication_required
def create_sale(request: Request, user: User): # User is the `__call__` response from `AuthenticateService` class
result = create_sale_service.handle(
{"salesman": user, "sale": request.dict()}
)
return result.dict()
# Option 2:
#router.post('/sale')
def create_sale(request: Request, user: User = Depends(authenticate_service)): # Something like that, using the depends to inject User to me
result = create_sale_service.handle(
{"salesman": user, "sale": request.dict()}
)
return result.dict()

So I read a little more the Depends documentation and realize whats was going wrong with my attempt to inject the user on controller signature.
Right way to implement:
> AuthService class
class AuthenticateService:
def __init__(self, user_repo: ISalesmanRepository):
self._repo = user_repo
def __call__(self, auth: AuthJWT = Depends()) -> User:
...
authenticate_service = AuthenticateService(user_repository)
> Controller
#router.post('/sale')
def create_sale(request: Request, user: User = Depends(authenticate_service)):
result = create_sale_service.handle(
{"salesman": user, "sale": request.dict()}
)
return result.dict()

Related

Change "Error: response status" details in FastAPI / OpenAPI

I implemented some Custom Exceptions. And now I want to display the Exception name at the red box. Where can I set this value?
class CustomExceptionHandler(Exception):
def __init__(self, exception):
self.message = exception.message
self.status_code_number = exception.status_code_number
#app.exception_handler(CustomExceptionHandler)
async def data_processing_error_handler(request: Request, exc: CustomExceptionHandler):
return JSONResponse(
status_code=exc.status_code_number,
content=jsonable_encoder({exc.message}),
)

FastAPI, Pytest TypeError: object dict can't be used in 'await' expression

I am writing an API using FastAPI when I run with uvicorn everything is normal, I get the error when I want to run a test using the FastAPI TestClient.
This is the error:
async def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = await collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
TypeError: object dict can't be used in 'await' expression
db\db.py:12: TypeError
project structure:
APP
|--__init__.py
|--run.py
|--main.py
|--test
|--test_app.py
|--routes
|--router.py
|--models
|--models.py
|--db
|--db_conn.py
|--db.py
|--auth_jwt
|--jwt_auth.py
|--auth
|--auth.py
This is the code of the test, I am using mongomock, I don't know if this will be the root of the problem:
import collections
from fastapi.testclient import TestClient
from fastapi import status
from main import app
from mongoengine import connect, disconnect, get_connection
from db.db_conn import db
client = TestClient(app)
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
db.client = get_connection('testdb')
db.client["CIA"]
db.client["Employees_Info"]
db.client.CIA.Employees_Info.insert_one({"name": "user_Name","password": "week"})
def test_ping():
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"message": "Conectado"}
def test_login():
data = {"username":'user_name', 'password':'week'}
response = client.post("/login", data=data)
assert response.headers['Content-Type'] == 'application/json'
assert response.status_code == status.HTTP_200_OK
db.client.disconnect()
I tried performing the Async test according to the FastAPI documentation but it doesn't work either, if I use the "normal" database the test works.
router.py
#router.post("/login", tags=["user"], response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncIOMotorClient = Depends(get_database)):
authenticate_user_id = await authenticate_user(db, form_data.username, form_data.password)
if not authenticate_user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"user_id": str(authenticate_user_id["_id"])})
return {"access_token": access_token, "token_type": "bearer"}
auth.py
async def authenticate_user(conn:AsyncIOMotorClient, username, password):
user_id = await verify_user(conn, username)
if not user_id:
return False
if not await verify_password(conn, user_id, password):
return False
return user_id
async def verify_user(conn, user):
return await get_user_id(conn,user)
async def verify_password(conn, user_id, password):
return pbkdf2_sha256.verify(password, await get_password(conn, user_id))
db.py
async def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = await collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
print(type(user))
if user:
return user
async def get_password(conn, user_id):
collection = conn.CIA.get_collection("Employees_Info")
db = await collection.find_one(user_id)
if db:
return db['password']
Maybe you need install pytest-asyncio.Here more info https://fastapi.tiangolo.com/advanced/async-tests/
collection.find_one() is not a async function, so you are trying to await the result of the function which is a dict, that is why you are getting the error TypeError: object dict can't be used in 'await' expression you are awaiting a dict, not a coroutine which would be returned by an async function.
To fix you code just remove await from
db = await collection.find_one(user_id)
When you do that, you won't really need it to be a async function, so you can just define it regularly, but you will than have to change all the function calls and remove the await from them, otherwise you will get this error again
Full code:
db.py
def get_user_id(conn, user):
collection = conn.CIA.get_collection("Employees_Info")
user = collection.find_one({'name':user},{"_id":1, "name":0, "password":0})
print(type(user))
if user:
return user
def get_password(conn, user_id):
collection = conn.CIA.get_collection("Employees_Info")
db = collection.find_one(user_id)
if db:
return db['password']
auth.py
def authenticate_user(conn:AsyncIOMotorClient, username, password):
user_id = verify_user(conn, username)
if not user_id:
return False
if not verify_password(conn, user_id, password):
return False
return user_id
def verify_user(conn, user):
return get_user_id(conn,user)
def verify_password(conn, user_id, password):
return pbkdf2_sha256.verify(password, get_password(conn, user_id))
router.py
# This probably has to stay as an async function, I'm not sure how the module works
#router.post("/login", tags=["user"], response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncIOMotorClient = Depends(get_database)):
authenticate_user_id = authenticate_user(db, form_data.username, form_data.password)
if not authenticate_user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"user_id": str(authenticate_user_id["_id"])})
return {"access_token": access_token, "token_type": "bearer"}
import collections
from fastapi.testclient import TestClient
from fastapi import status
from main import app
from mongoengine import connect, disconnect, get_connection
from db.db_conn import db
client = TestClient(app)
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
db.client = get_connection('testdb')
db.client["CIA"]
db.client["Employees_Info"]
db.client.CIA.Employees_Info.insert_one({"name": "user_Name","password": "week"})
def test_ping():
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"message": "Conectado"}
def test_login():
data = {"username":'user_name', 'password':'week'}
response = client.post("/login", data=data)
assert response.headers['Content-Type'] == 'application/json'
assert response.status_code == status.HTTP_200_OK
db.client.disconnect()

FastAPI AttributeError: 'job_board' object has no attribute 'query'

I'm trying to build a simple job board using Python, FastAPI and Async sqlalchemy by following the official FastAPI documentation.Problem occurring when I try to retrieve the job by the ID from the database, It's keep giving me this error AttributeError: 'job_board' object has no attribute 'query' when I hit the "/get/{id}"endpoint.
The Following is hopefully a minimum reproducible code segment:
schemas/jobs.py
from typing import Optional
from pydantic import BaseModel
from datetime import date, datetime
class JobBase(BaseModel):
title: Optional[str] = None
company_name: Optional[str] = None
company_url: Optional[str] = None
location: Optional[str] = "remote"
description: Optional[str] = None
date_posted: Optional[date] = datetime.now().date()
class JobCreate(JobBase):
title: str
company_name: str
location: str
description: str
class ShowJob(JobBase):
title: str
company_name: str
company_url: Optional[str]
location: str
date_posted: date
description: str
class Config():
orm_mode = True
routes/route_jobs.py
from fastapi import APIRouter, HTTPException, status
from fastapi import Depends
from sqlalchemy.orm.session import Session
from db.repository.job_board_dal import job_board
from db.models.jobs import Job as model_job
from schemas.jobs import JobCreate, ShowJob
from db.repository.job_board_dal import Job
from depends import get_db
router = APIRouter()
#router.post("/create-job",response_model=ShowJob)
async def create_user(Job: JobCreate, jobs: Job = Depends(get_db)):
owner_id = 1
return await jobs.create_new_job(Job, owner_id)
#router.get("/get/{id}")
def retreive_job_by_id(id:int, session: Session = Depends(get_db)):
#print(type(session))
job_id = job_board.retrieve_job(session, id=id)
if not job_id:
HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return job_id
db/repository/job_board_dal.py
from sqlalchemy.orm import Session
from schemas.users import UserCreate
from schemas.jobs import JobCreate
from db.models.users import User
from db.models.jobs import Job
from core.hashing import Hasher
class job_board():
def __init__(self, db_session: Session):
self.db_session = db_session
async def register_user(self, user: UserCreate):
new_user = User(username=user.username,
email=user.email,
hashed_password=Hasher.get_password_hash(user.password),
is_active = False,
is_superuser=False
)
self.db_session.add(new_user)
await self.db_session.flush()
return new_user
async def create_new_job(self, job: JobCreate, owner_id: int):
new_job = Job(**job.dict(), owner_id = owner_id)
self.db_session.add(new_job)
await self.db_session.flush()
return new_job
def retrieve_job(db: Session, id:int):
item = db.query(Job).filter(Job.id == id).first()
return item
depends.py
from db.session import async_session
from db.repository.job_board_dal import job_board
async def get_db():
async with async_session() as session:
async with session.begin():
yield job_board(session)
I did try a lot of things I even try to retrieve with a separate async session which also gives me the AttributeError: 'AsyncSession' object has no attribute 'query'.
Any help would be much appreciated.
get_db doesnt return a session, it returns an instance of job_board. So when you do retreive_job_by_id(id:int, session: Session = Depends(get_db)) your are setting session to be a class of job_board. So when you call job_id = job_board.retrieve_job(session, id=id) you are passing session which is an instance of job_board. So when you do item = db.query(Job) db here is an instance of Job_board and job_board doesnt have a method called query. Instead you probably just want to update retrieve job to utilise the job_board class instance to access its session.
def retrieve_job(self, id:int):
item = self.db_session.query(Job).filter(Job.id == id).first()
return item
in your router you can make it more readable as
#router.get("/get/{id}")
def retreive_job_by_id(id:int, job_board = Depends(get_db)):
#print(type(session))
job_id = job_board.retrieve_job(job_board, id=id)
if not job_id:
HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Job with id {id} does not exist")
return job_id

Unit test of endpoint decorated with Flask #auth.login_required decorator

I'm using Flask-HTTPAuth for authentication, Flask-login for login and want to add unit tests for endpoint decorated with #auth.login_required(scheme="Bearer"):
endpoints.py
class UserData(Resource):
#auth.login_required
def get(self, user_id):
user = db_models.User.query.get(user_id)
return result
authent.py
#auth.verify_token
def verify_token(token):
data = token_serializer.loads(token)
return data.get("username", False)
But got stuck with mocking (it doesn't work):
test.py
#mock.patch("myapp.auth")
#mock.patch("myapp.db_models.User")
def test_get_user(self, mock_user, mock_login):
with app.test_client() as client:
mock_login.verify_token.return_value = TEST_USER["username"]
mock_login.login_required.return_value = True
mock_user.query.get.return_value = TEST_USER
response = client.get("/user/100000")
self.assertIsNotNone(response)
self.assertIsInstance(response.get_json(), dict)
And the second approach:
test.py
#mock.patch("myapp.auth.login_required", return_value = True)
#mock.patch("myapp.db_models.User")
def test_get_user(self, mock_user, mock_login):
with app.test_client() as client:
mock_user.query.get.return_value = TEST_USER
response = client.get("/user/100000")
self.assertIsNotNone(response)
self.assertIsInstance(response.get_json(), dict)
Could you please help me to figure out how to do it in the right way?
Thank you for your help!

Python: New Object instance on every call of function

I'm trying to update a base class with a session token and user id for long polling.
Every time I call my function I create a new instance which calls a login function, that I don't want to happen.
I only want to call the login() method when the value is None
How do I return the instance of apiclient after the session token is set to use with the function for get_balance??
client.py
from __future__ import absolute_import, unicode_literals
import requests
import os
from matchbook import endpoints
class BaseClient(object):
def __init__(self, username, password=None, locale=None):
self.username = username
self.password = password
self.locale = locale
self.url_beta = 'https://beta.matchbook.com'
self.urn_main = '/bpapi/rest/'
self.session = requests.Session()
self.session_token = None
self.user_id = None
def set_session_token(self, session_token, user_id):
self.session_token = session_token
self.user_id = user_id
class APIClient(BaseClient):
def __init__(self, username, password=None):
super(APIClient, self).__init__(username, password)
self.login = endpoints.Login(self)
self.account = endpoints.Account(self)
def __repr__(self):
return '<APIClient [%s]>' % self.username
def __str__(self):
return 'APIClient'
get_bal.py
from client import APIClient
from celery import shared_task
def get_client():
apiclient = APIClient(username, password)
if apiclient.session_token is None:
apiclient.login()
session_token = apiclient.session_token
user_id = apiclient.user_id
apiclient.set_session_token(session_token,user_id)
else:
print('session token assigned',apiclient.session_token, apiclient.user_id)
return apiclient
#shared_task
def get_balance():
apiclient = get_client() *to call instance after login is set*
r = apiclient.account.get_account()
print(r)
You are creating a new instance of APIClient each time you call get_client(), which is what happens each time get_balance() get called.
You need to maintain an instance of the APIClient outside of the function scope for you to carry over your program and update your get_balance() to not call get_client() each time:
def get_balance(apiclient):
r = apiclient.account.get_account()
print(r)
def main():
apiclient = get_client()
get_balance(apiclient) # pass instance of APIClient in as an argument
Another note in your get_client() function, since both of the if conditions are the opposite of each other, just wrap them in an if... else block:
def get_client():
apiclient = APIClient(username, password)
if apiclient.session_token is None:
apiclient.login()
session_token = apiclient.session_token
user_id = apiclient.user_id
apiclient.set_session_token(session_token,user_id)
else:
print('session token assigned',apiclient.session_token, apiclient.user_id)
return apiclient
All that said, a much more OOP way would be to bake the get_balance() into the APIClient as an instance method so you don't even need to worry about the instance:
class APIClient(BaseClient):
...
def get_balance(self):
print(self.account.get_account())
# Then just call the get_balance() anywhere:
apiclient = get_client()
apiclient.get_balance()

Categories