fix(employee): list a non-admin's colleagues instead of refusing the members read
Problem
pm.gauzy.co (the Plane UI) shows no members for non-admins. Project member pages are empty, assignee pickers are empty, and "Add member" finds nobody. The browser gets:
403 {"message":"You can only list the members of a team you belong to.","error":"Forbidden"}
#68c39ed92c (2026-10-10, in prod since the 2026-10-26 release) scoped GET /employee/members and the employeeMembers GraphQL field. A caller without ALL_ORG_VIEW or CHANGE_SELECTED_EMPLOYEE must name a team they belong to, otherwise the request is refused. The Plane proxy loads a workspace's members with GET /employee/members?organizationId=…, with no team. Project members and every picker render from that list.
In the prod PM tenant only the 2 SUPER_ADMINs can see members. 42 EMPLOYEE users and 1 MANAGER see none. That includes the owner's day-to-day account ruslan@ever.co, which is EMPLOYEE (checked with a read-only DB query).
Fix (owner decision: "shared team/project")
The privacy intent of 68c39ed92c is kept: a non-admin never lists the whole organisation. Instead of a 403, they get the people they work with.
| Caller without an organisation-wide view | Before | After |
|---|---|---|
| No team or project named | 403 | the people they share an active team or project with, themselves included |
| Their own team named | that team | unchanged |
| Another team named | 403 | unchanged (403) |
| Project named, no team | 403 | that project's members if they belong to it, else 403 |
| No employee record | 403 | unchanged (403) |
- Organisation-wide viewers (SUPER_ADMIN, ADMIN,
CHANGE_SELECTED_EMPLOYEE): unchanged. - Ever Teams: unaffected, because it sends its active team.
- One rule:
ManagedEmployeeService.scopeMembersReadis shared by the REST route and the GraphQL field. New helpers:isMemberOfProjectandfindColleagueEmployeeIds. Both are tenant-scoped, use the same nullable-flag "active membership" predicate asisMemberOfTeam, and fail closed ([NO_ACCESSIBLE_EMPLOYEE_ID]) when the tenant is unknown. findMembers(options, employeeIds?): the colleague ids are an internal argument, not part ofFindMembersInputDTO, so a client cannot pass them. An empty list matches no one and is never read as "no filter".- The 403 message now reads "…a team or project you belong to."
What prod users would see (read-only query, Ever Tech organisation, 19 active employees):
ruslan@ever.co: 12 people.- Non-admins: 1–16 people, median 7.
- 3 non-admins with no team or project see only themselves.
- Project member lists are complete, because every project member shares that project with the viewer.
Tests
- New
managed-employee.service.members-scope.spec.tscovers:- the colleague union, de-duplicated with the caller included;
- the full tenant and active-flag predicates;
- no second read when the caller has no memberships;
- a named team or project being caller-only, and never widened;
- team taking precedence over project;
- refusal for a caller without an employee record;
- fail-closed when the tenant is unknown.
- New
employee.controller.members-scope.spec.ts:- route: colleagues when no team is named, an own team unnarrowed, refusals before any read, and the org-wide control;
findMembersid filter: present when ids are given,NO_ACCESSIBLEfor an empty list, absent otherwise.
employee.resolver.spec.ts: the field's scope tests are updated to the new behaviour.- Fails before the fix: the resolver's "names no team → colleagues" and narrowing tests fail on
develop's code, and the controller spec doesn't compile against the oldfindMemberssignature. - Result:
node --experimental-vm-modules node_modules/jest/bin/jest.js -c packages/core/jest.config.ts --runInBandon the employee specs: 7 suites, 134 tests pass, all type-checked by ts-jest.
🤖 Generated with Claude Code
Summary by cubic
Fixes member listings for non-admins in the Plane UI (pm.gauzy.co), where project member pages and assignee pickers were empty because GET /employee/members refused any read without a named team. A non-admin who names neither a team nor a project now gets the people they share an active team or project with, themselves included, instead of a 403.
Scoping rules
- A named team must still be the caller's own; a named project is allowed only if the caller belongs to it.
- Only memberships in effect count: teams and projects that are active, not archived, and not deleted; a named-project read is narrowed to the project's memberships in effect.
- A caller without an employee record is still refused, and org-wide viewers see the whole org as before.
- Ever Teams is unaffected because it sends its active team.
- The scope lives in
ManagedEmployeeService.scopeMembersRead, shared by the REST route and theemployeeMembersGraphQL field;findMemberstakes colleague ids as an internal argument, so clients can't pass them and an empty list matches no one.