Skip to content

fix(server): check workspace membership for private project access (SEC-03) - #2359

Merged
wilfredmulenga merged 3 commits into
mainfrom
fix/sec-03-getproject-workspace-membership
Aug 19, 2026
Merged

fix(server): check workspace membership for private project access (SEC-03)#2359
wilfredmulenga merged 3 commits into
mainfrom
fix/sec-03-getproject-workspace-membership

Conversation

@wilfredmulenga

Copy link
Copy Markdown
Contributor

Summary

FindActiveById only checked whether an operator was present, not whether that operator actually belonged to the project's workspace. Any non-nil operator satisfied the check.

This matters for the internal gRPC API's GetProject: for read-only methods, the auth interceptor skips the bearer-token check entirely, and the operator-attaching interceptor builds a full operator from a bare user-id metadata header with no proof of identity. So a caller who can reach the internal gRPC port could name any existing user ID with no credentials and read a private project belonging to a workspace that user has no relationship to.

Real-world exploitability is limited: the internal gRPC port is VPC-internal only in the current deployment, not reachable from the public internet. This closes the gap regardless, since it's a genuine authorization bug independent of network exposure, and the current network boundary is an infra detail, not a code guarantee.

Fix

Check the operator's actual workspace membership (operator.IsReadableWorkspace(pj.Workspace())) instead of just its presence, matching the pattern already used elsewhere in this file (e.g. Fetch) for project access checks.

Test plan

  • go build ./... passes
  • Added a regression test case: an operator that exists but has no relationship to the project's workspace is denied access to a private project
  • Verified the new test case fails when the fix is reverted, confirming it catches the regression
  • Confirmed internalapi/server.go's GetProject is the only caller of this interactor method, so the fix is scoped correctly

FindActiveById only checked whether an operator was present, not
whether that operator actually belonged to the project's workspace.
Any non-nil operator satisfied the check, including one built from an
unauthenticated internal-API "user-id" header naming any existing
user with no relationship to the project at all -- letting that
caller read private projects of workspaces they aren't a member of.

Check the operator's actual workspace membership instead of just its
presence, matching the pattern already used elsewhere in this file
for project access checks.
Copilot AI lite review requested due to automatic review settings August 5, 2026 07:02
@github-actions github-actions Bot added the server label Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR closes an authorization gap in the server’s project interactor by ensuring that access to private projects requires not just a non-nil operator, but an operator that is actually readable for the project’s workspace. This directly hardens the internal gRPC GetProject path described in the PR metadata.

Changes:

  • Tightened FindActiveById’s private-project gate to require operator.IsReadableWorkspace(pj.Workspace()), not merely operator != nil.
  • Added a regression test that ensures a non-nil operator with no relationship to the workspace is denied access to a private project.
  • Updated test imports to use require for the new assertion.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
server/internal/usecase/interactor/project.go Fixes private-project authorization by validating workspace readability for the operator.
server/internal/usecase/interactor/project_test.go Adds a regression test covering the previously-missing workspace membership check for private projects.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@wilfredmulenga wilfredmulenga changed the title fix(server): check workspace membership for private project access fix(server): check workspace membership for private project access (SEC-03) Aug 17, 2026

@airslice airslice left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This correctly closes the cross-workspace authorization gap, but do we also have a separate issue tracking authentication for the internal gRPC read-only methods?
From the PR description, a caller can still provide an arbitrary existing user-id without proof of identity. If they know the ID of a user who is a member of the target workspace, wouldn't IsReadableWorkspace then succeed?
I agree that this authorization check belongs here regardless, but it seems the ability to construct an operator from an unverified user-id is a separate defense-in-depth issue worth tracking.

@wilfredmulenga

Copy link
Copy Markdown
Contributor Author

This correctly closes the cross-workspace authorization gap, but do we also have a separate issue tracking authentication for the internal gRPC read-only methods? From the PR description, a caller can still provide an arbitrary existing user-id without proof of identity. If they know the ID of a user who is a member of the target workspace, wouldn't IsReadableWorkspace then succeed? I agree that this authorization check belongs here regardless, but it seems the ability to construct an operator from an unverified user-id is a separate defense-in-depth issue worth tracking.

Agreed. Will make a follow-up for this in a separate notion task.

@wilfredmulenga
wilfredmulenga merged commit 330dc52 into main Aug 19, 2026
18 of 19 checks passed
@wilfredmulenga
wilfredmulenga deleted the fix/sec-03-getproject-workspace-membership branch August 19, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants