Added an annotation to the WorkspaceModulesEndpoint to aggregate member IDs into an array, ensuring that only active members are included. This change improves the data structure returned by the API, allowing for better handling of member information in the frontend. Updated the corresponding utility function to handle potential null values for member IDs.
* [WEB-8068] fix: scope workspace cycles/modules listing to project membership
WorkspaceCyclesEndpoint and WorkspaceModulesEndpoint are guarded only by
WorkspaceViewerPermission (any active workspace member) and filtered by
workspace__slug alone, letting any workspace member enumerate cycle/module
metadata (names, dates, issue counts) of private projects they are not a
member of (GHSA-wcc5-qgfr-8g9c).
Restrict both querysets to projects the requesting user is an active member
of, mirroring WorkspaceStatesEndpoint / WorkspaceLabelsEndpoint:
project__project_projectmember__member=request.user
project__project_projectmember__is_active=True
project__archived_at__isnull=True
Add .distinct() to the Module query (the member join is to-many; Cycle already
had it).
Contract regression tests cover hidden cycles/modules for a non-project member,
the positive project-member path, and no row duplication; fail-before verified.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8068] refactor: drop unnecessary distinct() from module listing
Address Copilot review: the project-membership join is filtered to
request.user, and ProjectMember has a unique constraint on (project, member)
where deleted_at IS NULL, so the join yields at most one row per project and
cannot duplicate Module rows. distinct() was dead weight (and a planner cost
for large workspaces). Matches the reference WorkspaceStates/WorkspaceLabels
endpoints, which use no distinct().
Also drop the distinct-focused contract test: adding a *different* project
member never fans out the request.user-filtered join, so it would pass with or
without distinct() — misleading coverage.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>