Issue Comment Visibility
System documentation for the per-comment restriction model on issue
comments — what IsRestricted=true means, who can read a restricted
comment, and how the rule is enforced across the REST API and MCP
issues toolset.
Data model
- Table:
issue_comments - Column:
is_restricted BOOLEAN NOT NULL DEFAULT false(migration000193). - Go field:
issues.IssueComment.IsRestricted.
is_restricted=false is the default for every existing row at upgrade
time and for every comment created without a body-level opt-in — the
default visibility makes every comment visible to every repo-reader.
Visibility rule
A comment with IsRestricted=true is visible only to callers who
satisfy at least one of:
- The caller authored the comment (
caller.ID == comment.AuthorID). - The caller is an instance admin (
caller.IsAdmin == true). - The caller owns the parent repo (
caller.ID == repositories.owner_id). - The caller is a repo maintainer — i.e. has a
repo_collaboratorsrow whoseroleisrepo_adminorrepo_write, ORrole=customwith apermissionsset containingrepo:writeorrepo:admin.
Every other caller — including authenticated callers with only
repo_read access — is denied. A non-restricted comment is always
visible (subject to the repo-level read gate, which runs first).
The "maintainer" definition mirrors the canonical write-permission rule
used by *git.Store.canPush (internal/git/authz.go). The two checks
deliberately resolve "who is a maintainer on this repo" the same way so
that a user who can push code on a repo can also see its restricted
comments — no surprise gaps between the two surfaces.
Enforcement points
Accessor
issues.IsCommentRestricted (internal/issues/comment_visibility.go)
is the single source of truth for the rule. Signature:
func IsCommentRestricted(
ctx context.Context,
comment IssueComment,
caller CommentVisibilityCaller,
repoLookup CommentRepoLookup,
aclStore CommentVisibilityACL,
) (bool, error)
The accessor returns true when the caller must NOT see the comment,
false when they may. Two narrow seams (CommentRepoLookup,
CommentVisibilityACL) keep the accessor unit-testable without booting
Postgres; the production wiring satisfies both seams via *issues.Store
(LookupCommentRepo) and *acl.Store (GetCollaborator).
MCP list_comments
The MCP issues toolset wires IsCommentRestricted through the
CommentVisibilityChecker seam. The production wiring in
cmd/server/main.go installs
mcpissues.NewProductionCommentVisibility(issueStore, aclStore), which
delegates to IsCommentRestricted.
Restricted comments the caller cannot read are dropped from the page
entirely — they are not surfaced with a redacted body, they are absent.
This matches the contract documented on the
CommentVisibilityChecker interface.
REST API
The REST GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments
handler does not filter on is_restricted. The current dataset has no
restricted comments, so this does not change observable behaviour.
Setting the flag
issues.Store.CreateIssueComment writes is_restricted from the
IssueComment.IsRestricted field on insert. issues.Store.SetIssueCommentRestricted(ctx, commentID, restricted)
flips the flag on an existing comment; the UPDATE runs in the same tx as
the searchindex outbox enqueue so the indexed view is durable iff the
flag flip is.
issues.Store.UpdateIssueComment does NOT touch is_restricted —
restriction state and comment body change independently and through
different surfaces (the body via the REST PATCH …/comments/{cid}
endpoint; the flag via issues.Store.SetIssueCommentRestricted).
Tests
-
internal/issues/comment_visibility_test.gocovers:- Non-restricted comments are always visible (short-circuits before deps).
- Author can read own restricted comment.
- Instance admin can read any restricted comment.
- Repo owner can read any restricted comment in their repo.
repo_writeandrepo_admincollaborators can read restricted comments.- Custom role with
repo:writepermission can read restricted comments. repo_readcollaborator is denied.- Non-member (no
repo_collaboratorsrow) is denied. - Anonymous (zero UUID) caller is denied.
- Underlying lookup errors are surfaced (fail-closed).
- Nil deps are rejected (wiring bug).
-
internal/mcp/issues/list_comments_test.goexercises theCommentVisibilityCheckerseam with a fakefakeVisibility, so the production wiring is transparent to the unit tests.
Migration
db/migrations/000193_add_issue_comments_is_restricted.up.sql adds
the column with DEFAULT false. In PostgreSQL 11+ this is a
metadata-only change — no table rewrite, fast even on a populated
table. The matching .down.sql drops the column. Migrations are
single-statement so they apply cleanly inside golang-migrate's
implicit transaction wrapper.