A guard decides whether a request may proceed. It runs after routing and before the handler, and it is the right place for authorisation.
A guard#
from bustan import Injectable, Guard, ExecutionContext
@Injectable()
class ApiKeyGuard(Guard):
def __init__(self, settings: Settings) -> None:
self.settings = settings
async def can_activate(self, context: ExecutionContext) -> bool:
key = context.request.headers.get("x-api-key")
return key is not None and key == self.settings.api_key
Guards are providers, so they get dependencies injected like anything else.
Apply it#
To a single route:
from bustan import UseGuards
@Controller("/tasks")
class TaskController:
@UseGuards(ApiKeyGuard)
@Post("/")
async def create(self, payload: CreateTask) -> dict[str, int]: ...
To a whole controller:
@UseGuards(ApiKeyGuard)
@Controller("/tasks")
class TaskController: ...
Globally:
app = Bustan(AppModule, guards=[ApiKeyGuard])
Returning a reason#
Returning False produces a bare 403. To say why:
from bustan import ForbiddenError
async def can_activate(self, context: ExecutionContext) -> bool:
if "x-api-key" not in context.request.headers:
raise ForbiddenError("Missing x-api-key header")
...
Reading route metadata#
To let routes declare what they require:
from bustan import SetMetadata
@SetMetadata("roles", ["admin"])
@Delete("/{id}")
async def remove(self, id: int) -> None: ...
async def can_activate(self, context: ExecutionContext) -> bool:
roles = context.get_metadata("roles") or []
return not roles or context.user.role in roles
Testing past a guard#
The test harness does not bypass guards. Authenticate instead:
async with TestHarness(TaskModule) as harness:
response = await harness.post(
"/tasks/", json={"title": "x"}, headers={"x-api-key": "test-key"}
)