Skip to content
Open
737 changes: 565 additions & 172 deletions docs/core/authorization.md

Large diffs are not rendered by default.

219 changes: 158 additions & 61 deletions docs/core/graphql-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ This query can take a optional input `params` of type `SessionQueryInput` which
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `roles` | Array of string with valid roles | false |
| `scope` | List of openID scopes. If not present default scopes ['openid', 'email', 'profile'] is used | false |
| `required_permissions` | Array of `{resource, scope}` pairs evaluated with AND semantics against the caller's principal. Any deny or unmatched pair returns `unauthorized`. See [Authorization (FGA)](./authorization). | false |

It returns `AuthResponse` type with the following keys.

Expand All @@ -135,10 +134,7 @@ It returns `AuthResponse` type with the following keys.
```graphql
query {
session(params: {
roles: ["admin"],
required_permissions: [
{ resource: "dashboard", scope: "view" }
]
roles: ["admin"]
}) {
message
access_token
Expand Down Expand Up @@ -205,7 +201,6 @@ Query to validate the given jwt token. This query needs input `params` of type `
| `token_type` | Type of token that needs to be validated. One of `access_token`, `refresh_token`, `id_token`. | `true` |
| `token` | JWT string | `true` |
| `roles` | Array of roles to validate the JWT token for | `false` |
| `required_permissions` | Array of `{resource, scope}` pairs evaluated with AND semantics against the JWT's principal. Any deny or unmatched pair returns `unauthorized`. See [Authorization (FGA)](./authorization). | `false` |

It returns `ValidateJWTTokenResponse` type with the following keys.

