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-<business>, 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 <[email protected]>
This commit is contained in:
@@ -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 "[email protected]"
|
||||
```
|
||||
|
||||
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=<project id>
|
||||
@@ -197,10 +232,14 @@ P=<project id>
|
||||
|
||||
### 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/<old id>"`. 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/<old id>"`. 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.
|
||||
|
||||
@@ -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-<business>`,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user