Skip to content

refactor(bots): extract shared bot identity, share labels between sibling bots - #3637

Merged
kolaente merged 5 commits into
mainfrom
refactor-shared-bot-identity
Aug 29, 2026
Merged

refactor(bots): extract shared bot identity, share labels between sibling bots#3637
kolaente merged 5 commits into
mainfrom
refactor-shared-bot-identity

Conversation

@tink-bot

Copy link
Copy Markdown
Collaborator

Follow-up to #3609, as agreed in review. "These two user rows are the same identity" was encoded in five places, each slightly differently and each picking its own direction. Extracted into one place, then rewired every site onto it.

Also closes the sibling-bot gap found while reviewing #3609 — see the behavior change below, it is the one non-mechanical part of this PR.

The helper

pkg/user/bot_identity.go, two forms because callers need both:

func (u *User) IsBotOwnedBy(a web.Auth) bool                          // directional
func SameBotIdentityCond(a web.Auth, column string) builder.Cond      // symmetric

Organising idea is the identity root: a bot's bot_owner_id, or a human's own id. Two users share an identity when their roots match. Bots cannot own bots (CreateBotUser), so the root is at most one hop and no recursion is needed.

The root is resolved in SQL, not Go — hasAccessToLabel runs once per label inside Task.UpdateTaskLabels, and auth values built from a JWT or a CalDAV login carry no BotOwnerID, so the struct field cannot be trusted. Builder-only, no raw SQL.

Rewired: isLabelOwner, hasAccessToLabel, GetLabelsByTaskIDs, APIToken.CanDelete, APIToken create/read-all, BotUser.isOwner. Each site got shorter; labelCreatedByBotIdentityCond is gone.

At the two cond sites the separate created_by_id = caller branch also went away — the identity set contains the caller by construction, so keeping it encoded ownership twice.

Behavior change: sibling bots

One human owning two bots meant bot A seeded a label and bot B got a 403 — the #3592 trap one hop over, reproduced against a running server during the #3609 review. The identity-root formulation covers owner→bot, bot→owner and bot→sibling in one expression, so this closes as a side effect of the refactor rather than as a fourth branch.

Reads and label use become symmetric across an owner's bot fleet. Writes stay directional on purpose:

  • a bot still cannot rename or delete a label created by its owner or by a sibling (isLabelOwner keeps IsBotOwnedBy)
  • a bot still cannot mint or delete an API token owned by its human owner — symmetric identity there would be privilege escalation, not a refactor

Still denied, with tests: a different owner's bot, an unrelated user, and link shares (which remain excluded from the identity branches while still reading labels through the attached-task branch).

Out of scope

saved_filters.go:97 has the same gap — a bot cannot see filters its owner created. It becomes s.Where(user.SameBotIdentityCond(auth, "owner_id")) once this lands, but changing filter visibility is a product decision, not a refactor, so it is left alone deliberately.

users_project.go was also left alone: its conds answer "may this bot row appear in search results", and notSomeoneElsesBot deliberately includes all non-bots, which an identity cond must never do. Forcing the helper in there would widen user search.

How to verify

  1. Create a human user, then create two bots owned by them (PUT /api/v1/user/bots), and an API token for each (PUT /api/v1/tokens with owner_id).
  2. Give bot A a project it can write, and create a task in it.
  3. As bot B, create a label: POST /api/v2/labels {"title":"shared"} → note id N.
  4. As bot A, list and attach: GET /api/v2/labels, then POST /api/v2/tasks/{taskID}/labels {"label_id": N}.
    Expected: label N is listed, and the attach returns 201.
    Before this PR: the label was absent from the listing and the attach returned 403.
  5. As bot A, try to rename it: PUT /api/v2/labels/{N}. Expected: 403 — reads are shared, writes are not.
  6. Create a second human with their own bot, and as that bot try GET /api/v2/labels/{N}. Expected: 403.

@github-actions github-actions Bot added area/api-tokens Personal API tokens, token scopes, bot/service accounts area/internal-code Internal refactoring, cleanup, code-quality work area/labels Task labels and label management (the product feature) area/permissions Sharing, link sharing, roles, access control, assignee roles labels Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-3637.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:pr-3637 latest
https://sha-8748ac24366b3e2192dfb187927bb16957f3062b.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-8748ac24366b3e2192dfb187927bb16957f3062b 8748ac2
https://sha-4e3ae00c84b2ff6e3d5c2a6dd9fb37826e296f4b.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-4e3ae00c84b2ff6e3d5c2a6dd9fb37826e296f4b 4e3ae00
https://sha-0b6a7232de1d010037bb14db2a56cf13d86e764c.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-0b6a7232de1d010037bb14db2a56cf13d86e764c 0b6a723
https://sha-7f4e05cf409d90bdbba0987d1e826123c6478812.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-7f4e05cf409d90bdbba0987d1e826123c6478812 7f4e05c
https://sha-16bcbaf806dd11a7a1ede3f26c039c1b786d19e7.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-16bcbaf806dd11a7a1ede3f26c039c1b786d19e7 16bcbaf
https://sha-45ee2df0c5b2fcb1b565d08120806f43c7803164.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-45ee2df0c5b2fcb1b565d08120806f43c7803164 45ee2df
https://sha-652748f5835054d364a541f7ab3c04ad844d4351.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-652748f5835054d364a541f7ab3c04ad844d4351 652748f

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-3637 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/go-vikunja/vikunja:pr-3637
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-3637

Last updated for commit 8748ac2

@kolaente kolaente left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

marker

@tink-bot tink-bot added the pr-swarm/started PR Swarm run is currently active on this PR label Aug 28, 2026

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 1. See inline comments.

