Guards#

Guards are callables that receive two arguments - connection, which is the ASGIConnection instance, and route_handler, which is a copy of the BaseRouteHandler. Their role is to authorize the request by verifying that the connection is allowed to reach the endpoint handler in question. If verification fails, the guard should raise an HTTPException, usually a NotAuthorizedException with a status_code of 401.

To illustrate this we will implement a rudimentary role based authorization system in our Starlite app. As we have done for authentication, we will assume that we added some sort of persistence layer without actually specifying it in the example.

We begin by creating an Enum with two roles - consumer and admin:

from enum import Enum


class UserRole(str, Enum):
    CONSUMER = "consumer"
    ADMIN = "admin"

Our User model will now look like this:

from pydantic import BaseModel, UUID4
from enum import Enum


class UserRole(str, Enum):
    CONSUMER = "consumer"
    ADMIN = "admin"


class User(BaseModel):
    id: UUID4
    role: UserRole

    @property
    def is_admin(self) -> bool:
        """Determines whether the user is an admin user"""
        return self.role == UserRole.ADMIN

Given that the User model has a “role” property we can use it to authorize a request. Let’s create a guard that only allows admin users to access certain route handlers and then add it to a route handler function:

from starlite import ASGIConnection, BaseRouteHandler, NotAuthorizedException
from pydantic import BaseModel, UUID4
from starlite import post
from enum import Enum


class UserRole(str, Enum):
    CONSUMER = "consumer"
    ADMIN = "admin"


class User(BaseModel):
    id: UUID4
    role: UserRole

    @property
    def is_admin(self) -> bool:
        """Determines whether the user is an admin user"""
        return self.role == UserRole.ADMIN


def admin_user_guard(connection: ASGIConnection, _: BaseRouteHandler) -> None:
    if not connection.user.is_admin:
        raise NotAuthorizedException()


@post(path="/user", guards=[admin_user_guard])
def create_user(data: User) -> User:
    ...

Thus, only an admin user would be able to send a post request to the create_user handler.

Guard scopes#

Guards can be declared on all levels of the app - the Starlite instance, routers, controllers and individual route handlers:

from starlite import ASGIConnection, Controller, Router, Starlite, BaseRouteHandler


def my_guard(connection: ASGIConnection, handler: BaseRouteHandler) -> None:
    ...


# controller
class UserController(Controller):
    path = "/user"
    guards = [my_guard]

    ...


# router
admin_router = Router(path="admin", route_handlers=[UserController], guards=[my_guard])

# app
app = Starlite(route_handlers=[admin_router], guards=[my_guard])

The deciding factor on where to place a guard is on the kind of access restriction that are required: do only specific route handlers need to be restricted? An entire controller? All the paths under a specific router? Or the entire app?

As you can see in the above examples - guards is a list. This means you can add multiple guards at every layer. Unlike dependencies , guards do not override each other but are rather cumulative. This means that you can define guards on different levels of your app, and they will combine.

The route handler “opt” key#

Occasionally there might be a need to set some values on the route handler itself - these can be permissions, or some other flag. This can be achieved with the opts kwarg of route handler

To illustrate this lets say we want to have an endpoint that is guarded by a “secret” token, to which end we create the following guard:

from starlite import ASGIConnection, BaseRouteHandler, NotAuthorizedException, get
from os import environ


def secret_token_guard(
    connection: ASGIConnection, route_handler: BaseRouteHandler
) -> None:
    if (
        route_handler.opt.get("secret")
        and not connection.headers.get("Secret-Header", "")
        == route_handler.opt["secret"]
    ):
        raise NotAuthorizedException()


@get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")})
def secret_endpoint() -> None:
    ...