Files
paskia/docs/api/perm.md
LeoVasanko 79074dd4f1 OR semantics in perm query arg, strict parsing, segment-aware wildcards
perm=a|b+c now means (a or b) and c; repeated perm args remain ANDed.
Out-of-spec values (empty alternatives, chars outside the scope charset,
stray %2B) are rejected with 400 instead of being silently misparsed;
extra spaces between groups are tolerated. Forward endpoint 400/500
details name /auth/api/forward as origin without echoing query args.
Wildcards are now filename-like: * stays within a :- or /-separated
segment, ** spans segments, partial segments allowed. Slash added to
allowed scope characters for path-based permissions.
2026-08-11 00:44:58 +00:00

97 lines
3.7 KiB
Markdown

# The perm argument
[API overview](../API.md) · [`/auth/api/validate`](validate.md) · [`/auth/api/forward`](../forward.md)
The perm argument is used by endpoints that validate sessions to require one or more permission scopes. It is supported by:
- [POST /auth/api/validate](validate.md)
- [GET /auth/api/forward](forward.md)
## Passing the argument
Repeat the query parameter for each required scope:
```text
?perm=myapp:read&perm=myapp:write
```
You can also pass multiple scopes in one parameter by separating them with whitespace (a literal space, i.e. `+` or `%20` in the query string):
```text
?perm=myapp:read%20myapp:write
```
Both forms produce the same result.
## Semantics
The perm argument uses **AND** semantics between groups: **every** listed scope group must be satisfied by the effective permissions for the request to succeed. If any required group is not satisfied, the endpoint returns 403.
Within a single group, use `|` to list alternatives with **OR** semantics — the group is satisfied when **any one** of the alternatives is present:
```text
?perm=myapp:read|myapp:write+myapp:login
```
This requires `myapp:login` **and** (`myapp:read` **or** `myapp:write`). No whitespace is allowed around the `|` operator.
## Syntax errors
Parsing is strict: anything out of spec is rejected with **400 Bad Request** rather than guessed at. This includes:
- Empty values or alternatives (`?perm=`, `?perm=a||b`, `?perm=|a`, `?perm=a|`)
- Whitespace around `|` (`?perm=a+|+b`)
- Characters not allowed in scopes other than the operators (space and `|`); scopes match `^[A-Za-z0-9:._~/-]+$` plus the `*` wildcard. In particular a literal `+` in the decoded value (from a `%2B` in the query string) is rejected — use `+` or `%20` to encode a space, never `%2B`.
Extra spaces between groups (leading, trailing, or repeated) are tolerated, since they can easily result from URL formatting and carry no ambiguity — they only ever add required permissions, never remove them. The `|` operator is parsed strictly: `?perm=foo&perm=|bar` is an error, never a way to make `foo` optional.
## Wildcards
Wildcards work like filenames, with `:` and `/` acting as path separators:
- `*` matches any sequence of characters **within a single segment** (it never crosses a `:` or `/`)
- `**` matches any sequence of characters, **across separators**
- Part of a segment can be wildcarded, with required text on either or both sides
```text
?perm=myapp:*
```
This matches myapp:read and myapp:write, but **not** myapp:read:all — use `myapp:**` for that. Partial wildcards like `myapp:re*` or `myapp:r*d` match myapp:read. The same applies to path-based scopes: `myapp:path:/api/*` matches myapp:path:/api/clients but not myapp:path:/api/v2/clients — use `myapp:path:/api/**` to span path segments.
## Effective permissions
The permissions available to a session are determined as follows:
1. **Role permissions** — the role assigned to the user contains a set of permission UUIDs.
2. **Org grantable permissions** — only permissions that the user's organization is allowed to grant are effective.
3. **Domain filtering** — a permission can be restricted to a specific domain via its domain field. If the request's Host header does not match that domain, the permission is excluded.
The result is the set of effective permission scopes used for the perm check. Domain-restricted permissions let you grant a scope only for a specific site or subdomain without making it global.
## Examples
Require a single permission:
```text
?perm=myapp:login
```
Require two permissions:
```text
?perm=myapp:login&perm=myapp:api
```
Require any scope under myapp:
```text
?perm=myapp:*
```
Require myapp:login and either myapp:read or myapp:write:
```text
?perm=myapp:login&perm=myapp:read|myapp:write
```