# 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: ```yaml 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 `invited` record 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. - `grants` writes a record for the person the case is about, from the form's name and phone number or address. - `SEED_ADMIN_PHONE` or `SEED_ADMIN_EMAIL` writes 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 `groups` claim, if the provider sends one, still counts: a person's groups are the union. With `OIDC_EXTRA_SCOPES=phoneNumber` the sign-in asks the provider for the number and reads `phone_number` from userinfo. Set it for Vipps; not for Kanidm, which denies a sign-in that asks for a scope the person does not hold. Without it, 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..may`. A principal that names a person and lists no groups is looked up: ```json { "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.