Declare user_have_permission dependency for validating permissions
In this topic we will use at first fake database and very modest example of user_have_permission dependency for FastAPI, then write full example, that covers SQLAlchemy integration and more realistic flows.
# main.pyimportrandomfromfastapiimportDepends,FastAPI,HTTPExceptionfromrespoimportRespoClientfrom.respo_modelimportRespoModelRESPO_MODEL=RespoModel.get_respo_model()respo_client_admin=RespoClient()respo_client_admin.add_role(RESPO_MODEL.ROLES.ADMIN,RESPO_MODEL)respo_client_no_roles=RespoClient()fake_users=[{"name":"Peter","respo_field":respo_client_admin},{"name":"Sara","respo_field":respo_client_no_roles},]app=FastAPI()defuser_have_permission(permission):definner_user_have_permission():# normally we would get user by headers or param etc from# another dependency, here we use random user from listuser=random.choice(fake_users)respo_client:RespoClient=user["respo_field"]ifnotrespo_client.has_permission(permission,RESPO_MODEL):raiseHTTPException(403)returnuserreturninner_user_have_permission@app.get("/")defget_user(user=Depends(user_have_permission(RESPO_MODEL.PERMS.USER__READ_ALL)),):return{"name":user["name"]}
Run FastAPI app
Put above code in main.py and run
1
uvicorn main:app --reload
At the address localhost:8000/docs you can test the endpoint.
Click on Execute few times in a row to see expected result.
Expected result
Now, in user_have_permission dependency we check using RESPO_MODEL declared in previous chapters if a user have specific permission (or with more precision if a user have at least one role that includes this permission). Since we have only 2 users and we get them by random choice from list, sometimes we get 200 with
123
{"name":"Peter"}
when Peter is choosed and sometimes 403:
123
{"detail":"Forbidden"}
when Sara that has no permission.
Declare user_have_permission dependency with SQLAlchemy user table
# main.pyimportrandomfromfastapiimportDepends,FastAPI,HTTPExceptionimportasynciofromrespoimportRespoClientfromdataclassesimportdataclass,fieldfrom.respo_modelimportRespoModelfromsqlalchemyimportColumn,Integer,String,selectfromsqlalchemy.ext.asyncioimportAsyncSession,create_async_enginefromsqlalchemy.ormimportregistryfromsqlalchemy.orm.sessionimportsessionmakerfromrespoimportRespoClientfromrespo.fields.sqlalchemyimportSQLAlchemyRespoFieldfromsqlalchemy.sqlimportfuncRESPO_MODEL=RespoModel.get_respo_model()async_engine=create_async_engine("sqlite+aiosqlite:///",pool_pre_ping=True)async_session=sessionmaker(async_engine,expire_on_commit=False,class_=AsyncSession# type: ignore)mapper_registry=registry()@mapper_registry.mapped@dataclassclassUser:__tablename__="user_model"__sa_dataclass_metadata_key__="sa"id:int=field(init=False,metadata={"sa":Column(Integer,primary_key=True)},)name:str=field(metadata={"sa":Column(String(254),nullable=False,unique=True,index=True)})respo_field:RespoClient=field(default_factory=RespoClient,metadata={"sa":Column(SQLAlchemyRespoField,nullable=False,server_default="")},)app=FastAPI()asyncdefdb_setup():asyncwithasync_engine.begin()asconn:awaitconn.run_sync(mapper_registry.metadata.drop_all)awaitconn.run_sync(mapper_registry.metadata.create_all)asyncwithasync_session()assession:session:AsyncSessionrespo_client_admin=RespoClient()respo_client_admin.add_role(RESPO_MODEL.ROLES.ADMIN,RESPO_MODEL)respo_client_no_roles=RespoClient()user_peter=User("Peter",respo_client_admin)user_sara=User("Sara",respo_client_no_roles)session.add(user_peter)session.add(user_sara)awaitsession.commit()defuser_have_permission(permission):asyncdefinner_user_have_permission(db_setup=Depends(db_setup)):# normally we would get user by access_token from header or# another dependency, here we get random user from databaseasyncwithasync_session()assession:session:AsyncSessionuser:User=((awaitsession.execute(select(User).order_by(func.random()).limit(1))).scalars().one())ifnotuser.respo_field.has_permission(permission,RESPO_MODEL):raiseHTTPException(403)returnuserreturninner_user_have_permission@app.get("/")defget_user(user:User=Depends(user_have_permission(RESPO_MODEL.PERMS.USER__READ_ALL)),):return{"name":user.name}
Using SQLAlchemy RespoField
In SQLAlchemy user model we use custom field powered by respo: SQLAlchemyRespoField.
It is responsible for serialization and deserialization from respo.RespoClient to string in database back and forth. So we write:
12345678
respo_field:RespoClient=field(default_factory=RespoClient,metadata={"sa":Column(SQLAlchemyRespoField,nullable=False,server_default="")},)# or without extra typing supportrespo_field=Column(SQLAlchemyRespoField,nullable=False,server_default="")
Note
Note, this RespoClient-like field is mutable, so any direct changes to attributes won't trigger SQLAlchemy machinery and changes won't be saved to database. The only methods that trigger so called has_changed() event are add_role and remove_role!
Scaling
After above setup scaling is very easy using the same dependency factory for any new endpoint (with different permission). For example we can continue with:
create field respo_field based on custom field SQLAlchemyRespoField in our User model in database, so we can store user's roles in database
create user_have_permission dependency factory that is resposible for additional checking if user has permission to requested resource. Note, it may differ in certain scenarios and use cases, but basically user should have access either when is owner/creator of resource whatever that means, or has role that allow it.