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

+52 -5
View File
@@ -4,13 +4,60 @@ Target: GKE, Cloud SQL for PostgreSQL 17, and Cloud SQL Auth Proxy using Workloa
1. Provision a private-IP Cloud SQL instance, database and application login; enable backups/PITR. Use a VPC-native GKE cluster with access to the database VPC. Configure Workload Identity and grant its Google service account Cloud SQL Client. Follow [Google's GKE connection guide](https://cloud.google.com/sql/docs/postgres/connect-kubernetes-engine).
2. Build `Dockerfile`, push your image, and replace project, region, instance, release, service-account, image, and public URL placeholders. Review/pin the proxy image for your rollout; the template uses the version in Google's documented example. Native sidecars are required. Docker/GKE deployment must be validated in your environment.
3. Create the Kubernetes service account and ConfigMap from `gke.yaml`, then the `meguri-secrets` Secret through your deployment secret manager. It needs `DATABASE_URL=postgresql://USER:PERCENT_ENCODED_PASSWORD@127.0.0.1:5432/meguri`. Proxy-to-database transport is encrypted; loopback stays inside the pod. Add tenant-specific JSM `_EMAIL` / `_TOKEN` variables only for configured tenants. Never commit rendered secrets.
4. Apply only `migrate.yaml`, wait for its successful completion, then apply the Deployment, Service and PDB. Migrations use the same image with `MEGURI_ROLE=migrate`. Replicas have `MEGURI_SKIP_MIGRATIONS=true`; do not race migrations during rollout.
5. Add your internal HTTPS ingress. `MEGURI_PUBLIC_URL` must exactly match the browser origin (scheme, host, port); mutations and cookies depend on it. The default Service is ClusterIP. Keep access inside your organization while OIDC is being added. To bootstrap local accounts, temporarily enable signup in that restricted environment, create accounts, then disable signup. Never enable demo seeding outside loopback.
6. Run health checks, a two-user replacement, a tenant-isolation check, and the provider acceptance checklist in `testing.md`. Enable real JSM integration only after preview and acceptance.
3. Write the instance configuration (see below; start from `deploy/instance-config.example.yaml`) into the `meguri-instance-config` ConfigMap in `gke.yaml`. Create the Kubernetes service account and ConfigMaps from `gke.yaml`, then the `meguri-secrets` Secret through your deployment secret manager. It needs `DATABASE_URL=postgresql://USER:PERCENT_ENCODED_PASSWORD@127.0.0.1:5432/meguri` and, when OIDC is configured, `MEGURI_OIDC_CLIENT_SECRET`. Proxy-to-database transport is encrypted; loopback stays inside the pod. Add tenant-specific JSM `_EMAIL` / `_TOKEN` variables only for configured tenants. Never commit rendered secrets.
4. Apply only `migrate.yaml`, wait for its successful completion, then apply the Deployment, Service and PDB. The Job uses the same image with `MEGURI_ROLE=migrate` and mounts the same configuration: it runs the Cot migrations and then synchronizes the configuration (creates, adopts, renames, enables and disables workspaces, applies bootstrap accounts, recomputes every user's memberships). Replicas have `MEGURI_SKIP_MIGRATIONS=true`; they read the configuration but never synchronize it, so a configuration change takes effect by running the Job for the release first and then restarting replicas. Do not race migrations during rollout. A bootstrap password, if enabled, is in the Job's log.
5. Add your internal HTTPS ingress. `MEGURI_PUBLIC_URL` must exactly match the browser origin (scheme, host, port); mutations, cookies and the OIDC redirect URI (`{MEGURI_PUBLIC_URL}/api/oidc/callback`) depend on it. The default Service is ClusterIP. There is no registration or manual account bootstrap: people appear when they sign in with SSO and their groups map to a workspace or instance role. For a first deployment before SSO works, list a bootstrap account (below), sign in with the logged password, fix the setup, then remove the block. Never enable demo seeding outside loopback.
6. Run health checks, a two-user replacement, a workspace-isolation check (an employee of one workspace cannot see another), a sign-in with a user whose groups map to nothing (expect the explanatory message and no new account), and the provider acceptance checklist in `testing.md`. Enable real JSM integration only after preview and acceptance.
## Instance configuration
Meguri refuses to start without `MEGURI_CONFIG_FILE` (every role; only demo mode uses a built-in file). The file is YAML; unknown fields and invalid values fail startup with a message. It is the only place where workspaces and the groups that grant access are defined; the application has no UI or API for creating workspaces or editing group mappings.
```yaml
auth:
password_login: false # ordinary password accounts may sign in (default false)
bootstrap_admin: # optional break-glass instance administrators; omit in production
usernames: [rescue]
oidc: # optional; the client secret is MEGURI_OIDC_CLIENT_SECRET
issuer: https://sso.example.internal/realms/corp
client_id: meguri
scopes: [openid, profile, email] # default; must include openid
groups_claim: groups # default; dotted path, e.g. realm_access.roles
groups_sources: [id_token, userinfo] # default; searched in this order
instance_admin_groups: [/meguri/admins]
workspaces:
- key: payments # stable identifier, 3-64 chars: a-z 0-9 -
name: Payments # unique, 1-120 characters
admin_groups: [/payments/leads] # at least one; workspace administrators
member_groups: [/payments/team] # employees
```
At least one sign-in method (`oidc`, `password_login` or `bootstrap_admin`) is required, `oidc.issuer` must be HTTPS (loopback may use HTTP), and keys and names must be unique. Groups are compared by exact string: with Keycloak's *Full group path* the values look like `/payments/leads`, without it `leads`. A group in several workspaces is allowed; a person with both an administrator and an employee group of one workspace is an administrator.
Synchronization (run by the migrate Job, or by a single process without `MEGURI_SKIP_MIGRATIONS`) makes the database match the file:
- A configured key without a workspace creates it. A workspace that has no key and exactly the configured name is adopted; if several match, startup fails and nothing is changed. A different name renames the workspace as a normal revision.
- A workspace missing from the file is *disabled*, never deleted: nobody can open it (including open sessions), it is not published, its memberships and queued publication are removed, and its data stays. Listing it again re-enables it and queues a publication. If the file lists no workspaces while enabled ones exist, startup is refused unless `MEGURI_ALLOW_DISABLE_ALL=true`; a mistyped key therefore creates a new empty workspace and disables the old one, and fixing the file restores both.
- Disabled workspaces appear on the instance administrator's page. Deleting one is a deliberate, permanent action there (type its key; refused while it is configured or publishing). Remote provider resources it published are not removed.
- Every user's memberships are recomputed from the groups stored at their last sign-in (or set by an administrator), so changed group mappings apply to everyone at the release without waiting for logins. Group membership changes in the identity provider apply at the user's next SSO sign-in (sessions last 12 hours).
- Instance-level events (workspace created/adopted/renamed/enabled/disabled/deleted, bootstrap password set, password user created, groups changed, password reset) are written to `instance_audit`.
## Single sign-on with Keycloak
Create a confidential OpenID Connect client for Meguri: client authentication on, *Standard flow* only (no implicit, direct-access or service accounts), valid redirect URI `{MEGURI_PUBLIC_URL}/api/oidc/callback`, and put the generated secret into `MEGURI_OIDC_CLIENT_SECRET`. Request the `openid`, `profile` and `email` scopes; the email is used to link people already planned by email. Add a **Group Membership** mapper to the client with *Add to ID token* and *Add to userinfo* enabled (userinfo is the fallback source) and a token claim name equal to `oidc.groups_claim` (`groups` by default). *Full group path* decides whether values are `/payments/leads` or `leads`; the configuration must use exactly what the token contains. Keycloak does not send groups unless this mapper exists. Meguri verifies RS256 signatures against the realm's JWKS, so keep RS256 as the client's signature algorithm.
A sign-in whose token carries no groups claim is treated as having no groups; the log shows `the identity provider returned no groups claim` and the user sees the same explanation as for unmapped groups ("the instance could not map your groups") with a short reference. The log entry `SSO sign-in was not completed` carries that reference, the code (`no_access`, `failed`, `unavailable`) and the cause, including the groups that were received. Sign-out ends only the Meguri session; the identity provider session is untouched.
## Bootstrap administrators
`auth.bootstrap_admin.usernames` creates the listed accounts as instance administrators at synchronization and logs one line per account: `Bootstrap administrator enabled: sign in as NAME with password PASSWORD`. The password is derived from a secret stored in the database, the username and the current UTC hour, so every process that synchronizes in the same hour logs the same password; it changes in the next hour and ends the account's sessions. Replicas started across an hour boundary can disagree, which a restart resolves. These accounts may sign in even when `password_login` is false. To recover from a broken SSO setup, add the block, run the migrate Job, read its log, sign in, repair, and remove the block: removal clears the password, ends sessions and revokes the rights while keeping the user record. Treat log access as administrator access while the block exists. A bootstrap name that belongs to an SSO account stops synchronization.
The default Deployment has two replicas serving both web and workers. Conditional database leases serialize each team's publication. Roles can later be split into `web` and `worker` deployments without changing the binary. On interruption, a completed remote step remains checkpointed; an unfinished lease expires after five minutes.
Monitor failed/conflict sync jobs, oldest `last_success`, and `window_end` approaching the current time. Meguri publishes a finite horizon and must keep running to extend it. Configure operational monitoring around these values; Meguri does not send incident notifications itself. Readiness checks PostgreSQL; liveness checks the process.
The current polling worker and history/audit reads target team-scale workloads. Benchmark and add query pagination/queue batching before broad multi-tenant SaaS rollout. Configure ingress-level rate limits in addition to per-process login throttling. Password recovery, session administration, OIDC, history archival, and live provider acceptance are subsequent operational work.
The current polling worker and history/audit reads target team-scale workloads. Benchmark and add query pagination/queue batching before broad multi-tenant SaaS rollout. Configure ingress-level rate limits in addition to per-process login throttling. Password recovery, session administration, history archival, and live provider acceptance are subsequent operational work.
## Upgrading a database that used local accounts
Memberships are now derived entirely from user groups. The first configuration sync on an upgraded database recomputes every membership from the stored groups; users created before this change have no groups, so their old workspace memberships are removed until the instance administrator assigns groups (password users) or they sign in through SSO. Workspaces without a configuration key are adopted by exact name or disabled; none is deleted. Review the first sync log and the instance audit before opening the instance to users.