Skip to content

fix(api): enforce project visibility on all project sub-resources#319

Open
cavidelizade wants to merge 1 commit into
Devlaner:mainfrom
cavidelizade:fix/project-visibility
Open

fix(api): enforce project visibility on all project sub-resources#319
cavidelizade wants to merge 1 commit into
Devlaner:mainfrom
cavidelizade:fix/project-visibility

Conversation

@cavidelizade

@cavidelizade cavidelizade commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Project-scoped sub-resources didn't honour a project's visibility (network). A secret project (network != public) is hidden by ProjectService.GetByIDGET .../projects/:id/ returns 404 to a workspace member who isn't a project member — but every sub-resource route only checked workspace membership + project-in-workspace, so the data stayed reachable.

Linked issues

Refs #314

Type of change

  • Bug fix (fix:) — corrects broken behavior

Surface

  • API (apps/api/)

What changed

  • New internal/service/access.goenforceProjectVisibility(ctx, ps, ws, workspaceID, projectID, userID). It mirrors the gate already in ProjectService.GetByID: public project → any workspace member; secret project → workspace admin/owner or a member of the project, otherwise ErrProjectNotFound (404, so we don't leak existence).
  • Called it from all 11 ensureProjectAccess helpers (issue, issue_view, module, cycle, comment, attachment, page, estimate, intake, label, state), plus the two paths that don't go through it: AttachmentService.AuthorizeDownload (attachment binary downloads) and ExportService.ExportIssues (per selected project).
  • Added TestSecretProjectVisibility — drives the intake and states services end-to-end against a secret project for four callers: outsider (denied, 404), workspace admin (allowed), project member (allowed), project lead (allowed); and confirms the same project is readable by any member while it's public.

Why this approach

The gate already existed in GetByID; the bug was that sub-resources re-implemented access as bare workspace membership. Rather than duplicate the visibility logic 13 more times, I put it in one shared helper and called it right after the existing IsInWorkspace check, so the change is small and uniform and there's a single place to reason about visibility. Returning ErrProjectNotFound keeps the 404 behaviour consistent with GetByID (avoids leaking that a secret project exists).

Scope note — workspace "guest" role

Issue #314 also mentions that the guest role (RoleGuest) currently gets full member access because IsMember ignores role, and the project's guest_view_all_features flag isn't enforced. I left that out of this PR on purpose: it needs a decision on what guests are actually meant to see/do, whereas the secret-project leak is an unambiguous fix. Happy to follow up once the intended guest capability is settled — I'll keep #314 open for it.

Database / migrations

  • No schema changes.

Breaking changes

  • No — public projects (the default) are unaffected; only access to genuinely secret projects changes, and only for users who shouldn't have had it.

Test plan

  • go test ./... (API) green, including the new TestSecretProjectVisibility
  • go vet ./... clean
  • Hooks ran cleanly (no --no-verify on commit)

AI assistance

  • AI tools were used — tool(s): Claude Code (Opus 4.8) — and AI-assisted commits include a Co-Authored-By: trailer

Summary by CodeRabbit

  • New Features

    • Added consistent visibility controls for private projects across project resources and actions.
    • Private project access is limited to project members, project leads, and workspace administrators.
    • Unauthorized access now responds as if the project were not found.
  • Bug Fixes

    • Restricted unauthorized viewing, downloading, exporting, and management of private project data.
    • Added coverage confirming public projects remain accessible while private projects enforce permissions.

Secret projects (network != public) were hidden by ProjectService.GetByID —
GET .../projects/:id/ returns 404 to a workspace member who isn't a project
member — but every project-scoped sub-resource only checked workspace
membership + project-in-workspace, never the project's visibility. A plain
workspace member could therefore read and mutate a secret project's issues,
states, labels, cycles, modules, estimates, intake, comments, pages and views,
download its attachment binaries, and export its data through the sub-resource
routes.

Add a shared enforceProjectVisibility helper that mirrors the gate in
GetByID (public → any workspace member; secret → workspace admin/owner or a
member of the project, else 404) and call it from every ensureProjectAccess
plus AttachmentService.AuthorizeDownload and ExportService.ExportIssues.

Covered by TestSecretProjectVisibility, which drives the intake and states
services end-to-end for outsider / workspace-admin / project-member /
project-lead against a secret project.

Refs Devlaner#314

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cavidelizade cavidelizade requested a review from a team as a code owner July 16, 2026 09:54
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds centralized visibility enforcement for non-public projects and applies it across project-scoped services, attachment downloads, and exports. Integration tests verify access for public projects, workspace administrators, project members, project leads, and unauthorized workspace members.

Changes

Project visibility enforcement

Layer / File(s) Summary
Centralize visibility rules
apps/api/internal/service/access.go
Non-public projects return ErrProjectNotFound unless the caller is a workspace administrator, project member, or project lead.
Apply checks across services
apps/api/internal/service/{attachment,comment,cycle,estimate,export,intake,issue,issue_view,label,module,page,state}.go
Project access, attachment downloads, and exports invoke the centralized visibility check after existing workspace and project checks.
Validate secret project access
apps/api/internal/service/access_test.go
Integration coverage verifies public access and permitted or denied access after changing a project to secret.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels: bug, API, Vulnerability

Suggested reviewers: martian56

Poem

A rabbit guards the secret gate,
Hiding projects from prying fate.
Admins and members hop inside,
While outsiders meet a 404 tide.
Public paths remain bright and wide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, conventional, and accurately summarizes the main access-control fix.
Description check ✅ Passed The description follows the template well and covers summary, linked issue, scope, changes, tests, and rollout notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/api/internal/service/comment.go (1)

77-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the final return statement.

Since enforceProjectVisibility is the final operation executed in ensureProjectAccess across these services, and its return type (error) directly matches the outer function's return signature, you can simplify the if err != nil blocks to return the execution result directly.

  • apps/api/internal/service/comment.go#L77-L80: Replace lines 77-80 with return enforceProjectVisibility(ctx, s.ps, s.ws, wrk.ID, projectID, userID).
  • apps/api/internal/service/cycle.go#L100-L103: Replace lines 100-103 with return enforceProjectVisibility(ctx, s.ps, s.ws, wrk.ID, projectID, userID).
  • apps/api/internal/service/issue_view.go#L51-L54: Replace lines 51-54 with return enforceProjectVisibility(ctx, s.ps, s.ws, wrk.ID, projectID, userID).
  • apps/api/internal/service/module.go#L58-L61: Replace lines 58-61 with return enforceProjectVisibility(ctx, s.ps, s.ws, wrk.ID, projectID, userID).
  • apps/api/internal/service/page.go#L126-L129: Replace lines 126-129 with return enforceProjectVisibility(ctx, s.projectStore, s.ws, wrk.ID, projectID, userID).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/internal/service/comment.go` around lines 77 - 80, In each
ensureProjectAccess implementation, return the final enforceProjectVisibility
call directly instead of wrapping it in an error check followed by nil: update
apps/api/internal/service/comment.go lines 77-80, cycle.go lines 100-103,
issue_view.go lines 51-54, and module.go lines 58-61 with the existing
project-store arguments, and page.go lines 126-129 using s.projectStore.
Preserve the current arguments and function behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/api/internal/service/comment.go`:
- Around line 77-80: In each ensureProjectAccess implementation, return the
final enforceProjectVisibility call directly instead of wrapping it in an error
check followed by nil: update apps/api/internal/service/comment.go lines 77-80,
cycle.go lines 100-103, issue_view.go lines 51-54, and module.go lines 58-61
with the existing project-store arguments, and page.go lines 126-129 using
s.projectStore. Preserve the current arguments and function behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a79e559-13bd-49da-915a-c97aebcadca7

📥 Commits

Reviewing files that changed from the base of the PR and between a81f6a3 and 92e28b3.

📒 Files selected for processing (14)
  • apps/api/internal/service/access.go
  • apps/api/internal/service/access_test.go
  • apps/api/internal/service/attachment.go
  • apps/api/internal/service/comment.go
  • apps/api/internal/service/cycle.go
  • apps/api/internal/service/estimate.go
  • apps/api/internal/service/export.go
  • apps/api/internal/service/intake.go
  • apps/api/internal/service/issue.go
  • apps/api/internal/service/issue_view.go
  • apps/api/internal/service/label.go
  • apps/api/internal/service/module.go
  • apps/api/internal/service/page.go
  • apps/api/internal/service/state.go

@cavidelizade

Copy link
Copy Markdown
Contributor Author

@martian56 this one closes the access-control gap in #314 — secret projects were reachable through their sub-resource routes (issues, comments, attachments, exports…) by any workspace member, even though the project itself 404s for them. Centralised the visibility check in one shared helper and wired it into every sub-resource path, with a test that drives it end-to-end for outsider/admin/project-member/lead. CI's green and there are no open comments. I left the guest-role part of the issue out on purpose since it needs a call from you on what guests should actually see. Good to merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant