TerdutTeam: mint and surface a real invite link (spec.invite) #5

Merged
niklas merged 2 commits from terdutteam-invite-minting into main 2026-10-02 20:17:21 +00:00
Owner

Stacked on #4 (still open) — this branches from it since it edits the same run-demo.sh.

The actual fix for the human-onboarding gap niklas/terdut-server#23 found — not a terdut-server change at all. A team-scoped credential is already owner-equivalent for POST/GET/DELETE /api/teams/{teamID}/invites (requireTeamOwner's synthetic-membership mechanism, ratified not accidental per that repo's SERVICE-ACCOUNTS.md), and invite redemption bypasses signup_mode entirely — this TerdutTeam controller just never grew a feature to use either fact.

What changed

  • New spec.invite{enabled, role (member|owner, default member), maxUses (1-100, default 1)} and status.inviteSecretRef on TerdutTeam. The Secret lives in the TerdutTeam's own namespace, not the operator's — unlike status.credentialsSecretRef (a durable, high-privilege credential, kept operator-side per DESIGN.md §6), an invite is bounded and limited-use, meant for that namespace's own human operators to read and hand out. Same precedent as TerdutAlertSource's status.webhookURLSecretRef: same-namespace, OwnerReference'd, garbage-collected automatically when the TerdutTeam is deleted.
  • internal/controller/terdutteam_invite.go: mints on first spec.invite.enabled, refreshes a day ahead of terdut-server's fixed 7-day TTL, revokes server-side + deletes the Secret when flipped back to false. A lost invite Secret is silently re-minted (not fail-closed like TerdutAlertSource's webhook key) — nothing external depends on one specific invite link staying stable.
  • New tdclient.Invite/CreateInvite/RevokeInvite.
  • New envtest coverage (mint, refresh-before-expiry, revoke-on-disable) and fake-server invite support (its handleTeamSubPath dispatcher got split further — dead man switches into their own handleDeadmanSubPath — to stay under gocyclo's threshold with the new route).

Demo proof

examples/demo/02-team-platform.yaml turns on spec.invite; run-demo.sh's bootstrap_login/join_demo_teams (the psql signup_mode flip + a direct team_members INSERT) are replaced by redeem_platform_invite (reads status.inviteSecretRef, a real POST /api/signup with the token) and join_payments_team (POST /api/teams/{teamID}/members using Payments' own credential + alice's user id via GET /api/users — Payments deliberately has no spec.invite of its own, so the demo shows both onboarding paths this unlocks). Zero kubectl exec/psql calls remain anywhere in the script. README.md's "First login" section rewritten to match — it no longer documents the admin-token curl call that 403s against current terdut-server.

Sequencing

Depends on niklas/terdut-server#24 (the callerMayManageServiceAccount fix for #3) being released before this is deployed for real — not required to build or test this change itself, since the envtest fake never modeled that authorization gap to begin with.

Testing

make fmt lint test helm-lint green. New specs: spec.invite Describe block in terdutteam_controller_test.go (3 cases: mint, refresh, revoke).

Stacked on #4 (still open) — this branches from it since it edits the same `run-demo.sh`. The actual fix for the human-onboarding gap `niklas/terdut-server#23` found — not a `terdut-server` change at all. A team-scoped credential is already owner-equivalent for `POST/GET/DELETE /api/teams/{teamID}/invites` (`requireTeamOwner`'s synthetic-membership mechanism, ratified not accidental per that repo's `SERVICE-ACCOUNTS.md`), and invite redemption bypasses `signup_mode` entirely — this `TerdutTeam` controller just never grew a feature to use either fact. ## What changed - New `spec.invite{enabled, role (member|owner, default member), maxUses (1-100, default 1)}` and `status.inviteSecretRef` on `TerdutTeam`. The Secret lives in the **TerdutTeam's own namespace**, not the operator's — unlike `status.credentialsSecretRef` (a durable, high-privilege credential, kept operator-side per `DESIGN.md` §6), an invite is bounded and limited-use, meant for that namespace's own human operators to read and hand out. Same precedent as `TerdutAlertSource`'s `status.webhookURLSecretRef`: same-namespace, `OwnerReference`'d, garbage-collected automatically when the `TerdutTeam` is deleted. - `internal/controller/terdutteam_invite.go`: mints on first `spec.invite.enabled`, refreshes a day ahead of terdut-server's fixed 7-day TTL, revokes server-side + deletes the Secret when flipped back to `false`. A lost invite Secret is silently re-minted (not fail-closed like `TerdutAlertSource`'s webhook key) — nothing external depends on one specific invite link staying stable. - New `tdclient.Invite`/`CreateInvite`/`RevokeInvite`. - New envtest coverage (mint, refresh-before-expiry, revoke-on-disable) and fake-server invite support (its `handleTeamSubPath` dispatcher got split further — dead man switches into their own `handleDeadmanSubPath` — to stay under `gocyclo`'s threshold with the new route). ## Demo proof `examples/demo/02-team-platform.yaml` turns on `spec.invite`; `run-demo.sh`'s `bootstrap_login`/`join_demo_teams` (the `psql` `signup_mode` flip + a direct `team_members` INSERT) are replaced by `redeem_platform_invite` (reads `status.inviteSecretRef`, a real `POST /api/signup` with the token) and `join_payments_team` (`POST /api/teams/{teamID}/members` using Payments' own credential + alice's user id via `GET /api/users` — Payments deliberately has no `spec.invite` of its own, so the demo shows both onboarding paths this unlocks). **Zero `kubectl exec`/`psql` calls remain anywhere in the script.** `README.md`'s "First login" section rewritten to match — it no longer documents the admin-token curl call that 403s against current terdut-server. ## Sequencing Depends on `niklas/terdut-server#24` (the `callerMayManageServiceAccount` fix for #3) being released before this is deployed for real — not required to build or test this change itself, since the envtest fake never modeled that authorization gap to begin with. ## Testing `make fmt lint test helm-lint` green. New specs: `spec.invite` Describe block in `terdutteam_controller_test.go` (3 cases: mint, refresh, revoke).
niklas changed target branch from examples-demo/run-demo-script to main 2026-10-02 20:13:38 +00:00
niklas added 2 commits 2026-10-02 20:13:38 +00:00
TerdutTeam: mint and surface a real invite link (spec.invite)
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 52s
CI / test (pull_request) Successful in 2m43s
a0ea13955e
The actual fix for the human-onboarding gap niklas/terdut-server#23 found --
not a terdut-server change at all. A team-scoped credential is already
owner-equivalent for POST/GET/DELETE /api/teams/{teamID}/invites
(requireTeamOwner's synthetic-membership mechanism, ratified not
accidental per that repo's SERVICE-ACCOUNTS.md), and invite redemption
bypasses signup_mode entirely -- this TerdutTeam controller just never
grew a feature to use either fact.

New spec.invite{enabled, role (member|owner, default member), maxUses
(1-100, default 1)} and status.inviteSecretRef. The Secret lives in the
TerdutTeam's OWN namespace, not the operator's: unlike
status.credentialsSecretRef (a durable, high-privilege credential, kept
operator-side per DESIGN.md §6), an invite is bounded and limited-use,
meant for this namespace's own human operators to read and hand out --
same precedent as TerdutAlertSource's status.webhookURLSecretRef, same-
namespace and OwnerReference'd so deleting the TerdutTeam garbage-collects
it automatically.

internal/controller/terdutteam_invite.go: mints on first spec.invite.enabled,
refreshes a day ahead of terdut-server's fixed 7-day TTL (reading the
Secret's own stored expiresAt, no extra server round-trip per reconcile),
revokes server-side and deletes the Secret when flipped back to false. A
lost invite Secret is silently re-minted rather than treated as
unrecoverable the way TerdutAlertSource's webhook key is -- nothing
external holds a durable dependency on one specific invite link staying
stable, it's read once by one human and handed out.

New tdclient.Invite/CreateInvite/RevokeInvite. New envtest coverage: mint
into the team's own namespace, refresh-before-expiry, revoke-on-disable
(internal/controller/terdutteam_controller_test.go's new "spec.invite"
Describe block), plus the fake server growing invite support
(terdutserver_controller_test.go) -- its handleTeamSubPath dispatcher was
split further (deadman switches into their own handleDeadmanSubPath,
matching the existing handleIntegrationSubPath precedent) to stay under
golangci-lint's gocyclo threshold with the new route added.

examples/demo updated to prove this end to end: 02-team-platform.yaml
turns on spec.invite; run-demo.sh's bootstrap_login/join_demo_teams (the
psql signup_mode flip + a direct team_members INSERT) are replaced by
redeem_platform_invite (reads status.inviteSecretRef, a real POST
/api/signup with the invite token) and join_payments_team (POST
/api/teams/{teamID}/members using Payments' own credential and alice's
user id resolved via GET /api/users, deliberately not given its own
spec.invite, so the demo shows both onboarding paths this feature
unlocks) -- zero kubectl exec/psql calls remain anywhere in the script.
README.md's "First login" section rewritten to match; it no longer
documents the admin-token curl call that 403s against current
terdut-server (niklas/terdut-server#23).

Depends on niklas/terdut-server#24 (the callerMayManageServiceAccount fix
for terdut-operator#3) being released before this is deployed for real --
not required to build or test this change itself, since the envtest fake
never modeled that authorization gap to begin with.
examples/demo: bump terdut-server to v0.34.0 (the service-account fix)
CI / chart (pull_request) Successful in 2s
CI / security (pull_request) Successful in 1m18s
CI / test (pull_request) Successful in 3m40s
4aa4f17c42
Required for this demo to actually exercise the fix for
niklas/terdut-operator#3 -- v0.33.2 still has the authorization gap this
demo hit live (callerMayManageServiceAccount had no branch letting an
instance-scoped account adopt a team-scoped account's key).
niklas merged commit 478ae6284a into main 2026-10-02 20:17:21 +00:00
niklas deleted branch terdutteam-invite-minting 2026-10-02 20:17:21 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: niklas/terdut-operator#5