Skip to content

SearchUserProjects diverges from ListProjectsByCurrentUser — missing inherited and group-expanded projects #1650

@AmanGIT07

Description

@AmanGIT07

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:

  1. Direct project policies (gated by schema.ProjectDirectVisibilityPerms).
  2. Group-expanded — principal's group memberships → policies on those groups.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions