A membership is a case in the built-in portal_memberships bucket: invited by a desk, a grants, the seed or the responsible pass; active from the person's first sign-in, when the provider's subject, phone number or address matches the record; removed by a desk, and invited again after that. One record per group and person however it was keyed, and at sign-in every record that matched learns what the provider verified. The active memberships join the groups claim, so Cedar sees the same parents as before. PEOPLE_BACKEND picks Kanidm or memberships; unset, a KANIDM_API_TOKEN means Kanidm. The NATS access service looks a person's groups up when a question names them by sub, phone or email and lists none. The sign-in asks for the phoneNumber scope and reads it from userinfo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
4.5 KiB
Memberships
Who belongs to which group, kept by portal itself.
A site's groups are named in its content: qualifies on a page,
requires_group on a list, invite: { group } on a desk, grants on
a state. Until now the groups themselves lived in Kanidm, and portal
read them from the sign-in token. With memberships they live in portal,
as records in the built-in portal_memberships bucket, and the
identity provider only has to say who someone is. Cedar sees no
difference: a person's active memberships are the parents of their
User entity, exactly as a groups claim was.
A membership is a case
One record per group and person:
| state | means | how it gets there |
|---|---|---|
invited |
a place is held for them | a desk invite, a state that grants, the first person, someone the content names as responsible |
active |
they are in | they signed in, and the provider's subject, phone number or address matched the record |
removed |
a desk took them out | a button on the desk that lists the bucket; inviting them again reopens the record |
The record carries name, group, by (who or what put them there:
a username, seed, responsible, or the case as bucket:id), and
what is known of the person: phone, email, sub. At sign-in
everything the provider verified is written onto every record that
matched, so a record made from an address learns the phone number,
and the next sign-in matches on the subject before anything else.
Because it is a case, a desk lists it like any other bucket:
resource:
source: { kind: kv, bucket: portal_memberships }
requires_group: styret
transitions:
- { from: invited, to: removed, label: Withdraw }
- { from: active, to: removed, label: Take out }
- { from: removed, to: invited, label: Invite again }
A person is their phone number first
A provider that verifies phone numbers (Vipps) makes the number the
surest thing a record can hold. So a person is known by, in order:
their phone number, their email address, the provider's subject. A
form that collects phone, telefon, tel or mobil gives the
first; email or epost the second. Numbers are kept as +4791234567
whatever way they were typed; a bare national number gets the site's
country (PHONE_COUNTRY, +47 unless set). Addresses are lowercased.
A record matches a person on any one of the three, so a desk that invited by address and a decision that granted by number are talking about one person, and there is one record.
Switching a site to memberships
PEOPLE_BACKEND=memberships
Unset, a KANIDM_API_TOKEN means Kanidm and no token means
memberships. With memberships:
- A desk invite writes an
invitedrecord and mails "log in at the site, and your place is there". No account is made: the provider makes people, portal makes members. So the provider has to be one everyone can already sign in to, which is what Vipps Login is for a Norwegian association, and what Kanidm is not. grantswrites a record for the person the case is about, from the form's name and phone number or address.SEED_ADMIN_PHONEorSEED_ADMIN_EMAILwrites the first person's.- The people the content names as responsible get theirs in
RESPONSIBLE_GROUP, and lose it when the content stops naming them. - The
groupsclaim, if the provider sends one, still counts: a person's groups are the union.
The sign-in flow asks for the phoneNumber scope and reads
phone_number from userinfo; a provider without either gives nothing,
and matching falls back to the address.
Asking who belongs
Other applications ask portal the same access question they always
could, over NATS on portal.access.<site>.may. A principal that names
a person and lists no groups is looked up:
{ "principal": { "kind": "user", "sub": "vipps:abc", "phone": "912 34 567" },
"action": "transition",
"resource": { "kind": "bucket", "name": "hazards" },
"context": { "from": "open", "to": "fixed" } }
The answer is Cedar's, with the rule ids that granted it, for a person who may never have opened the site. That is the whole authorization engine: the provider says who, memberships say what they belong to, the policy compiled from content says what they may do.
What Kanidm still does
For a site on Kanidm nothing changes: invites make accounts and add to
Kanidm groups, and portal_invites tracks the one-time links. The two
backends can be swapped by the one variable; the content and the
policy are the same either way.