From bc33faa38f6f59a5dae407f79479c4cb2c0aacec Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 8 Oct 2026 17:13:51 -0500 Subject: [PATCH] docs: lead decision 68, a service account owns the Vikunja bots (sage) Darkwing's row 38 labels probe on the pinned 2.7.0 image: a bot created from the owner's account reads and attaches every label that account created, in any project (upstream #3592). Bots owned by svc-, an account with no labels and no shares, see only labels on the shared project's tasks. The runbook now creates the bots and mints and revokes their tokens as svc-$BIZ; the owner keeps the project and the shares. Co-Authored-By: Claude Opus 5.5 --- docs/guides/slice-1-identities.md | 100 +++++++++++++++++------- docs/plans/2026-09-26_lead-decisions.md | 34 ++++++++ 2 files changed, 105 insertions(+), 29 deletions(-) diff --git a/docs/guides/slice-1-identities.md b/docs/guides/slice-1-identities.md index e8187586..e86d6fb9 100644 --- a/docs/guides/slice-1-identities.md +++ b/docs/guides/slice-1-identities.md @@ -16,8 +16,8 @@ the bundled one. - Run every step in one shell with `umask 077` set, so every file you create is 0600 and every directory 0700. - To check a token file, use `stat`, never `cat`. -- The Vikunja owner password and the Gitea admin login are the - high-value secrets. They are used only in this guide, and never reach +- The Vikunja owner and `svc-$BIZ` passwords and the Gitea admin login + are the high-value secrets. They are used only in this guide, and never reach the broker or an agent. ## 0. Set up the shell @@ -101,8 +101,8 @@ password, so this part uses the web UI. ## 2. Vikunja: the instance and the owner account -You need Vikunja 2.7.0 or later, with local login on and an owner account -that is yours. +You need Vikunja 2.7.0 or later, with local login on, an owner account +that is yours, and a service account `svc-$BIZ` that owns the bots. ### Path A, an existing instance @@ -151,16 +151,47 @@ Both mounts are required. The container runs as your uid, and `/app/vikunja` isn't writable to it, so Vikunja needs the files mount for its startup write check and the db mount for its database. -### Owner login for this guide +### The service account -This writes the owner's session header to a file. The password comes -from a prompt, and the token never reaches the terminal. +Create the bots from `svc-$BIZ`, never from your own account. In +Vikunja 2.7.0 a bot can read and attach every label its owning account +created, in any project (upstream test `pkg/models/label_test.go`, +#3592). Row S3's probe showed it on the pinned image. A bot created by +the owner and shared only into `mosaic-stack` listed the owner's +Launchpad, personal and unattached labels, and attached two of them to +a `mosaic-stack` task. Owned by an account with no labels, the same bot +saw only the labels on `mosaic-stack` tasks and got 403 on the rest +(lead decision 68). + +- `svc-$BIZ` owns this business's five bots and nothing else: no + labels, no projects, no other bots. Never use it in the web UI. +- Nobody shares a project with it. Section 3 shares the bots, not the + account. +- Its password is a high-value secret, like the owner's, and is used + only in this guide and in rotation. + +On Path B, create it the same way as the owner: ```sh -python3 - "$VK" jason "$S/vikunja-owner.hdr" <<'PY' +docker exec -it mosaic-vikunja /app/vikunja/vikunja user create -u "svc-$BIZ" -e "svc-$BIZ@example.invalid" +``` + +On Path A, the instance's operator creates it with the same command on +the instance's container, unless the instance allows registration. + +### Logins for this guide + +`vklogin` writes one account's session header to a file. The password +comes from a prompt, and the token never reaches the terminal. You +need both headers: the owner's for the project and the shares, +`svc-$BIZ`'s for the bots and their tokens. + +```sh +vklogin() { # vklogin USER HEADER_FILE +python3 - "$VK" "$1" "$2" <<'PY' import getpass, json, sys, urllib.request base, user, out = sys.argv[1:4] -pw = getpass.getpass("Vikunja owner password: ") +pw = getpass.getpass("Vikunja password for %s: " % user) req = urllib.request.Request(base + "/api/v2/login", data=json.dumps({"username": user, "password": pw}).encode(), headers={"Content-Type": "application/json"}) @@ -168,13 +199,16 @@ body = json.load(urllib.request.urlopen(req)) if "token" not in body: sys.exit("login answered without a token field; stop and report the keys: %s" % sorted(body)) open(out, "w").write("Authorization: Bearer " + body["token"] + "\n") -print("owner header written") +print("header written for %s" % user) PY +} +vklogin jason "$S/vikunja-owner.hdr" +vklogin "svc-$BIZ" "$S/vikunja-svc.hdr" ``` The `token` field is confirmed in the v2.7.0 source (`auth_login.go`). -The script still stops if it's missing. Section 4 deletes -the header file. +The script still stops if it's missing. Section 4 deletes both +header files. ## 3. Vikunja: the project, the board and the bots @@ -188,8 +222,9 @@ In the web UI, as the owner: 3. In the view's settings, check that the done bucket is `done` and the default bucket is `todo`. Leave bucket configuration on manual. 4. Slice 1 needs no labels, since the requirement id lives in the task, - not in a label. If you create any, note each id. The business file - names labels by id until row S3 settles which labels a bot can see. + not in a label. If you create any, note each id. A bot owned by + `svc-$BIZ` sees only the labels on `mosaic-stack` tasks, and the + broker attaches only the label ids the business file lists. ```sh P= @@ -197,10 +232,14 @@ P= ### The five bots +`api` calls as the owner. `svc` calls as `svc-$BIZ`, which creates the +bots. + ```sh api() { curl -sf -H @"$S/vikunja-owner.hdr" -H 'Content-Type: application/json' "$@"; } +svc() { curl -sf -H @"$S/vikunja-svc.hdr" -H 'Content-Type: application/json' "$@"; } for r in pm cto coder reviewer sync; do - api -X POST "$VK/api/v2/user/bots" \ + svc -X POST "$VK/api/v2/user/bots" \ -d "{\"username\":\"bot-$BIZ-$r\",\"name\":\"$BIZ $r\"}" | jq -c '{id, username}' done ``` @@ -213,8 +252,8 @@ collide. ### Shares -The role bots get write (1). The sync bot gets read (0), so a bug in -the poll path can't write. +The owner shares the project with each bot. The role bots get write +(1). The sync bot gets read (0), so a bug in the poll path can't write. ```sh for r in pm cto coder reviewer; do @@ -244,15 +283,17 @@ cto, coder and reviewer all use `scopes-worker.json`. ### Tokens -`mint` sends the request, writes the token straight to its file, and -prints only the metadata. Vikunja shows a token once. +`mint` sends the request as `svc-$BIZ`, writes the token straight to +its file, and prints only the metadata. Vikunja shows a token once. +Only the account that owns a bot can mint its token. The owner's +account gets 404, code 1005. ```sh mint() { # mint ROLE BOT_ID SCOPES_FILE local r=$1 id=$2 sc=$3 resp="$S/.mint-$1.json" jq -n --arg t "$BIZ-$r-$(date +%F)" --argjson o "$id" --arg e "$EXP" --slurpfile p "$sc" \ '{title:$t, owner_id:$o, expires_at:$e, permissions:$p[0]}' | - curl -s --fail-with-body -H @"$S/vikunja-owner.hdr" -H 'Content-Type: application/json' \ + curl -s --fail-with-body -H @"$S/vikunja-svc.hdr" -H 'Content-Type: application/json' \ -X POST "$VK/api/v2/tokens" --data-binary @- -o "$resp" || { jq -c '{code, message}' "$resp"; rm -f "$resp"; return 1; } jq -je '.token | strings' "$resp" > "$S/$r-vikunja.token.new" && @@ -278,7 +319,7 @@ doesn't empty the live file. ## 4. Check and clean up ```sh -rm -f "$S/vikunja-owner.hdr" +rm -f "$S/vikunja-owner.hdr" "$S/vikunja-svc.hdr" stat -c '%a %s %n' "$S"/*.token ``` @@ -307,12 +348,12 @@ role. The stack never writes this file. ## 5. Rotation -- **Vikunja**, before `expires`: in a new shell, rerun section 0, the - owner login in section 2, and the `api` and `mint` definitions in - section 3. Mint a new token for the same bot, update `expires` in the - business file, and restart the broker. Revoke the old token while the - owner header still exists: - `api -X DELETE "$VK/api/v2/tokens/"`. Then finish with section +- **Vikunja**, before `expires`: in a new shell, rerun section 0, + `vklogin` for `svc-$BIZ` only (section 2), and the `svc` and `mint` + definitions in section 3. Mint a new token for the same bot, update + `expires` in the business file, and restart the broker. Revoke the + old token while the svc header still exists: + `svc -X DELETE "$VK/api/v2/tokens/"`. Then finish with section 4's `rm -f`, which deletes the header. The broker refuses to start with a token past its `expires`, and it treats any 401 as a refusal, never a retry. @@ -323,6 +364,7 @@ role. The stack never writes this file. Update `rotateBy`. Each rotation gets one line in `docs/SESSIONS.md`: date, who, which identities, and no values. - **Revoking a role at once:** delete its token, the Gitea one in the - bot's settings and the Vikunja one with the `DELETE` above, or remove - the bot's collaborator access or project share. The broker's next + bot's settings and the Vikunja one with the `DELETE` above as + `svc-$BIZ`, or remove the bot's collaborator access or, as the owner, + its project share. The broker's next call gets a 401 or 403 and refuses. diff --git a/docs/plans/2026-09-26_lead-decisions.md b/docs/plans/2026-09-26_lead-decisions.md index 68825a03..4f96cd9a 100644 --- a/docs/plans/2026-09-26_lead-decisions.md +++ b/docs/plans/2026-09-26_lead-decisions.md @@ -1196,3 +1196,37 @@ which stay with him. Each item names who decided it and what happened. proposed Sage as PM and Darkwing as Lead (the PRD's CTO role). That is the slice 1 brief's staffing already: PM held by Sage, CTO by Darkwing. No row changes. +68. **A per-business service account owns the Vikunja bots + (2026-10-08).** Source: Darkwing's row 38 probe record, + `agents/darkwing/work/slice1-s3/probes.md` section L, on the + pinned 2.7.0 image in a scratch container. Decision 66 made the + labels probe a precondition for the estate instance, and it failed + with the runbook as written. + - In 2.7.0 a bot reads and attaches every label its owning account + created, in any project. Upstream treats that as intended + (`pkg/models/label_test.go`, #3592). A PM bot created from the + owner's account and shared only into mosaic-stack listed the + owner's Launchpad, personal and unattached labels, and attached + two of them to a mosaic-stack task (201). On the estate instance, + bots under Jason's account would see every Launchpad, personal + and system label. + - Control: the same bot owned by `svc-mosaic-stack`, an account + with no labels and no shares, saw only labels on mosaic-stack + tasks and got 403 on the others. The project owner can share a + bot that another account owns. Only the owning account can mint + its token (the owner got 404, code 1005). + - Ruling: each business gets a service account `svc-`, + not Jason's. It creates that business's five bots and mints and + revokes their tokens. It owns no labels, projects or other bots, + and nobody shares a project with it. Jason's account still owns + the project and makes the shares. The runbook's sections 2 to 5 + say this now. + - S3's broker also attaches only the label ids the business file + lists, so a label that later becomes visible can't be attached. + This is a second guard and doesn't replace the first. + - Still to probe before Jason runs section 3: whether `svc` can + revoke a bot's token with `DELETE /tokens/{id}`, as the rotation + step now says. Darkwing adds it to the record. + - T236 has to provide `svc-mosaic-stack` on the estate instance. + Its operator creates it with `vikunja user create`, unless the + instance allows registration. Sent to Mos.