Loosen the Forgejo trigger write-access gate (owners, openers, private-repo conversations) #214
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#214
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Every Forgejo trigger (comment/@mention, label, issue opened, review request, submitted review) is dropped with
author lacks write access (<perm>)unless the broker reportswrite/adminfor the trigger author. This should be loosened in three ways:readaccess is allowed, but only when the trigger leads to a read-only session (aforgejo_conversationsession). They must never reach a planning, implementer or pull-request session.Plan
Context
The gate is in
apps/server/src/forgejo/dispatcher.ts, ~L623–643:checkPermission(row.repo, row.commentAuthor)followed by theperm.data !== "write" && perm.data !== "admin"drop. The localauthor(L623–626) is the PR opener for PR-scoped triggers andpayload.issue.user.loginotherwise; for comments on PRs,payload.issueis the PR.api,ownerandrepoare in scope there (used at L678).The PR-author gates for auto-review (~L926) and the CI watchdog (~L973) stay unchanged. They check the PR author, who is always the opener, so exempting openers there would make the
require_write_permissionsrepo config meaningless.Facts behind the private-repo rule:
resolveSession(~L1700–1725). Any trigger on a non-PR issue with no mapped session getsforgejo_conversation.getForgejoSession(row.repo, row.number)→reuseMapped), and that session can be aforgejo_implementer(implementer sessions are mapped from their issue).forgejo_conversation's tool set (apps/server/src/bridge/session-policy.ts,CONVERSATION_TOOL_NAMES~L157–185) has no edit, write, bash or git tools. It can still post comments and create issues; this is accepted.forgejo_pull_request, which can push whenothersPrsPushis set, so PRs are excluded.Approach
Owner/opener exemption. In
dispatcher.ts, just before the permission block (L633), add:Take the owner from
row.repo, never from the payload. For an org-owned repo the exemption never matches, which is intended. Add!trusted &&to the block's condition so trusted authors skipcheckPermissionentirely.Repo visibility lookup. Add the following to the
DispatchApiinterface inapps/server/src/forgejo/dispatch-api.ts:Implement it in
createDispatchApinext toreadIssue:client.json<ApiRepo>("GET", repoApiPath(owner, repo), { signal }), then return{ ...res, data: res.data.private === true }on success, orreson failure. Mirror howreadIssuereturns a mappedApiResult; importApiRepofrom./tools/api-types.ts. The repo-levelfetchVisibilityintools/index.tsis bound to a tool context, so it can't be reused here. Leave it unchanged. The webhook payload'srepository.privateis not trusted.Private-repo read exemption. In the permission block, replace the plain drop on non-write permission with the following. If
perm.data === "read"andawait this.readOnlyAllowed(job, row, reason)returns true, continue (do not drop). Otherwise drop with the existing reason stringauthor lacks write access (${perm.data}). Add the private method:(Use the row type that
job.rowalready has; reuse whatever type name the file uses.) It returns true only if all of the following hold:CONVERSATION_REASONS.has(reason), which excludes labels, issue-opened, review requests and submitted reviews.!row.isPull.const mapped = getForgejoSession(row.repo, row.number)isnull, orgetSessionTrigger(mapped)returns a trigger withkind === "forgejo_conversation". IfgetSessionTriggerthrows or returnsnull, return false. OtherwiseresolveSessioncould replace an implementer mapping with a new session.api.readRepoPrivate(job.owner, job.repo)returns{ ok: true, data: true }. Any API failure returns false (the author is dropped) after alog.debugnaming the repo and the error.Call the visibility API last, so it only runs when the other conditions already pass.
Comment. Replace the comment above the block (L628–632) with text that keeps the existing rationale and adds: the repo owner and the issue/PR opener are always allowed; on private repos,
readauthors may hold a conversation on an issue, because that session has no write tools.Fakes. Add
readRepoPrivatetoFakeApiinapps/server/src/forgejo/dispatcher.test.ts(L97). It is backed by a fieldrepoPrivate: ApiResult<boolean> = ok(false)and records calls inrepoPrivateReads: string[]("owner/repo"). Theas unknown as DispatchApifakes inbridge/session-policy.test.tsandforgejo/plan-issue.test.tsneed no change.UI text. In
apps/web/src/components/settings/ForgejoPanel.tsx(~L255), change "from authors with write access" to "from authors with write access, the repository owner, or the issue's opener (on private repositories, anyone with read access may converse on issues)".Critical files & anchors
apps/server/src/forgejo/dispatcher.ts: the permission block ~L613–643;resolveSession~L1684;reuseMapped~L1834.getForgejoSessionandgetSessionTriggerare already imported (L73, L106).apps/server/src/forgejo/dispatch-api.ts: theDispatchApiinterface (L15) andreadIssueimpl (~L332).apps/server/src/forgejo/dispatcher.test.ts:enqueue()(L461) supportsauthor,issueAuthor(defaultcarol),repoandisPull. The permission is set withh.permission = { ok: true, data: "read" }(see L802–809). The mapped-session setup is shown in "the issue's existing conversation session is reused" (~L664).Verification
Add tests near "a sender without write access is dropped" (L652). Unless stated otherwise, each uses
h.permission = { ok: true, data: "read" }and authoralice:enqueue({ author: "carol", issueAuthor: "carol" })→ not dropped, a session is created,h.permissionChecksis empty.enqueue({ author: "lucas" })onlucas/omp-deck→ not dropped, no permission check.enqueue({ author: "Carol", issueAuthor: "carol" })→ not dropped (case-insensitive).h.api.repoPrivate = ok(true), plainenqueue()→ not dropped, and the created session's trigger kind isforgejo_conversation.repoPrivate = ok(true),enqueue({ isPull: true })→ droppedauthor lacks write access (read), andrepoPrivateReadsis empty.repoPrivate = ok(true), with an implementer session mapped to issue 7 → dropped with the same reason, and the mapping is unchanged.repoPrivate = ok(true),enqueueLabel(...)→ dropped.repoPrivate = { ok: false, status: 500, message: "boom" }, plainenqueue()→ dropped with the same reason.repoPrivate = ok(false)→ dropped (the existing behaviour).h.permission = { ok: true, data: "none" }withrepoPrivate = ok(true)→ dropped.Existing tests expecting
author lacks write access (read)must still pass, because the defaultrepoPrivateisok(false)and their authors are neither owner nor opener. If a fixture's opener equals its author, change the fixture's opener, not the assertion.Run:
cd apps/server && bun test src/forgejo/dispatcher.test.ts.