Custom Authorization Policies
The @policy directive lets you enforce authorization rules that the router cannot evaluate on its
own, such as resource ownership, tenant isolation, or any business rule that lives in your own
systems. Instead of hard-coding that logic in every subgraph, you declare a policy name in your
schema and let a coprocessor decide
whether the current request satisfies it.
@policy complements @authenticated and @requiresScopes,
which are described in the general Authorization guide. Use
@policy when the decision cannot be derived from a JWT scopes alone.
For the complete configuration reference, see
authorization configuration.
How It Works
- Collect - before calling the
graphql.analysiscoprocessor stage, the router walks the requested operation and publishes every@policypolicy it depends on to thehive::authorization::required_policiesrequest context key, mapped tonull. - Decide - your coprocessor (or a plugin) looks at
the request and overwrites each entry with
trueorfalse. - Enforce - after the stage returns, the router applies the decisions. Any policy left
null, or missing from the answer entirely, is treated as denied. Unauthorized fields are then handled exactly like@authenticated/@requiresScopesviolations, following your configuredauthorization.directives.unauthorized.mode.
Defining Policies in Your Schema
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.6", import: ["@policy"])
type Query {
users: [User] @policy(policies: [["admin"], ["read_users", "internal"]])
}
type Product {
upc: String!
# Requires the "read_inventory" policy to be granted
inStock: Boolean @policy(policies: [["read_inventory"]])
}Just like @requiresScopes, policies is a list of lists: an OR of AND groups.
- Single list (AND logic): every policy in the list must be granted.
- Multiple lists (OR logic): at least one full list must be granted.
In the example above, users is allowed if the admin policy is granted, or if both
read_users and internal are granted.
The router publishes every policy referenced by an operation, including policies that belong to an OR group that ultimately won't be needed. Your coprocessor doesn't need to know which combination is enough - deciding that is the router's job.
When multiple directives protect the same field, all of them must be satisfied - @policy is
combined with @authenticated/@requiresScopes using AND, the same way multiple auth directives
on one field already compose in the Authorization guide.
Wiring Up a Coprocessor
Enable the graphql.analysis stage and include the request context, so your coprocessor can read
hive::authorization::required_policies and reply with its decisions:
coprocessor:
url: http://127.0.0.1:8081/coprocessor
protocol: http1
stages:
graphql:
analysis:
include:
context: true
authorization:
directives:
enabled: true
unauthorized:
mode: filter # Or 'reject'Coprocessor Input
For an operation that selects Query.users, the graphql.analysis stage payload includes the
policies the router needs a decision on, each initialized to null:
{
"version": 1,
"stage": "graphql.analysis",
"control": "continue",
"id": "...",
"context": {
"hive::authorization::required_policies": {
"admin": null,
"read_users": null,
"internal": null
}
}
}Coprocessor Output
Your coprocessor looks up the current user (from headers, a session store, an internal service - whatever your business logic requires) and answers by overwriting the relevant entries:
{
"version": 1,
"control": "continue",
"context": {
"hive::authorization::required_policies": {
"admin": false,
"read_users": true,
"internal": true
}
}
}Given this answer, the read_users AND internal group is fully satisfied, so users is
authorized even though admin was denied.
If your coprocessor omits a policy from its response, or explicitly answers
with null, the router treats it as denied. There is no way to leave a
policy undecided and still access the field it protects.
Handling Denied Policies
A denied policy is handled exactly like any other unauthorized field access - the field is filtered
out (or the whole request is rejected in reject mode) and an error is returned:
{
"data": {
"users": null
},
"errors": [
{
"message": "Unauthorized field or type",
"extensions": {
"code": "UNAUTHORIZED_FIELD_OR_TYPE",
"affectedPath": "users"
}
}
]
}See Handling Authorization Errors
for the full behavior of filter and reject modes.