Add OIDC sign-in, group-derived roles and declarative instance configuration

Workspaces, group mappings and sign-in methods now come from a configuration
file synchronized at startup; workspaces dropped from it are disabled, never
deleted. Instance administrators (a new role) see and help every workspace and
may delete disabled ones; workspace administrators and employees are derived
from the user's groups at every SSO sign-in or administrator edit, and the
employee permission set belongs to the workspace. Self-registration is gone:
users come from SSO (only when their groups map) or from the administrator.
Break-glass bootstrap accounts get an HMAC-derived hourly password that all
replicas agree on. First SSO sign-in links a matching email contact to the
user. The demo runs on the same model with a generated per-start password.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-07 13:28:43 +01:00
1 parent e37d583179
commit 4764f3545f
68 files changed
+7220 -964

No files matched your search

+24 -6
View File
@@ -1,6 +1,24 @@
# Product contract
Meguri is a multi-tenant on-call configuration service. Workspace is the single tenant boundary: there are no nested teams. A user may belong to several independent workspaces. Workspace grants separately control reading, schedule editing, replacement requests, forced application, membership administration, provider configuration, and audit access. Workspace owners administer their workspace and its drift policy.
Meguri is a multi-tenant on-call configuration service. Workspace is the single tenant boundary: there are no nested teams. A user may belong to several independent workspaces. Three roles exist: instance administrators, workspace administrators, and employees. Workspaces and the groups that grant roles are declared in the instance configuration; the application never lets anyone create them.
## Access and administration
**Instance configuration.** Everything about who may enter which workspace is declared in one file read at startup (`MEGURI_CONFIG_FILE`, YAML; JSON is valid YAML). The process refuses to start without it, except demo mode, which carries a built-in configuration of the same format. Unknown fields and invalid values fail startup with the reason. The file declares: `auth.password_login` (default false); optional `auth.bootstrap_admin.usernames`; optional `oidc` (`issuer`, `client_id`, `scopes` default `openid profile email`, `groups_claim` default `groups` as a dotted path, `groups_sources` default `[id_token, userinfo]`); `instance_admin_groups`; and `workspaces`, each with a stable `key` (3–64 lowercase letters, digits and hyphens), a unique `name`, at least one `admin_groups` entry and optional `member_groups`. At least one sign-in method must be configured. The OIDC client secret is a deployment secret (`MEGURI_OIDC_CLIENT_SECRET`), never part of the file. [`deploy/instance-config.example.yaml`](../deploy/instance-config.example.yaml) is a commented example.
**Roles.** An *instance administrator* sees every enabled workspace, may act in it with every permission without being a member, and manages password users and disabled workspaces from the instance page; configuration, workspaces and group mappings are read-only there. A *workspace administrator* holds every permission in exactly that workspace and cannot see, create or affect any other. An *employee* holds the workspace's employee permission set: calendar, schedule editing and replacement requests by default (`view`, `edit_schedules`, `request_overrides`). Administrators change that set in workspace settings and it applies to every current and future employee immediately; `view` is mandatory and `manage_members`/`manage_integration` can never be granted to employees. Granting `force_overrides` lets every employee apply replacements without confirmation; leaving it out requires approval.
**Groups decide roles.** A person's groups are compared to the configured groups by exact string match (case, slashes and prefixes matter). Workspace administrator beats employee when both match; matching `instance_admin_groups` makes an instance administrator. Memberships are recomputed from the stored groups at every SSO sign-in, whenever an administrator edits a password user's groups, and for every user at each configuration sync, so removing a group or a workspace removes access. A sign-in whose groups map to nothing is refused with an explanation and a support reference; no account is created or kept enabled for it.
**Accounts.** There is no self-registration. Accounts come into being only through an SSO sign-in whose groups map to something, or when an instance administrator creates a password user (username, display name, password of 12–256 characters, optional email, groups chosen from the configured list). SSO accounts have no password and their groups belong to the identity provider (read-only in Meguri); password accounts have groups edited by an instance administrator, who cannot edit their own, and can have their password reset (which ends their sessions). An SSO identity is never linked to an existing password account automatically. Ordinary password sign-in works only when `auth.password_login` is true.
**Workspace lifecycle.** On startup the configuration is synchronized: a configured key without a workspace creates one; an unkeyed workspace with exactly the configured name is adopted (several candidates abort startup); a changed name renames it through an ordinary revision; a workspace missing from configuration is disabled, never deleted. A disabled workspace is invisible and inaccessible to everyone including sessions already open, receives no publication and keeps all its data; configuring it again re-enables it with its previous data and queues a publication. A configuration that lists no workspace while enabled ones exist is refused unless `MEGURI_ALLOW_DISABLE_ALL=true`. An instance administrator may permanently delete a workspace that is disabled and not in the configuration by typing its key (its name if it never had one); history, audit and provider checkpoints go with it, but resources already published to a provider are not removed. Instance-level changes are recorded in `instance_audit`.
**Bootstrap administrators.** Optional break-glass accounts listed in `auth.bootstrap_admin.usernames` are created at startup as instance administrators with a generated password written to the log of the process that synchronized the configuration. The password is derived from a secret held in the database, the username and the current UTC hour, so replicas restarted in the same hour agree; a new hour rotates it and ends the account's sessions. They may sign in even when `auth.password_login` is false. Removing the block locks the accounts (password cleared, sessions ended, administrator rights revoked) but keeps their records. While enabled, anyone who can read the logs can become an instance administrator; production normally omits the block.
**Single sign-on.** Authorization code with PKCE for a confidential client. The browser carries a one-time state cookie bound to a database record; callbacks without a matching, unused, unexpired record are refused. ID tokens must be RS256 with a valid signature, issuer, audience, expiry and nonce. Groups come from the configured claim in the ID token, then from userinfo; a missing claim is treated as no groups and logged as a provider misconfiguration. Failures return to the login screen with `login_error` of `no_access`, `failed` or `unavailable` and a short reference that support finds in the log. Sign-out ends only the Meguri session.
**Email linking.** The sign-in email is lowercased. In each workspace the user belongs to, an existing email-only contact with that email becomes the user: the contact's ID is replaced by the user's ID throughout the current document (schedules, absences, requests, variables), the old ID is kept as an alias for history and audit, and the contact's name and country are kept. A user without a matching contact gains a person carrying the email, which providers need to resolve them. Ambiguous or conflicting data (several contacts, a contact already tied to another login, a person with a different email) is skipped. An email never grants access; only groups do.
## Scheduling
@@ -46,7 +64,7 @@ Schedule removal shows a server-generated dependency summary before applying. Us
Routing rules are evaluated from top to bottom. Drag-and-drop and keyboard/touch move buttons save the ordered list of rule IDs; the server requires an exact permutation and assigns consecutive priorities. The rule editor selects an insertion position instead of exposing conflicting numeric priorities. Reordering requires schedule-edit permission and rejects stale versions.
Local password authentication is the initial identity adapter. User IDs, memberships, sessions, and audit identities are provider-independent; later OIDC identities bind `(issuer, subject)` to an existing local user ID. Demo mode must be explicitly enabled and uses only the local preview provider.
Identity is provider-independent: user IDs, memberships, sessions and audit identities are internal; an SSO identity binds `(issuer, subject)` to one user. Sessions last 12 hours; sign-in attempts are throttled per username. Demo mode must be explicitly enabled and uses only the local preview provider.
Daily use starts on the calendar with incoming requests and one-click Accept/Decline, outgoing pending requests, and the selected schedule's current/next personal shift. Schedules are searchable. Clicking a duty day prefills owner, schedule and exact local boundaries. Own days offer searchable teammates; other people's days preselect the signed-in employee. An exact incoming request can be accepted directly from that day. Split days explicitly choose a segment; past days are read-only. Start/end dates and clock times are always visible and are validated on the server before send. Reciprocal swaps are optional and have their own explicit start/end boundaries. Force permission adds a separate Apply without approval action.
@@ -56,9 +74,9 @@ Infrequent administration lives under Setup. Dialogs name their action. Project-
Before migrations or deletion, startup checks a separate database provenance marker recording the last runtime mode. Only positive previous-demo provenance permits resetting application data. An unmarked database, a normal-mode database, or an unreadable marker is protected. Demo startup switches to a separately named database (or an explicitly supplied empty demo database), with a permanent prominent header and server-log warning requiring administrator attention. An empty database can be claimed only after proving it has no user relations. If isolation cannot be established, startup fails without changing protected data. No DROP DATABASE, schema deletion, or TRUNCATE CASCADE is used. Migration history and runtime metadata survive resets; another active database client blocks reset.
During a run, a minute-based maintenance task soft-deletes visitor workspaces and their configuration graphs after 24 hours from creation, and visitor accounts after 48 hours. They disappear from access/listing immediately; dependent data is physically removed after a ten-minute grace period, preserving referential integrity. Nested schedules, contacts, variables, rules, absences, and memberships expire with their owning workspace rather than leaving broken references. Requests expire independently after two hours from creation; audit/history have six-hour retention with a historical boundary snapshot retained. Editing never extends creation-based lifetime. Times are configurable. Demo accounts remain usable; seeded workspaces refresh after expiry. The permanent header describes retention and restart reset. Normal mode never runs demo retention.
During a run, a minute-based maintenance task soft-deletes demonstration workspaces and their configuration graphs after 24 hours from creation, and accounts that an instance administrator created during the run after 48 hours. They disappear from access/listing immediately; dependent data is physically removed after a ten-minute grace period, preserving referential integrity. Nested schedules, contacts, variables, rules, absences, and memberships expire with their owning workspace rather than leaving broken references. Requests expire independently after two hours from creation; audit/history have six-hour retention with a historical boundary snapshot retained. Editing never extends creation-based lifetime. Times are configurable. The seeded accounts are pinned and remain usable; seeded workspaces are re-created from the demonstration configuration and reseeded after expiry. The permanent header describes retention and restart reset. Normal mode never runs demo retention.
The minimal examples are Acme Engineering / Project matrix (six services with two or three ordered owners during working hours and a shared weekly Monday 11:00 Time off rotation) and Customer Support / Daily support (daily 09:00 shifts and a 15-minute escalation). Alice leads both, Bob, Dave and Erin demonstrate Engineering swaps; Carol participates in Engineering and demonstrates Support approvals. The Commerce example includes an email-only contact without login access.
The minimal examples are Acme Engineering / Project matrix (six services with two or three ordered owners during working hours and a shared weekly Monday 11:00 Time off rotation) and Customer Support / Daily support (daily 09:00 shifts and a 15-minute escalation). Demo mode uses the built-in configuration `src/demo-config.yaml`, which has the same format as a production file, and signs in only with passwords (no SSO, no registration). Every start generates one password for all demonstration accounts, writes it to the log and shows the account list with the password on the login screen. `admin` is the instance administrator (a bootstrap account); `alice` administers all four workspaces; `bob`, `carol`, `dave` and `erin` are employees. Bob, Dave and Erin demonstrate Engineering swaps; Carol participates in Engineering and demonstrates Support approvals. The Commerce example includes an email-only contact without login access.
## Anonymized reference behavior
@@ -67,7 +85,7 @@ Alice, Bob, and Carol rotate weekly from Monday at 11:00. Payments primary is Al
Project escalation: primary at minute 0, secondary at minute 10, all team members at minute 30, if unacknowledged. General escalation: on-call at minute 0, random team member at minute 30, named lead at minute 45. Both may repeat 10 times, reset recipient states, and leave the alert open. Working-hours routes select project policies on weekdays 11:00–19:00; a catch-all route selects the general policy. Development alerts may be routed to no notification.
## People, calendars, and working intervals
Scheduling people are separate from login accounts. A workspace can select its members or add an email-only contact without creating an account or granting access. Their name, email, and ISO country belong to the versioned workspace state. Country defaults to Cyprus (CY); the UI offers all 249 ISO country codes. Existing login IDs remain stable; an email is never proof of identity. Only authenticated, authorized users can approve requests; an email-only recipient requires a lead's explicit forced application until a verified account-link workflow exists. Selecting a workspace user does not grant them workspace access.
Scheduling people are separate from login accounts. A workspace can select its members (people who reached it through groups) or add an email-only contact without creating an account or granting access. Their name, email, and ISO country belong to the versioned workspace state. Country defaults to Cyprus (CY); the UI offers all 249 ISO country codes. Existing login IDs remain stable and an email never grants access; at SSO sign-in it can bind a contact to the signing-in user (see Access and administration). Only authenticated, authorized users can approve requests; an email-only recipient requires an administrator's explicit forced application until that person signs in and the contact is linked. Selecting a workspace user does not grant them workspace access.
Each schedule has its own local handover time (11:00 by default) in the workspace's IANA timezone. Duration is still measured in calendar days. Dependencies resolve their own handover boundaries. Workspace working days, start/end time, and IANA timezone are shared by all routing rules with “Only during working hours” enabled. Changing this workspace interval atomically updates workspace snapshots/revisions and queues publication. Old migrations preserve historical handovers and timezone values.
@@ -156,7 +174,7 @@ The rule editor materializes absent optional escalation repeat count/wait fields
Server-preview changes use labeled green/create, blue/update, red/remove and gray/unchanged badges. The expanded list scrolls at 320px (280px on mobile), with a compact footer toggle.
The main Acme Engineering demo contains six services (Payments, Search, Billing, Identity, Data platform, Developer portal), each with two or three ordered owners drawn from five login members: alice, bob, carol, dave, erin. All five share the weekly Monday 11:00 Time off rotation. Dave and Erin only access Acme; Carol also retains Support/Commerce access. Other demo workspaces remain separate. Login password remains `meguri-demo-only`.
The main Acme Engineering demo contains six services (Payments, Search, Billing, Identity, Data platform, Developer portal), each with two or three ordered owners drawn from five login members: alice, bob, carol, dave, erin. All five share the weekly Monday 11:00 Time off rotation. Dave and Erin only access Acme; Carol also retains Support/Commerce access. Other demo workspaces remain separate. Access comes from groups defined in the built-in demo configuration, exactly as in production; the accounts share one password generated at every start.
Generation rule results use the actual authorized server expansion, including schedules, escalation policies and routes, with resolved names and compact settings. The panel is always present with explicit checking, invalid, disabled and empty states; it respects iteration filters and does not simulate generation in the browser. Preview data contains only scheduling resources and scoped people, never integration configuration.