One access policy from content; onboarding into Kanidm as a form

Who may do what to what was five checks in five files: `qualifies` on
a page, `requires_group`/`public` on a resource, a desk's transition
allow-list, a self_transition's email check, the automation token.
They are now compiled from content on every load into one Cedar
policy set (src/access.rs), and every server entry point asks it: open
and submit a page, read a resource, move a record, fire a self
transition, read a bucket as the automation service, extend an
invite. Each decision names the rules that granted it; refusals and
every mutation are published on portal.access.decided, and
portal.access.<site>.may answers the same question for other apps
over request-reply (proven live against uhhm's content). The policy
validates against a fixed schema, so a content shape the compiler
cannot express fails instead of granting nothing. Identity is still
Kanidm's; the browser keeps is_qualified as a rendering hint only.

`invite: {group}` on an alternative of a gated page onboards a person:
src/people.rs creates the account, records the email, adds the group
and mints a one-time credential link through Kanidm's REST API with a
service-account token (KANIDM_API_TOKEN, in idm_people_on_boarding).
The result is recorded with the answer into the runtime's
portal_invites bucket and announced on portal.people.invited. Validated:
an invite needs a gated page and required name and email fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-23 11:44:39 +02:00
co-authored by Claude Fable 5.1
parent 7408fa972a
commit eeeb89b217
15 changed files with 1416 additions and 42 deletions
+15
View File
@@ -75,6 +75,21 @@ stream.
it - the chain verifiably answered the alternative that leads there,
or it hangs off an earned page by a link that records nothing. A
page behind an answer they have not given is never offered.
- **One access policy** (`src/access.rs`): who may do what to what is
compiled from content on every load into a Cedar policy set - `qualifies`
on a page, `requires_group`/`public` on a resource, a desk's transitions,
a `self_transition` by link and email, an `invite` - and every server
entry point asks that one policy. Decisions carry the rule ids that
granted them; refusals and every mutation are published on
`portal.access.decided`, and other apps ask the same question over
request-reply on `portal.access.<site>.may`. Identity stays Kanidm's.
- **Onboarding as a form** (`src/people.rs`): an alternative with
`invite: {group}` on a gated page creates the person in Kanidm, records
their email, adds them to the group and mints a one-time link to set
their own credentials, through a service account in
`idm_people_on_boarding` (`KANIDM_API_TOKEN`). The result lands in the
`portal_invites` bucket for the inviter's desk and on
`portal.people.invited` for whatever sends the mail.
- **Business needs as a checkable spec** (in
[uhhm/iris](https://project.uhhm.no/uhhm/iris)): a content repo may
ship a `needs.yaml` saying who wants what, which bucket the answer