Comment thread pkg/db/fixtures/users.yml
Comment thread pkg/user/bot_identity.go
Comment thread pkg/user/bot_identity.go
Comment thread pkg/models/label_task_test.go
Comment thread pkg/models/user_list_test.go

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 1: deferred items needing your call.

Comment thread pkg/user/bot_identity.go
Comment thread pkg/user/bot_identity.go
Comment thread pkg/user/bot_identity.go
Comment thread pkg/user/bot_identity.go

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 2. Re-reviewed round 1's fixes; one of them had swapped a blind spot rather than removing it.

Comment thread pkg/db/fixtures/users.yml
Comment thread pkg/user/bot_identity_test.go

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 3 (convergence check). One finding, in a comment the earlier rounds approved.

Comment thread pkg/user/bot_identity.go Outdated
@tink-bot

tink-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — automated review. Panel: bughunter · security · conventions · design · verifier — round 5 @ 8748ac2 (+0/−0 this round, PR +425/−61)

Verdict: ✅ APPROVE

Maintainer accepted sibling-label sharing for now. Docs PR #394 records owner-fleet label access and task/link-share visibility. No deferred swarm items remain.

❓ Needs your input (0)

None.

✅ Fixed this round (1)

  • pkg/user/bot_identity.go:25 — documented the accepted sibling-label behavior and task-reader consequence in website PR #394.

💤 Resolved as nit (0)

None.

🔎 Runtime verification

  • Sibling attachment and link-share visibility — bot A attached bot B's unattached label with 201; a read-only link share changed from 403 before attachment to 200 after it. Unattached controls stayed hidden, and an unrelated bot stayed isolated.
  • OIDC fallback bot-login rejection — preview was stale and OIDC-disabled. Targeted TestGetOrCreateUser passed on the fixed head.

Reviewer summaries

Reviewer Assessment
🐛 bughunter No unresolved correctness findings.
🛡 security OIDC fallback now rejects bot accounts before side effects.
📏 conventions Permission coverage and comment sweep are clean.
📐 design Directional helper remains useful; sibling sharing is documented.
🔎 verifier Sibling publication behavior verified.
Previous rounds (4)

round 4 @ 4e3ae00c — sibling-label decision deferred; OIDC fallback fixed.
round 3 @ 16bcbaf8 — fixture, test, and documentation fixes.
round 2 @ 7f4e05cf — NULL-shape coverage and fixture-load tests.
round 1 @ 45ee2df0 — fixture fidelity, helper input hardening, comment sweep.

@tink-bot tink-bot added pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input pr-swarm/started PR Swarm run is currently active on this PR and removed pr-swarm/started PR Swarm run is currently active on this PR pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input labels Aug 28, 2026

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 4. See inline comments.

Comment thread pkg/models/label_test.go
Comment thread pkg/db/fixtures/labels.yml
@tink-bot tink-bot added pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input and removed pr-swarm/started PR Swarm run is currently active on this PR labels Aug 29, 2026
IsBotOwnedBy and SameBotIdentityCond replace the ad-hoc bot ownership checks
scattered across labels and api tokens.
Replaces the four hand-rolled IsBot() && BotOwnerID == caller checks in labels
and api tokens. No behaviour change.
The label access condition covered owner-to-bot and bot-to-owner, so two bots
of the same human could not use each other's labels: bot A seeded a label and
bot B got a 403 on it.

Replacing it with SameBotIdentityCond resolves every caller to an identity root
- a bot's owner, or a human themselves - and matches everyone under that root,
which covers all three directions in a single condition. Write access is
unchanged: only the creator and the human owner can update or delete a label.
Both helpers compared a.GetID() against users.id, but *LinkSharing also
satisfies web.Auth and its GetID() is a link_shares.id - a different id
namespace. Passing one would have emitted users.id = <share id> and lent
that share another user's identity, which is the confusion behind
GHSA-vvcv-vpph-h844. Guarded at every call site, but the helpers moved
out of models and into user, so the guard drifted away from the code
needing it. Taking *User makes the case unrepresentable.

Also fail closed on a zero id: humans store bot_owner_id = 0, so an
unguarded zero would have matched all of them.

The doc claimed JWT and CalDAV auth carry no BotOwnerID, but bots can
reach neither path, and claimed bots-cannot-own-bots as an invariant when
the guard is inert for JWT principals. Reworded to chain-safety by
construction, which holds regardless.
@tink-bot
tink-bot force-pushed the refactor-shared-bot-identity branch from 4e3ae00 to 8748ac2 Compare August 29, 2026 20:47
@tink-bot tink-bot added pr-swarm/started PR Swarm run is currently active on this PR pr-swarm/done PR Swarm finished, nothing deferred and removed pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input pr-swarm/started PR Swarm run is currently active on this PR labels Aug 29, 2026
@kolaente
kolaente enabled auto-merge (rebase) August 29, 2026 21:10
@github-actions github-actions Bot added the auto-merge PR has GitHub auto-merge enabled label Aug 29, 2026
@kolaente
kolaente merged commit 48322d5 into main Aug 29, 2026
44 of 45 checks passed
@kolaente
kolaente deleted the refactor-shared-bot-identity branch August 29, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/api-tokens Personal API tokens, token scopes, bot/service accounts area/internal-code Internal refactoring, cleanup, code-quality work area/labels Task labels and label management (the product feature) area/permissions Sharing, link sharing, roles, access control, assignee roles auto-merge PR has GitHub auto-merge enabled pr-swarm/done PR Swarm finished, nothing deferred

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants