2026-09-25 21:35:25 +02:00
|
|
|
# Access
|
|
|
|
|
|
|
|
|
|
Who may do what to what is compiled from content on every load into
|
|
|
|
|
one Cedar policy set, and every server entry point asks that one
|
|
|
|
|
policy. Identity stays Kanidm's; portal only decides.
|
|
|
|
|
|
2026-09-30 13:28:28 +02:00
|
|
|
## Where the groups come from
|
|
|
|
|
|
|
|
|
|
The policy names groups; it does not hold them. A `User` entity's
|
|
|
|
|
parents are the person's groups, and those come from the sign-in: the
|
|
|
|
|
`groups` claim the provider sent, joined with the person's active
|
|
|
|
|
[memberships](memberships.md) when portal keeps them itself. Cedar
|
|
|
|
|
sees one list either way.
|
|
|
|
|
|
2026-09-25 21:35:25 +02:00
|
|
|
## What content compiles to
|
|
|
|
|
|
|
|
|
|
| content | rule |
|
|
|
|
|
|---|---|
|
|
|
|
|
| a page with no `qualifies` | anyone may open and submit |
|
|
|
|
|
| `qualifies: case_officers` | that group may open and submit |
|
|
|
|
|
| `requires_chain: /start` | the visitor's verified answer lineage must reach it |
|
|
|
|
|
| a resource with `public: true` | anyone may read it |
|
|
|
|
|
| a resource with `requires_group: g` | that group may read it |
|
|
|
|
|
| a desk resource's `transitions` | that group may make those moves |
|
|
|
|
|
| an alternative's `self_transition` | the submitter may make it, from each state in `from`, with their link and their matching email |
|
|
|
|
|
| an alternative's `invite: {group}` | the page's own group may invite into that group |
|
|
|
|
|
|
|
|
|
|
Mutations always need a group, whatever `public` says. A resource with
|
|
|
|
|
neither `public` nor `requires_group` is unreachable rather than open:
|
|
|
|
|
the policy fails closed.
|
|
|
|
|
|
|
|
|
|
## Reading the policy
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
iris check --path questions --access
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
prints every rule the content compiled to, as a table of who may do
|
|
|
|
|
what from where to where. It is the fastest way to find a rule a
|
|
|
|
|
content repo did not mean to write — a refusal available on the day a
|
|
|
|
|
case arrives, a board that can decide a case it never received.
|
|
|
|
|
|
|
|
|
|
## Asking it
|
|
|
|
|
|
|
|
|
|
Every decision carries the rule ids that granted it. Refusals and
|
|
|
|
|
every mutation are published on `portal.access.decided`.
|
|
|
|
|
|
|
|
|
|
Other applications ask the same question over NATS request-reply on
|
|
|
|
|
`portal.access.<site>.may`, with the same principal, action, resource
|
|
|
|
|
and context the server uses internally — so an automation acting as a
|
|
|
|
|
group gets exactly the answer a person in that group would.
|
|
|
|
|
|
2026-09-30 13:28:28 +02:00
|
|
|
A principal may also name a person by `sub`, `phone` or `email` and
|
|
|
|
|
list no groups; portal then looks their [memberships](memberships.md)
|
|
|
|
|
up before deciding. That is what makes the policy usable for someone
|
|
|
|
|
who has never opened the site.
|
|
|
|
|
|
2026-09-25 21:35:25 +02:00
|
|
|
## The submitter
|
|
|
|
|
|
|
|
|
|
A person who sent a form has no account. Their credential is the link:
|
|
|
|
|
an unguessable chain hash, plus the email they gave, which the policy
|
|
|
|
|
compares against the one stored on the record.
|
|
|
|
|
|
|
|
|
|
What they may do, and from where, is `self_transition.from`. Its
|
|
|
|
|
default is the initial state alone. A refusal reads "not found" while
|
|
|
|
|
the record is somewhere the move could have been made from — so the
|
|
|
|
|
endpoint cannot be used to discover which records or addresses exist —
|
|
|
|
|
and "already processed" once the case has moved past it.
|