Symptom
AdminService/SearchUserProjects and FrontierService/ListProjectsByCurrentUser answer effectively the same question — "which projects can this user see?" — but return different results for the same user, same org, same state.
Concrete repro
User Alice is org owner of OrgX (5 projects). She has no direct user→project policies; her visibility comes entirely from the app_organization_owner role on OrgX.
| RPC |
Result |
ListProjectsByCurrentUser(orgID=OrgX) |
5 projects (org-inheritance expansion) |
SearchUserProjects(userID=Alice, orgID=OrgX) |
0 projects |
The same gap appears for group-mediated access: a user in a group that holds a project policy is visible to ListProjectsByCurrentUser but invisible to SearchUserProjects.
Where it lives
internal/store/postgres/user_projects_repository.go::buildBaseQuery filters the inner subquery with:
WHERE pol.principal_id = :userID
AND pol.principal_type = 'app/user' -- excludes group-mediated policies
AND pol.resource_type = 'app/project' -- excludes org-level (inherited) policies
Both filters together restrict the result to direct user→project policy rows only.
project.Service.List(Filter{Principal}), used by ListProjectsByCurrentUser, delegates to membership.Service.ListProjectsByPrincipal, which unions three paths:
- Direct project policies (gated by
schema.ProjectDirectVisibilityPerms).
- Group-expanded — principal's group memberships → policies on those groups.
- Org-inherited (skipped when
NonInherited=true) — org-level policies gated by schema.OrganizationProjectInheritPerms.
The aggregate query in user_projects_repository only implements path 1.
Background
The divergence is pre-existing. It became visible after the four-PR membership listing migration (parent #1478) made the ListProjects* semantics explicit and policy-table-backed end-to-end. PR #1648 originally landed a TODO(fix) comment at the aggregate; the comment was dropped in favour of this tracked issue.
Options
- A. Align the aggregate. Rewrite
buildBaseQuery to UNION (a) direct user→project, (b) group-expanded — principal's groups joined into project policies, (c) org-inherited — Alice's org policies whose role grants any permission in OrganizationProjectInheritPerms, expanded to all projects in those orgs. Mirrors the membership method's logic in SQL.
- B. Redefine the aggregate. Document
SearchUserProjects as "projects on which the user has a direct policy" by product decision. Update the admin UI label if needed.
- C. Document and leave both as-is. Keep the divergence; add a comment explaining the semantic difference.
A is the consistent fix; B is a smaller scope decision; C is the cheapest if the divergence is acceptable.
Acceptance
- Decision recorded.
- If A: aggregate returns the same set as
project.Service.List(Filter{Principal}) for a given user/org. Regression test covering an org-owner-with-no-direct-policy case.
- If B: admin UI / API docs reflect the narrowed semantic.
- If C: comment near
buildBaseQuery documenting the divergence and linking to this issue.
Symptom
AdminService/SearchUserProjectsandFrontierService/ListProjectsByCurrentUseranswer effectively the same question — "which projects can this user see?" — but return different results for the same user, same org, same state.Concrete repro
User Alice is org owner of OrgX (5 projects). She has no direct user→project policies; her visibility comes entirely from the
app_organization_ownerrole on OrgX.ListProjectsByCurrentUser(orgID=OrgX)SearchUserProjects(userID=Alice, orgID=OrgX)The same gap appears for group-mediated access: a user in a group that holds a project policy is visible to
ListProjectsByCurrentUserbut invisible toSearchUserProjects.Where it lives
internal/store/postgres/user_projects_repository.go::buildBaseQueryfilters the inner subquery with:Both filters together restrict the result to direct user→project policy rows only.
project.Service.List(Filter{Principal}), used byListProjectsByCurrentUser, delegates tomembership.Service.ListProjectsByPrincipal, which unions three paths:schema.ProjectDirectVisibilityPerms).NonInherited=true) — org-level policies gated byschema.OrganizationProjectInheritPerms.The aggregate query in
user_projects_repositoryonly implements path 1.Background
The divergence is pre-existing. It became visible after the four-PR
membershiplisting migration (parent #1478) made theListProjects*semantics explicit and policy-table-backed end-to-end. PR #1648 originally landed aTODO(fix)comment at the aggregate; the comment was dropped in favour of this tracked issue.Options
buildBaseQuerytoUNION(a) direct user→project, (b) group-expanded — principal's groups joined into project policies, (c) org-inherited — Alice's org policies whose role grants any permission inOrganizationProjectInheritPerms, expanded to all projects in those orgs. Mirrors the membership method's logic in SQL.SearchUserProjectsas "projects on which the user has a direct policy" by product decision. Update the admin UI label if needed.A is the consistent fix; B is a smaller scope decision; C is the cheapest if the divergence is acceptable.
Acceptance
project.Service.List(Filter{Principal})for a given user/org. Regression test covering an org-owner-with-no-direct-policy case.buildBaseQuerydocumenting the divergence and linking to this issue.