Let each team name its own OIDC group, not a global mapping
Team membership from single sign-on used to come from one env var,
TERDUT_OIDC_GROUP_MAPPINGS, matched against a team by name and creating
the team if none existed. That put the decision in the server's
environment rather than the team's own hands, needed a restart to
change, and let a typo in a team name silently create a stray team.
Each team now carries its own oidc_member_group and oidc_owner_group,
set by its owner (or an administrator) from the Members tab, or PUT
/api/teams/{teamID}/oidc-groups. The "highest role wins" rule
TERDUT_OIDC_GROUP_MAPPINGS used to apply across mappings now applies
across one team's own two fields: being in both makes somebody an
owner. The sync no longer creates a team by name; a group only ever
grants into a team that already exists.
This is a breaking change for anyone already using
TERDUT_OIDC_GROUP_MAPPINGS, deliberately not auto-migrated: an
OIDC-sourced membership is dropped at a user's next sign-in until its
team's owner re-sets the group. The README's OIDC section spells out
the migration and the risk of a visible access gap during it.
TERDUT_OIDC_ADMIN_GROUP and TERDUT_OIDC_ALLOWED_GROUPS are untouched --
only team membership moved. terdut-tui needs no change: it only reads
GET /api/teams and GET /api/teams/{id}/members, and neither response
shape moved.
This commit is contained in:
@@ -152,9 +152,14 @@ TERDUT_OIDC_CLIENT_ID=terdut
|
||||
TERDUT_OIDC_CLIENT_SECRET=...
|
||||
TERDUT_OIDC_ALLOWED_GROUPS=terdut-users,terdut-admins
|
||||
TERDUT_OIDC_ADMIN_GROUP=terdut-admins
|
||||
TERDUT_OIDC_GROUP_MAPPINGS='[{"group":"sre","team":"SRE","role":"member"},{"group":"sre-leads","team":"SRE","role":"owner"}]'
|
||||
```
|
||||
|
||||
Which team a group grants is not server-wide config: each team names its own
|
||||
group(s), set by that team's own owner (or an administrator) from its Members
|
||||
tab, or `PUT /api/teams/{teamID}/oidc-groups {"member_group":"sre","owner_group":"sre-leads"}`.
|
||||
A team must already exist before a group can grant access to it — the sync
|
||||
never creates one.
|
||||
|
||||
The web UI's sign-in page shows a "Sign in with <name>" button (a plain link to
|
||||
`/api/oidc/login`) above the password form, or instead of it when
|
||||
`TERDUT_PASSWORD_LOGIN=false`; it asks `GET /api/auth/config` what the server offers
|
||||
@@ -175,8 +180,8 @@ Account page does not offer to set a password nobody could use.
|
||||
2. *Whether.* With `TERDUT_OIDC_ALLOWED_GROUPS` set, somebody in none of them is
|
||||
refused and nothing is created.
|
||||
3. *What.* The administrator flag follows `TERDUT_OIDC_ADMIN_GROUP`. Team roles
|
||||
follow the mappings; where several groups grant the same team the highest role
|
||||
wins.
|
||||
follow each team's own `oidc_member_group`/`oidc_owner_group`; where both of a
|
||||
team's groups match, the owner group wins.
|
||||
|
||||
**Managed access.** What the sync grants is marked as managed by single sign-on,
|
||||
and only that is ever changed by it. It is added at sign-in, and removed at the
|
||||
@@ -184,10 +189,18 @@ next sign-in after the group is gone, even if that leaves a team without an owne
|
||||
(an administrator can always repair a team) — the provider is the source of truth
|
||||
for what it grants, so the last-owner and last-administrator guards do not apply.
|
||||
Memberships and administrators added by hand are left alone; the exception is a
|
||||
hand-added member whose mapping grants a *higher* role, who is raised and from then
|
||||
on managed. Editing managed access by hand (`POST` or `DELETE` on a team's
|
||||
members, revoking an SSO-granted administrator) is refused with `409`, since the
|
||||
next sign-in would undo it.
|
||||
hand-added member whose team's own group grants a *higher* role, who is raised and
|
||||
from then on managed. Editing managed access by hand (`POST` or `DELETE` on a
|
||||
team's members, revoking an SSO-granted administrator) is refused with `409`, since
|
||||
the next sign-in would undo it.
|
||||
|
||||
> **Upgrading past migration 013: reconfigure every team's groups.**
|
||||
> `TERDUT_OIDC_GROUP_MAPPINGS` is gone, and the sync no longer creates a team by
|
||||
> name. Group-to-team-role mapping is now each team's own setting — an owner sets
|
||||
> it from the Members tab, or `PUT /api/teams/{teamID}/oidc-groups`. Until a team's
|
||||
> owner does that, an OIDC-sourced membership in it is dropped at that user's next
|
||||
> SSO sign-in, the same as any other loss of group access. Set every team's groups
|
||||
> before affected users next sign in, to avoid a visible gap in access.
|
||||
|
||||
**How fast changes arrive.** Groups are read only at sign-in. A session made by an
|
||||
SSO sign-in has a hard ceiling (`TERDUT_OIDC_SESSION_MAX_AGE`, default 12h) that
|
||||
@@ -329,7 +342,6 @@ over an administrator's edit.
|
||||
| `TERDUT_OIDC_TRUST_EMAIL` | `false` | Link a first sign-in to an existing local user by email even if the provider does not mark the address verified |
|
||||
| `TERDUT_OIDC_ALLOWED_GROUPS` | — | Comma-separated. Only people in one of these may sign in. Empty admits everybody the provider authenticates |
|
||||
| `TERDUT_OIDC_ADMIN_GROUP` | — | Members are system administrators |
|
||||
| `TERDUT_OIDC_GROUP_MAPPINGS` | — | JSON list of `{"group","team","role"}` (`role` is `owner` or `member`). Teams that do not exist are created |
|
||||
| `TERDUT_OIDC_SESSION_MAX_AGE` | `12h` | Hard ceiling on a session made by an SSO sign-in |
|
||||
|
||||
Durations use Go syntax (`30m`, `12h`, `168h`). An unparseable value falls back to the default.
|
||||
@@ -829,6 +841,8 @@ administrator who is not in the team gets the same `404` as anybody else.
|
||||
| `GET` | `/api/teams/{teamID}/members` | member | Who is in the team, with `status` (`oncall` if the rota has them today, `unpageable` when a page to them would go nowhere — even if they are on call — else `reachable`), `on_call`, `next_shift` (first rota day after today), `pageable` and `problem` (`has no ntfy topic` / `account is disabled`; never the topic itself) and `last_active_at` (their newest session or API-key use). Every member sees the same list |
|
||||
| `POST` | `/api/teams/{teamID}/members` | **owner** | Add a member, or change their role `{"user_id","role"}`. `409` when it would demote the last owner, or the membership is managed by single sign-on |
|
||||
| `DELETE` | `/api/teams/{teamID}/members/{userID}` | **owner** | Remove a member. `409` for the last owner, or a membership managed by single sign-on |
|
||||
| `GET` | `/api/teams/{teamID}/oidc-groups` | member | Which groups control this team's membership: `{"member_group","owner_group"}`. An empty string means no group grants that role here |
|
||||
| `PUT` | `/api/teams/{teamID}/oidc-groups` | **owner** | Set them. An empty string clears a binding |
|
||||
| `GET` | `/api/teams/{teamID}/integrations` | member | List integrations. Never returns keys. Each carries `status` (`active` if its key posted within 24h, `quiet` if it has but not lately, `never`), `last_used_at` (last webhook, usable or not), `last_alert_at` (when an alert last arrived on it) and `alerts_24h` (distinct alerts it refreshed in the last day). Alerts delivered before the source was recorded (migration 010) have none, so the last two fill in as Alertmanager re-sends them |
|
||||
| `PATCH` | `/api/teams/{teamID}/integrations/{integrationID}` | **owner** | Rename `{"name"}`. The key does not change |
|
||||
| `POST` | `/api/teams/{teamID}/integrations` | **owner** | Mint an integration `{"name","kind"}` — key and URL shown once |
|
||||
|
||||
Reference in New Issue
Block a user