I am looking to implement a generic approvals system in my FastAPI application. The system should enforce an approval flow on endpoints it is added to, such that:
- Users who do not have the required privileges can call a protected endpoint, the request is defered and stored in a database.
- Privileged users can grant approval to these requests.
- When a request is approved, the protected operation is executed – immediately.
I am looking for a solution that is non-invasive to the endpoint itself, ideally a dependency i can add to the endpoint. E.g:
@router.delete("/survey/survey_id", dependencies=[Depends(requires_approval(min_role=Role.ADMIN))])
def delete_survey(survey_id: int, repo: SurveyRepo):
repo.delete(survey_id)
However, the issue I have is around how to best deal with executing the protected operation upon granting approval. I am an avid user of dependencies in FastAPI and use them on many of my endpoints to govern business logic, e.g:
@router.delete("/survey/survey_id", dependencies=[Depends(assert_status(SurveyStatus.IN_PROGRESS))])
def delete_survey(survey_id: int, repo: SurveyRepo):
repo.delete(survey_id)
Ideally, on approval, I would call the original endpoint again with the original request so that the exact same dependencies are executed. I think this route is much more scalable and less error prone than some examples I have seen using the command pattern e.g:
@router.delete(“/survey/{survey_id}”, dependencies=[Depends(assert_status(SurveyStatus.IN_PROGRESS)), Depends(requires_approval(min_role=Role.ADMIN))])
def delete_survey(survey_id: int, repo: SurveyRepo):
repo.delete(survey_id)
class ActionCommand(BaseModel):
action_type: str
custom: Dict
def execute(self):
# Somehow call all dependencies e.g. assert_status(SurveyStatus.IN_PROGRESS)
repo.delete(self.custom['survey_id'])
@router.post("/approve/{approval_id}")
def approve(approval_id: str, cmd: ActionCommand = Depends(get_action_cmd)):
cmd.execute()
This method means doubling up business logic controlled in the dependency array of the endpoint, which also means doubling up on tests for a single operation.
Re-invoking Endpoints with Original Dependencies:
I am contemplating a method that allows for the re-invocation of the original endpoint after approval so that all associated dependencies (such as assert_status) are naturally re-executed without any duplication of logic or additional overhead.
Possible Approaches I’m Considering:
-
3 part flow – When an approval is approved, the action is not executed until the original requester calls the protected endpoint again. The system now sees there is a granted approval so the action is carried out. However, this degrades the user experience by requiring the user to carry out the action again.
-
Internal Request Simulation: Using a tool like httpx to programmatically simulate the original HTTP request, using the original request serialised and stored in a database. However, this feels like it might introduce unnecessary complexity and potential performance overhead.
-
Not using dependencies on protected endpoints and adding the logic to the service layer. This is not ideal as it means changing many endpoints and introduces potential developer error if dependencies are used in the endpoint and not the service layer.
Any better ideas / best practices would be greatly appreciated.
Thank you in advance for your help!