Skip to content

[WEB-8074] fix: scope IssueListEndpoint to guest created_by#9374

Open
mguptahub wants to merge 1 commit into
previewfrom
web-8074/issue-list-guest-scope
Open

[WEB-8074] fix: scope IssueListEndpoint to guest created_by#9374
mguptahub wants to merge 1 commit into
previewfrom
web-8074/issue-list-guest-scope

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

IssueListEndpoint.get (/workspaces/<slug>/projects/<project_id>/issues/list/) returned any issue whose id was supplied in ?issues=, without applying the guest created_by restriction that its sibling IssueViewSet.list enforces. A project GUEST (role=5) on a project with guest_view_all_features=False could therefore read issues they did not author by passing their ids.

Advisory: GHSA-32c7-84jc-4w67 · MEDIUM.

Fix

Replicate the guest scope from IssueViewSet.list: when the requester is an active role=5 ProjectMember and not project.guest_view_all_features, filter the queryset to created_by=request.user. Applied to the base queryset so it flows through filtering, annotation, and grouping.

project = Project.objects.get(pk=project_id, workspace__slug=slug)
if (
    ProjectMember.objects.filter(
        workspace__slug=slug, project_id=project_id,
        member=request.user, role=5, is_active=True,
    ).exists()
    and not project.guest_view_all_features
):
    queryset = queryset.filter(created_by=request.user)

(Project.objects.get() is safe here — @allow_permission(level=PROJECT) already guarantees the requester is a project member, same as the sibling.)

Tests

plane/tests/contract/app/test_issue_list_guest_scope_app.py:

  • restricted guest gets back only their own issue (not the foreign one);
  • positive control: a full member gets every requested issue;
  • positive control: a guest with guest_view_all_features=True gets every requested issue.

Fail-before verified: with the fix stashed, the restricted guest reads the foreign issue; the two positive controls pass regardless.

Gates: ruff ✅ · manage.py check ✅.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Restricted guest access to issue lists when a project does not allow guests to view all features, so guests only see issues they created.
    • Ensured users with broader project access can still view all requested issues as expected.
  • Tests

    • Added coverage for guest-scoped issue visibility, including restricted guest access, full project access, and projects that allow guests to view all features.

IssueListEndpoint.get (/workspaces/<slug>/projects/<project_id>/issues/list/)
returned any issue whose id was passed in ?issues=, without the guest
created_by restriction its sibling IssueViewSet.list enforces. A project GUEST
(role=5) on a project with guest_view_all_features=False could read issues they
did not author by supplying their ids (GHSA-32c7-84jc-4w67).

Replicate the guest scope: when the requester is an active role=5 ProjectMember
and not project.guest_view_all_features, filter the queryset to
created_by=request.user. Applied to the base queryset so it flows through
filtering, annotation and grouping.

Contract regression tests cover the restricted guest (own-only), a full member
(sees all), and a guest with guest_view_all_features enabled (sees all);
fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings July 8, 2026 11:02
@makeplane

makeplane Bot commented Jul 8, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a guest-visibility restriction to IssueListEndpoint.get that limits issue-list results to issues created by the requester when the requester is a role-5 project member and the project's guest_view_all_features flag is disabled. Adds a corresponding contract test module.

Changes

Guest issue list scoping

Layer / File(s) Summary
Guest visibility filter
apps/api/plane/app/views/issue/base.py
Filters returned issues to created_by=request.user for active role-5 members when guest_view_all_features is disabled on the project.
Contract tests and fixtures
apps/api/plane/tests/contract/app/test_issue_list_guest_scope_app.py
Adds fixtures for project, guest user/client, and deterministic issue creation, plus tests verifying restricted guest access, unrestricted member access, and guest access when guest_view_all_features is enabled.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Guest
  participant IssueListEndpoint
  participant Project
  participant IssueQueryset

  Guest->>IssueListEndpoint: GET issue list (issue ids)
  IssueListEndpoint->>Project: fetch project
  IssueListEndpoint->>IssueListEndpoint: check role==5 and guest_view_all_features
  alt guest restricted
    IssueListEndpoint->>IssueQueryset: filter created_by=request.user
  end
  IssueListEndpoint-->>Guest: return filtered issues
Loading

