Write a guard

Protect a route or a controller with an authorisation check.

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"}
    )