Expand All @@ -222,10 +217,7 @@ It returns `ValidateJWTTokenResponse` type with the following keys.
query {
validate_jwt_token(params: {
token_type: "access_token",
token: "some jwt token",
required_permissions: [
{ resource: "docs", scope: "read" }
]
token: "some jwt token"
}) {
is_valid
claims
Expand All @@ -242,7 +234,6 @@ Query to validate the browser session. This query needs input `params` of type `
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `cookie` | Browser cookie. Either the browser HTTP cookie is present or this parameter must be supplied. | `false` |
| `roles` | Array of roles to validate session for | `false` |
| `required_permissions` | Array of `{resource, scope}` pairs evaluated with AND semantics against the cookie's principal. Any deny or unmatched pair returns `unauthorized`. See [Authorization (FGA)](./authorization). | `false` |

It returns `ValidateSessionResponse` type with the following keys.

Expand All @@ -257,39 +248,65 @@ It returns `ValidateSessionResponse` type with the following keys.
```graphql
query {
validate_session(params: {
cookie: "",
required_permissions: [
{ resource: "docs", scope: "write" }
]
cookie: ""
}) {
is_valid
}
}
```

### `permissions`
### Authorization (client-facing)

Query the flat list of `(resource, scope)` pairs the calling principal has been granted. Requires a valid session or bearer token.
These queries answer authorization questions against the embedded FGA (ReBAC) engine. They require a valid session or bearer token. The subject is pinned server-side from the caller's token/cookie; the optional `user` is honored only for super-admins or when it equals the caller's own subject. See [Authorization (FGA)](./authorization) for the full model.

**Response**
#### `check_permissions`

| Key | Description |
| ---------- | ---------------------------------------- |
| `resource` | Resource name granted to the principal. |
| `scope` | Scope name granted on that resource. |
Evaluate one or more permission checks in a single call. Returns `{ results { relation object allowed } }`, positionally aligned with `checks` and echoing each pair.

**Sample Query**
Input `CheckPermissionsInput`:

| Key | Description | Required |
| -------- | -------------------------------------------------------------------------------------------------------- | -------- |
| `checks` | `[PermissionCheckInput!]!` — each `{ relation!, object!, contextual_tuples? }`. | `true` |
| `user` | Subject ("type:id", bare id → `user:<id>`). Honored only for super-admins or self; defaults to the caller. | `false` |

```graphql
query {
check_permissions(params: {
checks: [
{ relation: "can_view", object: "document:1" },
{ relation: "can_edit", object: "document:1" }
]
}) {
results { relation object allowed }
}
}
```

#### `list_permissions`

List the objects of a given type on which the subject holds a relation. Returns `{ objects }`.

Input `ListPermissionsInput`:

| Key | Description | Required |
| ------------- | -------------------------------------------------------------------------------------------------------- | -------- |
| `relation` | Relation to list for (e.g. `can_view`). | `true` |
| `object_type` | Object type to enumerate (e.g. `document`). | `true` |
| `user` | Subject ("type:id", bare id → `user:<id>`). Honored only for super-admins or self; defaults to the caller. | `false` |

```graphql
query {
permissions {
resource
scope
list_permissions(params: {
relation: "can_view",
object_type: "document"
}) {
objects
}
}
```

See [Authorization (FGA)](./authorization) for the full model.
`FgaTupleInput` (used by `contextual_tuples` above and by the admin operations) is `{ user: String!, relation: String!, object: String! }`.

### `_user`

Expand Down Expand Up @@ -1673,60 +1690,140 @@ mutation {

### Authorization (admin)

Manage the FGA policy graph. All require super-admin authentication (cookie or `X-Authorizer-Admin-Secret`). See [Authorization (FGA)](./authorization) for the conceptual model.
Manage the embedded FGA (ReBAC) engine: the authorization model and the relationship tuples. All require super-admin authentication (cookie or `X-Authorizer-Admin-Secret`). All admin authorization operations are namespaced with the `_fga_` prefix. See [Authorization (FGA)](./authorization) for the conceptual model.

All admin authorization operations are namespaced with the `_authz_` prefix.
#### `_fga_write_model`

#### `_authz_add_resource` / `_authz_update_resource` / `_authz_delete_resource` / `_authz_resources`
Write (replace) the authorization model from an FGA DSL string. Returns `FgaModel` `{ id, dsl }`.

Manage **resources** (the nouns the application protects).
```graphql
mutation {
_fga_write_model(params: {
dsl: """
model
schema 1.1
type user
type document
relations
define viewer: [user]
define editor: [user]
"""
}) {
id
dsl
}
}
```

#### `_fga_get_model`

Read the current authorization model. Returns `FgaModel` `{ id, dsl }`.

```graphql
mutation { _authz_add_resource(params: { name: "docs" }) { id name } }
mutation { _authz_update_resource(params: { id: "<id>", name: "documents" }) { id name } }
mutation { _authz_delete_resource(id: "<id>") { message } }
query { _authz_resources(params: { pagination: { limit: 25, page: 1 } }) {
pagination { total limit page }
resources { id name }
} }
query {
_fga_get_model {
id
dsl
}
}
```

#### `_authz_add_scope` / `_authz_update_scope` / `_authz_delete_scope` / `_authz_scopes`
#### `_fga_write_tuples`

Manage **scopes** (verbs / actions). Same input/output shape as resources.
Write relationship tuples. Returns `Response` `{ message }`.

#### `_authz_add_policy` / `_authz_update_policy` / `_authz_delete_policy` / `_authz_policies`
Input `params.tuples` is `[FgaTupleInput!]!`, each `{ user: String!, relation: String!, object: String! }`.

Manage **policies** (principal selectors).
```graphql
mutation {
_fga_write_tuples(params: {
tuples: [
{ user: "user:1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", relation: "viewer", object: "document:1" }
]
}) {
message
}
}
```

#### `_fga_delete_tuples`

Delete relationship tuples. Same input shape as `_fga_write_tuples`. Returns `Response` `{ message }`.

```graphql
mutation {
_authz_add_policy(params: {
name: "user-role-can-read",
type: "role",
targets: [{ target_type: "role", target_value: "user" }],
logic: "positive",
decision_strategy: "affirmative"
}) { id name }
_fga_delete_tuples(params: {
tuples: [
{ user: "user:1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", relation: "viewer", object: "document:1" }
]
}) {
message
}
}
```

`type` accepts `role`, `user`, or `attribute`. `target_value` for `role` policies must be a configured role (see `--roles`). `target_value` for `user` policies is the user's **ID** (not email).
#### `_fga_read_tuples`

Read stored tuples with pagination. Returns `FgaTuples` `{ tuples { user relation object }, continuation_token }`.

#### `_authz_add_permission` / `_authz_update_permission` / `_authz_delete_permission` / `_authz_permissions`
Input `params`:

Bind a resource + scopes + policies into a single permission row.
| Key | Description | Required |
| -------------------- | ------------------------------------------------ | -------- |
| `page_size` | Maximum number of tuples to return. | `false` |
| `continuation_token` | Token from a previous response to fetch the next page. | `false` |

```graphql
mutation {
_authz_add_permission(params: {
name: "docs-read",
resource_id: "<resource-id>",
scope_ids: ["<read-scope-id>"],
policy_ids: ["<policy-id>"],
decision_strategy: "affirmative"
}) { id name }
query {
_fga_read_tuples(params: { page_size: 50 }) {
tuples {
user
relation
object
}
continuation_token
}
}
```

#### `_fga_list_users`

List the users that have a given relation on an object (admin only).

```graphql
query {
_fga_list_users(params: {
relation: "viewer",
object: "document:1"
}) {
users
}
}
```

`decision_strategy` is one of `affirmative` (default), `consensus`, or `unanimous`. See [Authorization §6](./authorization#6-decision-strategies).
#### `_fga_expand`

Expand the relationship/userset tree for a relation on an object (admin only). Useful for debugging how access is derived.

```graphql
query {
_fga_expand(params: {
relation: "viewer",
object: "document:1"
}) {
tree
}
}
```

#### `_fga_reset`

Delete the authorization model, all of its versions, and all tuples. Returns `Response` `{ message }`. The operation is refused while any tuple still exists, so delete tuples first.

```graphql
mutation {
_fga_reset {
message
}
}
```
60 changes: 29 additions & 31 deletions docs/core/metrics-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,41 +132,48 @@ raised. Alert at the rate that distinguishes the two for your traffic
profile. See [GraphQL hardening](./security#graphql-hardening) for the
limits themselves.

### Authorization Metrics
### Authorization (FGA) Metrics

These metrics cover the embedded fine-grained authorization (OpenFGA) engine. They
appear only once FGA is enabled and the corresponding operation has run at least
once. See [Authorization (FGA)](./authorization).

| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `authorizer_required_permissions_checks_total` | Counter | `endpoint`, `outcome` | Per-endpoint outcome of `required_permissions` on session APIs. |
| `authorizer_authz_checks_total` | Counter | `result` | Every `CheckPermission` call. `result=allowed\|denied\|unmatched\|error`. |
| `authorizer_authz_unmatched_total` | Counter | — | `CheckPermission` calls that found no permission row for `(resource, scope)`. |
| `authorizer_authz_check_duration_seconds` | Histogram | — | End-to-end `CheckPermission` latency. |
| `authorizer_fga_checks_total` | Counter | `operation`, `result` | Access decisions from `check_permissions`. The headline metric for adoption and denial/error alerting. |
| `authorizer_fga_check_duration_seconds` | Histogram | `operation` | Latency of the client-facing FGA engine reads. |
| `authorizer_fga_operations_total` | Counter | `operation`, `result` | Non-decision FGA operations (model/tuple management, enumeration, reset) by outcome. |

**`required_permissions_checks_total` labels:**
**`authorizer_fga_checks_total` labels:**

| Label | Values |
| ----- | ------ |
| `endpoint` | `session`, `validate_session`, `validate_jwt_token` |
| `outcome` | `granted` (all listed permissions allowed) · `denied` (one or more denied) · `not_requested` (caller omitted the field) · `error` (CheckPermission errored — DB/validation) |
|---|---|
| `operation` | `check_permissions` (each supplied pair is counted individually) |
| `result` | `allowed` · `denied` · `error` (the engine call failed — fail-closed, so the caller was denied) |

**Use cases:**
**`authorizer_fga_check_duration_seconds`** `operation`: `check_permissions` · `list_permissions`. The histogram's `_count` also gives you a call rate per operation for free.

- `outcome="denied"` rising on a given endpoint = either a policy gap or an attacker probe. Cross-check with `authorizer_authz_unmatched_total` (gap) versus `authorizer_authz_checks_total{result="denied"}` (policy deny).
- `outcome="error"` should sit at zero. Any non-zero rate is an infra problem — alert on it.
- `outcome="not_requested"` is the FGA *adoption gap* — share of calls not yet opting into permission gating.
**`authorizer_fga_operations_total`** `operation`: `get_model` · `write_model` · `read_tuples` · `write_tuples` · `delete_tuples` · `list_users` · `expand` · `list_permissions` · `reset`. `result`: `success` · `error`.

```promql
# Adoption: share of calls per endpoint that still don't pass required_permissions
sum by (endpoint) (rate(authorizer_required_permissions_checks_total{outcome="not_requested"}[5m]))
/
sum by (endpoint) (rate(authorizer_required_permissions_checks_total[5m]))
```
Useful queries:

```promql
# Alert candidate: required_permissions errors over 5 minutes
sum(rate(authorizer_required_permissions_checks_total{outcome="error"}[5m])) > 0
# FGA denial rate (last 5 minutes)
sum(rate(authorizer_fga_checks_total{result="denied"}[5m]))

# FGA check error rate — should be ~0; a spike means the engine/store is failing closed
sum(rate(authorizer_fga_checks_total{result="error"}[5m]))

# Admin authorization changes (model/tuple writes, resets)
sum by (operation) (increase(authorizer_fga_operations_total{operation=~"write_model|write_tuples|delete_tuples|reset"}[1h]))

# p99 check latency
histogram_quantile(0.99, sum by (le, operation) (rate(authorizer_fga_check_duration_seconds_bucket[5m])))
```

See [Authorization (FGA)](./authorization) for the underlying model.
A non-zero `result="error"` rate on `authorizer_fga_checks_total` is an operational
signal — the engine or its datastore is failing, and checks are denying as a result.
Page on it.

### Infrastructure Metrics

Expand Down Expand Up @@ -285,15 +292,6 @@ groups:
severity: warning
annotations:
summary: "Elevated GraphQL error rate"

- alert: AuthzRequiredPermissionsErrors
expr: sum(rate(authorizer_required_permissions_checks_total{outcome="error"}[5m])) > 0
for: 5m
labels:
severity: page
annotations:
summary: "required_permissions checks are failing with errors"
description: "Authorizer's FGA evaluator is returning errors on required_permissions checks. Storage or validation failure is likely. Inspect server logs."
```

## Manual Testing
Expand Down
Loading