Suggested reviewers: dheeru0198, pablohashescobar, sangeethailango

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: scoping IssueListEndpoint results to the guest creator.
Description check ✅ Passed The PR description covers the summary, fix, tests, and references, with only non-critical template sections missing.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web-8074/issue-list-guest-scope

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
Contributor

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/plane/app/views/issue/base.py (1)

96-109: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Optimize: avoid unconditional Project.objects.get() for non-guest requests.

The Project.objects.get() on line 98 executes on every request to this endpoint, even for non-guest users who don't need the guest restriction. Similarly, the ProjectMember.objects.filter(...).exists() check always runs. Since project is only used for guest_view_all_features, you can leverage Python's short-circuit and to skip the Project query entirely for non-guests by moving the ProjectMember check first:

♻️ Proposed refactor
-        # Restrict guests without full feature access to issues they created,
-        # mirroring IssueViewSet.list.
-        project = Project.objects.get(pk=project_id, workspace__slug=slug)
-        if (
-            ProjectMember.objects.filter(
-                workspace__slug=slug,
-                project_id=project_id,
-                member=request.user,
-                role=5,
-                is_active=True,
-            ).exists()
-            and not project.guest_view_all_features
-        ):
-            queryset = queryset.filter(created_by=request.user)
+        # Restrict guests without full feature access to issues they created,
+        # mirroring IssueViewSet.list.
+        if (
+            ProjectMember.objects.filter(
+                workspace__slug=slug,
+                project_id=project_id,
+                member=request.user,
+                role=5,
+                is_active=True,
+            ).exists()
+            and not Project.objects.filter(
+                pk=project_id, workspace__slug=slug
+            ).values_list("guest_view_all_features", flat=True).first()
+        ):
+            queryset = queryset.filter(created_by=request.user)

This reduces from 2 queries to 1 for non-guest users (the majority), since and short-circuits when the ProjectMember check returns False. For guests, it remains 2 queries but avoids constructing a full Project model instance.

🤖 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/plane/app/views/issue/base.py` around lines 96 - 109, In the issue
filtering logic in the base issue view, the Project lookup runs unconditionally
even when the requester is not a guest, causing avoidable database work. Reorder
the guest restriction check so the ProjectMember existence test is evaluated
first and use short-circuiting to skip the Project.objects.get call unless the
user is actually an active guest member; keep the created_by filtering behavior
the same and preserve the existing guest_view_all_features gate in the same
branch.
🤖 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/plane/app/views/issue/base.py`:
- Around line 96-109: In the issue filtering logic in the base issue view, the
Project lookup runs unconditionally even when the requester is not a guest,
causing avoidable database work. Reorder the guest restriction check so the
ProjectMember existence test is evaluated first and use short-circuiting to skip
the Project.objects.get call unless the user is actually an active guest member;
keep the created_by filtering behavior the same and preserve the existing
guest_view_all_features gate in the same branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 510f19ca-beb9-421a-b03d-1034124ef10b

📥 Commits

Reviewing files that changed from the base of the PR and between 4fc79a2 and 2844c1d.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/issue/base.py
  • apps/api/plane/tests/contract/app/test_issue_list_guest_scope_app.py

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

Fixes an authorization gap in the IssueListEndpoint.get API (/workspaces/<slug>/projects/<project_id>/issues/list/) where restricted project guests could fetch arbitrary issues by ID, by applying the same guest created_by scoping used elsewhere when guest_view_all_features=False.

Changes:

  • Apply guest scoping to the base IssueListEndpoint queryset so restricted guests only see issues they created.
  • Add contract regression tests covering restricted guests, full members, and guests with guest_view_all_features=True.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
apps/api/plane/app/views/issue/base.py Adds guest created_by scoping for IssueListEndpoint so restricted guests can’t fetch non-authored issues by ID.
apps/api/plane/tests/contract/app/test_issue_list_guest_scope_app.py Adds contract tests to prevent regressions of the guest scoping vulnerability (GHSA-32c7-84jc-4w67).

Comment on lines +96 to +109
# Restrict guests without full feature access to issues they created,
# mirroring IssueViewSet.list.
project = Project.objects.get(pk=project_id, workspace__slug=slug)
if (
ProjectMember.objects.filter(
workspace__slug=slug,
project_id=project_id,
member=request.user,
role=5,
is_active=True,
).exists()
and not project.guest_view_all_features
):
queryset = queryset.filter(created_by=request.user)
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.

2 participants