Skip to content

Security

GraftQL's router accepts auxiliary hooks that enable access control at multiple levels of the API.

Table Control

In most cases this is all you need: it controls access per table.

permission_getter

If you want the same access-control logic across your API, define a function that receives the model class (a GraftModel subclass), the request context, and the request type (e.g. 'query', 'mutation'), and returns a boolean indicating whether the request is permitted.

async def get_context(request, response):
    # In a real app you would derive the user from a JWT or session cookie.
    user = {"categories": ["A", "B"], "classes": ["Resource", "Owner"]}
    return {"user": user}

async def permission_getter(module_class, context, request_type):
    """Allow access only to models listed in the user's `classes`."""
    return module_class.__name__ in context["user"]["classes"]

Note

These hooks should be passed to GraftRouter.

app = FastAPI(title="GraftQL auth tutorial")
router = GraftRouter(
    engine=engine,
    dl_filter=dl_filter,
    query_filter=query_filter,
    permission_getter=permission_getter,
    context_getter=get_context,
)
app.include_router(router, prefix="/graphql")

for more details about context:Strawberry.

Tip

You can also define permission_getter on a GraftModel subclass to make the permission logic model-specific.

If you set a generic permission_getter in router and also in a model, the model permission_getter has priority above router's permission_getter

Rows Control

For more complex access control (e.g. row-level filtering), you can provide additional query filter hooks to GraftRouter.

query_filter

Adds a filter to every top-level query. In our example:

async def get_context(request, response):
    # In a real app you would derive the user from a JWT or session cookie.
    user = {"categories": ["A", "B"], "classes": ["Resource", "Owner"]}
    return {"user": user}

def query_filter(model, context):
    """Add a WHERE clause to top-level queries based on the requesting user's allowed categories."""
    if model.__name__ == "Resource":
        return model.category.in_(context["user"]["categories"])
    return True

Note

This function should return a SQLAlchemy filter statement.

dl_filter

Post-filters the results returned by DataLoaders (relationship fields). In our example:

async def get_context(request, response):
    # In a real app you would derive the user from a JWT or session cookie.
    user = {"categories": ["A", "B"], "classes": ["Resource", "Owner"]}
    return {"user": user}

def dl_filter(data, context):
    """Post-filter DataLoader results so relationship fields respect category access."""
    user = context["user"]
    if data and isinstance(data, list) and len(data) > 0 and isinstance(data[0], Resource):
        return [d for d in data if d.category in user["categories"]]
    return data

Note

It receives the loaded rows and the context, and returns the filtered rows.