Expand description
§cratefield-module-orgs
Organizations, memberships, roles and email invitations for a Cratefield venture: a person creates an organization and is its owner, invites people by address, and everyone who belongs holds a role the venture configured.
Mounted at /v1/orgs. Four migrations, seven tables. Every route but the admin
listing acts for the account the venture’s Auth port identifies; no route
reads a subject id from anywhere else, so one person cannot act as another.
§Composing it
use cratefield_module_orgs::Orgs;
let orgs = Orgs::builder()
// `owner` is always in the set and is the creator's role.
.roles(["owner", "manager", "staff"])
// Non-owner roles that may manage members and invitations.
.managers(["manager"])
.build();| builder | meaning |
|---|---|
.roles([..]) | The roles an organization may use. owner is added if omitted. A role outside the set is refused with 422 orgs-unknown-role, so a typo cannot be stored. |
.managers([..]) | The non-owner roles that may add, remove, re-role and invite. The default is that only owners manage. |
.staff_org(org_id) | The venture’s staff organization. ORGS_STAFF_ORG overrides it per deployment. |
.staff_roles([..]) | The roles in the staff organization that may see the admin listing. |
.invitation_ttl(Duration) | How long an invitation stays acceptable. Seven days by default. |
Configuration keys (all prefixed ORGS_):
| key | meaning |
|---|---|
ORGS_API_BASE | The public base the accept link is built from (default https://api.<venture domain>). |
ORGS_ACCEPT_URL | A venture’s own accept page, when it has one. |
ORGS_STAFF_ORG | Overrides the builder’s staff organization. |
ORGS_FROM / ORGS_REPLY_TO | The invitation mail’s sender. |
§The rules
- An organization always has at least one owner. The last owner cannot leave,
be removed or be demoted — each is one guarded statement whose affected-row
count decides it, answered with
409 orgs-last-owner. - Owners may do anything. A manager role may add, remove, re-role and invite,
but only an owner may grant or revoke the
ownerrole and only an owner may change an owner’s membership. - A caller who is not a member gets the
404 orgs-not-foundan unknown id gets, so organizations cannot be enumerated. - An invitation stores only the SHA-256 of its token and of the invitee’s
normalized address, and is spent by one guarded
UPDATE: a second accept, or one from the wrong address, finds it already gone. The raw token is mailed, never returned in a response.
§Typed API
// May this person see this organization's page?
let role = cratefield_module_orgs::member_role(&*db, org_id, &subject).await?;
// May this caller use an admin route?
let staff = cratefield_module_orgs::require_staff(&ctx, &headers, staff_org, &roles).await?;§Moving from ADMIN_TOKEN to a staff org
require_staff and the admin listing accept either a machine holding
ADMIN_TOKEN or a person whose verified subject is a member of the staff
organization holding one of the staff roles. A deployment that has an
ADMIN_TOKEN keeps working unchanged; the staff organization is the path for
people.
To move:
- Compose the module with
.roles([.., "admin"]).staff_org("<org id>").staff_roles(["admin"]). A staff role configured without a staff organization is aself_checkfailure, because nobody could ever hold it. - Create the staff organization in the venture and give the people who should have access the staff role there. Nothing else changes: membership is grantable and revocable through the ordinary routes.
- Once no machine needs
ADMIN_TOKEN, remove it. Every admin route then answers401unless the caller is staff.
No shared secret to rotate, and removing somebody from the staff organization
is a DELETE — that is the reason to move.
§Service accounts, grants and the audit log
Three library modules sit beside the routes (issue #878). They are plain
functions over the Database port with no routes of their own, so a venture
whose people sign in some other way can still use them — it applies
MIGRATION_SERVICE_ACCOUNTS, MIGRATION_GRANTS and MIGRATION_AUDIT under a
module of its own and declares ACCESS_PERSONAL_DATA. A venture that mounts
Orgs gets the tables with it.
service_accounts— non-human principals an organization owns (name, description, owning member, creator, disabled flag) and their API keys:{brand}_{id}_{secret}, returned once bymint_key, stored only as a SHA-256 and compared in constant time, revocable, rotatable, with an optional expiry and a last-used stamp.verify_keyanswersNonefor every refusal alike — unknown, wrong secret, revoked, expired, account disabled or deleted.disable_owned_bydisables every account a member owns and revokes their keys in one transaction, for when the member leaves.grants—principal × selector × permission, where a selector is a kind (or*), a space and a path prefix matched on whole segments (reports/q3coversreports/q3/plan, neverreports/q3x), and permissions are orderedread < write < admin.decideis deny-by-default and an applicable deny wins; expired grants stop applying, and a stored row that no longer parses fails closed.decide_with_credentialandcoverageintersect with a credential’s scope list, which can only narrow.coverageanswers prefixes, for a listing that must filter before it ranks or limits.may_grantlets only anadminholder hand out what lies inside what they hold. A venture’s default access goes in asGrant::implicitvalues, so the same rule governs it.audit— an append-only log: actor, the member a service account acted for, the credential’s prefix (never a secret), action, resource, detail.listpages newest first.
use cratefield_module_orgs::grants::{decide, split_path, Grant, Permission, Resource, Selector};
let mine = Selector::new("*", "personal:alice", vec![])?;
let grants = vec![Grant::implicit("member:alice", mine, Permission::Admin)];
let page = Resource::new("pages", "personal:alice", split_path("notes/today"))?;
assert!(decide(&grants, &["member:alice"], &page, Permission::Write, now).is_allowed());§Ports
Requires Db and Auth. Optionally uses Clock (expiry and single-use),
IdGen (ids and the invitation token) and Mailer. A mailer that reports
NotConfigured leaves no invitation behind: the row is deleted and the
caller gets a 503 mail-not-configured. Orgs is what still surfaces that
failure to the client; the waitlist, whose answer must not vary, logs the
same NotConfigured and answers 202 anyway.
§Personal data (ADR 0015)
| table | subject | kind | disposition |
|---|---|---|---|
org_members | user_sub | identifier | erase |
orgs | created_by | identifier | retain |
org_invitations | — | contact | unreachable (the address is held only as a hash) |
org_service_accounts | owner_sub | identifier | erase |
org_service_account_keys | via org_service_accounts.owner_sub | identifier | erase (secret_hash redacted) |
org_grants | principal | identifier | erase |
org_audit | actor_id | usage | retain (an erasable audit trail is no trail) |
An organization outlives its creator — deleting the row would delete every
other member’s organization with it — so orgs is retained, with the reason
published in the manifest. A membership is the member’s, and erasing their
account removes it.
Modules§
- audit
- The audit log (issue #878): who did what to which resource of an organization, and with which credential.
- grants
- Access grants (issue #878): who may do what to which part of an organization’s data.
- service_
accounts - Service accounts (issue #878): non-human principals an organization owns — an agent, a CI job, an integration — and the API keys they authenticate with.
Structs§
- Invitation
Mail - What the invitation template is given. Serialized through the registry,
so it is a wire format: adding a field is fine, renaming one breaks
overrides. The module also puts the resolved theme under
theme. - Member
- One membership, as a member of the organization reads it.
- Membership
- An organization and the role the caller holds in it.
- Org
- An organization, as a venture reads it.
- Orgs
- The organizations module (issue #652): organizations, memberships, roles
and email invitations. Compose it with
Orgs::builder. - Orgs
Builder - Builds an
Orgs. - Staff
- An authorized staff caller, as
require_staffreturns it.
Enums§
- Orgs
Error - What an orgs entry point can fail with.
Constants§
- ACCESS_
PERSONAL_ DATA - The personal-data declarations for the three issue #878 tables, for a
venture that applies
MIGRATION_SERVICE_ACCOUNTS,MIGRATION_GRANTSandMIGRATION_AUDITunder a module of its own and must declare what they hold.Orgsdeclares the same four sets. - MIGRATION_
AUDIT - The audit log (issue #878):
audit. Public for the reasonMIGRATION_SERVICE_ACCOUNTSis. - MIGRATION_
GRANTS - Access grants (issue #878):
grants. Public for the reasonMIGRATION_SERVICE_ACCOUNTSis. - MIGRATION_
SERVICE_ ACCOUNTS - Service accounts and their keys (issue #878):
service_accounts. Public so a venture with its own identity, which does not mount this module’s routes, can apply the table under its own module. - MODULE_
NAME - The module’s name: it is mounted at
/v1/orgs, and its config keys are prefixedORGS_. - TEMPLATE_
INVITATION - The registry id a venture overrides.
Functions§
- default_
templates - The module’s default template, for
Harness::builder().templates(..). It renders in the theme the module resolves for each mail (the venture’s coreBrand, with the deployment’sMAIL_THEMEconfig on top); usethemed_templatesto compose the venture’s own theme. - member_
role - The role
subholds inorg_id, orNonewhen they are not a member — the one call an external app needs to answer “may this person see this organization’s page”, without going through HTTP. - require_
staff - Authorizes a staff caller for an admin action: a machine holding
ADMIN_TOKEN, or a person whose verified subject is a member ofstaff_orgholding one ofroles. - themed_
templates - The module’s template in
theme, the venture’s own style. The deployment’sMAIL_THEMEconfig still applies on top.