Groups
Every user belongs to exactly one group: a workspace, a tenant, an
organization. A group is an id, a name and two timestamps. Keep more
about it in fields, or in tables of your own keyed by
the id.
An app that never mentions groups needs to do nothing. Everyone is in one
group, whose id is group.default_id, "default".
Choosing a mode
Decide how users relate to groups once, at startup:
import howdy/auth
import howdy/auth/group
let assert Ok(identity) = auth.with_groups(identity, group.OneGroupPerUser)
-
group.Single, the default: one group, and one account per email address. -
group.OneGroupPerUser: many groups. An address has one account, which is in one group. -
group.AccountPerGroup: many groups. An address can have a separate account in each group, with its own user id, password and sessions.
Acting in a group
auth.in_group(identity, id) is the same configuration, acting in one
group. Requests made through it apply to that group, and it only
authenticates that group's users:
let acme = auth.in_group(identity, "acme")
// The new user joins acme.
auth.request_token(acme, email, auth.Register)
// Only acme's users get in.
controller.guarded("/acme", auth.required(acme))
When a group must be named depends on the mode:
-
Under
OneGroupPerUser, registering needs a group, because that is how the new user's group is chosen. Signing in does not; if one is given, the account must be in it. -
Under
AccountPerGroup, everything that takes an address needs a group, because the address alone does not name an account. Without one the answer is400. -
A group that does not exist is
404. UnderSinglethat is every id butdefault.
Over HTTP, the endpoints that take an email also take an optional
"group", and the starter pages pass on a ?group= query parameter:
{"email": "ada@example.com", "group": "acme"}
That value is only the client's claim about where it wants to sign in. Once
a request is authenticated, trust principal.user.group_id, or guard the
route with auth.required(auth.in_group(identity, id)).
With registration on, anybody who knows a group's id can register into it.
If membership is by invitation, leave allow_registration off and create
members with auth.provision.
Managing groups
howdy/auth/groups creates and changes groups. These are trusted
operations: none is exposed over HTTP, and each records who acted in by:.
Check that the caller is allowed before you call one.
import howdy/auth/groups
import howdy/auth/user
let assert Ok(acme) = groups.create(identity, name: "Acme", by: user.System)
let assert Ok(_) =
groups.create_with_id(identity, id: "globex", name: "Globex", by: user.System)
groups.list(identity)
groups.get(identity, "globex")
groups.rename(identity, acme.id, to: "Acme Ltd", by: user.Acting(principal))
groups.members(identity, acme.id)
groups.move(identity, user_id, to: "globex", by: user.Acting(principal))
groups.delete(identity, acme.id, by: user.System)
-
creategenerates the id.create_with_idtakes one of your own: 1 to 64 letters, digits, hyphens and underscores, such as a tenant slug. -
movetakes the user's sessions and password with them. UnderAccountPerGroupit is aConflictif the destination already has an account for that address. -
deleteis aConflictwhile the group has users. -
Groups cannot be created under
Single.
Groups are separate from roles. To give somebody a role
within a group, use the scope authorization.Organization(group_id), and
revoke those roles yourself when you move a user.
Changing mode
The mode is recorded in the database. auth.new adopts the recorded mode,
and auth.with_groups with a different one converts the installation in a
single transaction, or refuses with Conflict when the existing users do
not fit: Single needs everyone in default, and leaving AccountPerGroup
needs no address to have accounts in two groups.
Sessions survive a conversion. Emailed tokens that have not been exchanged do not. Stop your other nodes first, because a running node keeps the mode it started with.
