Skip to content

refactor(group): migrate ListByUser callers to membership, delete legacy method#1643

Merged
AmanGIT07 merged 3 commits into
mainfrom
migrate-group-listbyuser-to-membership
May 22, 2026
Merged

refactor(group): migrate ListByUser callers to membership, delete legacy method#1643
AmanGIT07 merged 3 commits into
mainfrom
migrate-group-listbyuser-to-membership

Conversation

@AmanGIT07
Copy link
Copy Markdown
Contributor

Summary

Switches every reader of "groups a principal belongs to" off group.Service.ListByUser (SpiceDB LookupResources + repo enrichment) onto Postgres policy reads via membership.Service.ListGroupsByPrincipal. Mirrors the org migration in #1639.

Changes

  • Add group.Filter.Principal; group.Service.List branches on it.
  • Add membership.Service.ListGroupsByPrincipal(ctx, principal, orgID) shim (sibling of ListOrgsByPrincipal).
  • Migrate callers:
    • core/project/service.go::listNonInheritedProjectIDs
    • core/invitation/service.go::Accept
    • internal/api/v1beta1connect/user.go::ListUserGroups
    • internal/api/v1beta1connect/user.go::ListCurrentUserGroups
  • Delete group.Service.ListByUser, intersectPATScope, and RelationService.LookupResources from the group package — no remaining callers.
  • Drop ListByUser from project.GroupService, invitation.GroupService, and internal/api/v1beta1connect.GroupService interfaces.

Technical Details

  • membership.listGroupsForPrincipal reads only ResourceType=group policies, preserving today's group.membership = member + owner semantics. No org→group inheritance expansion.
  • PAT scoping is inherited from membership.ListResourcesByPrincipal, which intersects user-side and PAT-side resource IDs.
  • group.Service.List branching: when Filter.Principal != nil, the shim is called with Filter.OrganizationID as the org narrowing; result is intersected with Filter.GroupIDs if both are set; empty result short-circuits to []Group{}.

Test Plan

  • make lint — 0 issues
  • make test — affected packages green (core/group, core/project, core/invitation, core/membership, internal/api/v1beta1connect, core/deleter, core/aggregates/orgpats, core/userpat)
  • go test -race on the same set — all green
  • Manual verification of ListUserGroups / ListCurrentUserGroups against pre-change for a user with mixed direct + PAT principals

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment May 22, 2026 9:13am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Enhanced group filtering to respect user permissions based on authenticated principals (both user and personal access token authentication supported).
  • Improvements

    • Refined group listing to intersect results with principal-based access policies for more accurate permission-scoped results.

Walkthrough

This PR refactors group-listing APIs to use principal-based filtering through a filter struct field rather than a dedicated method parameter. Filter gains a Principal pointer field, MembershipService adds ListGroupsByPrincipal as a shared shim, RelationService.LookupResources is removed, and Service.List adds principal filtering logic. All consumers—invitation, project, and API handlers—are updated to construct principals within filters and call the unified List method.

Changes

Principal-Based Group Filtering Refactor

Layer / File(s) Summary
Filter contract and MembershipService group shim
core/group/filter.go, core/group/service.go, core/membership/service.go, core/group/mocks/membership_service.go, core/membership/service_test.go
Filter gains a Principal pointer field. MembershipService interface adds ListGroupsByPrincipal(ctx, principal, orgID) that resolves principal to subject and returns matching group IDs within an org. Implementation delegates to listResourcesForPrincipal. Tests verify user principals, PAT resolution without separate PAT policy query, and org scoping.
Group service core refactoring
core/group/service.go, core/group/service_test.go, core/group/mocks/relation_service.go
RelationService drops LookupResources method. Service.List adds principal-based filtering logic: when flt.Principal is set, it calls membershipService.ListGroupsByPrincipal, intersects returned group IDs with any pre-supplied flt.GroupIDs, and returns empty list when intersection is empty. Exported Service.ListByUser method is removed. Tests replace ListByUser coverage with TestService_List_PrincipalFilter validating principal filter behavior across user and PAT principals.
Invitation service consumer update
core/invitation/service.go, core/invitation/service_test.go, core/invitation/mocks/group_service.go
GroupService interface changes from ListByUser(ctx, principal, flt) to List(ctx, flt). Service.Accept constructs an authenticate.Principal from the accepting user and calls s.groupSvc.List(ctx, group.Filter{Principal: &principal}). New test TestService_Accept_DedupesExistingGroupMembers verifies Accept only adds missing group members and triggers audit/deletion side effects.
Project service consumer update
core/project/service.go, core/project/service_test.go, core/project/mocks/group_service.go
GroupService interface changes from ListByUser(ctx, principal, flt) to List(ctx, flt). listNonInheritedProjectIDs constructs an authenticate.Principal from principal ID/type and calls s.groupService.List(ctx, group.Filter{Principal: &principal}). Tests update non-inherited scenarios (no groups, with groups, PAT principal) and add new subtest for PAT principal with group-mediated projects intersected with PAT scope.
API handler consumer update
internal/api/v1beta1connect/user.go, internal/api/v1beta1connect/user_test.go, internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/mocks/group_service.go
GroupService interface removes ListByUser. ListUserGroups and ListCurrentUserGroups handlers construct principals and call h.groupService.List with filter containing principal and OrganizationID. Tests update all success/empty/error cases across both handlers to expect List with principal embedded in filter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • raystack/frontier#1537: Main PR extends the new core/membership service by adding ListGroupsByPrincipal to support group filtering, directly building on the membership package introduced by this change.
  • raystack/frontier#1447: Both PRs modify group listing to accept/derive from authenticate.Principal and adjust group authorization semantics in core/group/service.go—one adds PAT-scope intersection logic, the other replaces ListByUser with principal-based Filter.Principal.

Suggested reviewers

  • rohilsurana
  • whoAbhishekSah
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@coveralls
Copy link
Copy Markdown

coveralls commented May 22, 2026

Coverage Report for CI Build 26279162990

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.2%) to 42.847%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 6 uncovered changes across 1 file (29 of 35 lines covered, 82.86%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
core/group/service.go 15 9 60.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37891
Covered Lines: 16235
Line Coverage: 42.85%
Coverage Strength: 11.96 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
core/group/filter.go (1)

10-12: ⚡ Quick win

Align filter documentation with the new intersection semantics.

The new Principal docs describe Principal ∩ GroupIDs, but the struct still says only one filter is applied. Please update that stale comment to avoid misleading callers.

core/membership/service_test.go (1)

2293-2395: ⚡ Quick win

Add failure-path coverage for ListGroupsByPrincipal.

This new test only validates success scenarios. Please add cases for policy list failure and groupService.List failure (when orgID is set) to lock in error propagation behavior.

core/group/service_test.go (1)

253-354: ⚡ Quick win

Add negative tests for the new principal branch in List.

Please include cases where (1) membership service is not wired and (2) ListGroupsByPrincipal returns an error, asserting that the error is returned and repository List is not called.

core/invitation/service.go (1)

322-323: ⚡ Quick win

Scope invited-user group lookup to the invitation org.

You already have invite.OrgID; include it in the filter to avoid querying groups across unrelated orgs during accept flow.

Proposed change
 		principal := authenticate.Principal{ID: userOb.ID, Type: schema.UserPrincipal}
-		userGroups, err := s.groupSvc.List(ctx, group.Filter{Principal: &principal})
+		userGroups, err := s.groupSvc.List(ctx, group.Filter{
+			Principal:      &principal,
+			OrganizationID: invite.OrgID,
+		})
 		if err != nil {
 			return err
 		}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c347f942-1bf7-4ec2-90aa-6e9b9aba606a

📥 Commits

Reviewing files that changed from the base of the PR and between a9f2e87 and 42a5c50.

📒 Files selected for processing (16)
  • core/group/filter.go
  • core/group/mocks/membership_service.go
  • core/group/mocks/relation_service.go
  • core/group/service.go
  • core/group/service_test.go
  • core/invitation/mocks/group_service.go
  • core/invitation/service.go
  • core/membership/service.go
  • core/membership/service_test.go
  • core/project/mocks/group_service.go
  • core/project/service.go
  • core/project/service_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/group_service.go
  • internal/api/v1beta1connect/user.go
  • internal/api/v1beta1connect/user_test.go
💤 Files with no reviewable changes (3)
  • core/group/mocks/relation_service.go
  • internal/api/v1beta1connect/mocks/group_service.go
  • internal/api/v1beta1connect/interfaces.go

Comment thread internal/api/v1beta1connect/user.go
@AmanGIT07 AmanGIT07 marked this pull request as ready for review May 22, 2026 08:46
AmanGIT07 and others added 2 commits May 22, 2026 14:38
…acy method

Switches every reader of "groups a principal belongs to" off
`group.Service.ListByUser` (SpiceDB `LookupResources` + repo enrichment) onto
Postgres policy reads via `membership.Service.ListGroupsByPrincipal`. Adds
`group.Filter.Principal` so the composition lives inside `Service.List` instead
of the handler. Four caller sites migrated; old `ListByUser`, `intersectPATScope`,
and the group service's `RelationService.LookupResources` dependency are deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PATs scope orgs and projects, not groups, so a PAT's view of groups is
just its user's. ListResourcesByPrincipal was intersecting user-side
groups with an empty PAT-side set (PATs hold no group policies) and
returning []. Resolve up front and skip the PAT branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AmanGIT07
Copy link
Copy Markdown
Contributor Author

AmanGIT07 commented May 22, 2026

End-to-end test results

Tested the six affected RPCs on a freshly-provisioned org (refactor-test-c20b60) with four users (alice/bob/charlie/dave), a second org (cross-org isolation), three groups (g-alpha, g-beta, g-gamma), a fourth in org B (g-delta), a project (p-atlas) with one user-direct binding and one group-bound binding, plus a service user.

Initial run surfaced one regression in PAT-scoped group listings. After applying the fix to core/membership/service.go (ListGroupsByPrincipal resolves PAT → underlying user and bypasses ListResourcesByPrincipal's PAT intersection — PATs only scope orgs/projects, not groups), the rerun is clean.

Full run

# RPC Auth Filter Expected Observed Status
1 ListUserGroups super-admin alice, orgA g-alpha, g-beta g-alpha, g-beta
2 ListUserGroups super-admin bob, orgA g-gamma g-gamma
3 ListUserGroups super-admin charlie (org owner), orgA empty empty
4 ListUserGroups super-admin dave, orgA empty empty
5 ListUserGroups super-admin alice, no org g-alpha, g-beta, g-delta g-alpha, g-beta, g-delta
6 ListUserGroups super-admin alice, orgB g-delta g-delta
7 ListUserGroups super-admin bob, orgB (wrong org) empty empty
8 ListUserGroups super-admin fake-uuid, orgA empty empty
9 ListUserGroups super-admin "not-a-uuid", orgA invalid_argument internal ⚠️ pre-existing, unrelated
10 ListCurrentUserGroups alice cookie orgA, withMemberCount=true groups + counts g-alpha (2), g-beta (2)
11 ListUserGroups (g-beta disabled) super-admin alice, orgA g-alpha only g-alpha
12 ListUserGroups (g-beta re-enabled) super-admin alice, no org g-alpha, g-beta, g-delta g-alpha, g-beta, g-delta
13 ListCurrentUserGroups alice cookie orgA g-alpha, g-beta g-alpha, g-beta
14 ListUserGroups after RemoveGroupUser(alice, g-alpha) super-admin alice, orgA g-beta g-beta
15a ListCurrentUserGroups PAT (alice, org-viewer/orgA) orgA g-alpha, g-beta g-alpha, g-beta
15b ListCurrentUserGroups PAT (alice, org-viewer/orgA) no org g-alpha, g-beta, g-delta g-alpha, g-beta, g-delta
15c ListCurrentUserGroups PAT (alice, org-viewer/orgA) orgB g-delta g-delta
16 ListProjectsByCurrentUser alice cookie orgA p-atlas p-atlas
17 ListProjectsByCurrentUser bob cookie (via group policy) orgA p-atlas p-atlas
18 ListProjectsByCurrentUser charlie cookie (org owner) orgA p-atlas, pat-proj-d2d3 p-atlas, pat-proj-d2d3
19 ListProjectsByCurrentUser charlie cookie orgA, nonInherited=true pat-proj-d2d3 pat-proj-d2d3
20 ListProjectsByCurrentUser dave cookie orgA empty empty
21 ListProjectsByUser super-admin alice p-atlas p-atlas
22 ListProjectsByUser super-admin bob (via group) p-atlas p-atlas
23 ListServiceUserProjects super-admin SU w/ direct project policy p-atlas p-atlas
24 ListServiceUserProjects super-admin fresh SU, no group/policy empty empty
25 AcceptOrganizationInvitation invitee groupIds=[g-alpha, g-gamma] joins both g-alpha, g-gamma
26 ListCurrentUserGroups PAT (alice, project-viewer on p-atlas) orgA g-alpha, g-beta g-alpha, g-beta
27 ListProjectsByCurrentUser PAT (alice, project-viewer on p-atlas) orgA p-atlas p-atlas
28 ListProjectsByCurrentUser PAT (alice, org-viewer/orgA, no project grant) orgA empty empty

Affected RPCs covered

RPC Layer Tested
FrontierService/ListUserGroups direct
FrontierService/ListCurrentUserGroups direct
FrontierService/ListProjectsByUser indirect via project.ListByUser
FrontierService/ListProjectsByCurrentUser indirect via project.ListByUser
FrontierService/ListServiceUserProjects indirect via project.ListByUser
FrontierService/AcceptOrganizationInvitation indirect via invitation.Accept

Case #9 (malformed UUID → internal instead of invalid_argument) reproduces on the previous SHA too; flagged for a separate follow-up.

… and invitation group dedup; correct error log labels

- core/project/service_test.go: PAT principal with NonInherited=true where the resolved user has group memberships and those groups grant project access; verifies the migrated groupService.List(Filter{Principal:&p}) path intersects with PAT scope correctly.
- core/invitation/service_test.go: Accept where the invitee is pre-existing in one of the invite's groups; verifies dedup path inside Accept calls SetGroupMemberRole only for the new group.
- internal/api/v1beta1connect/user.go: ListUserGroups / ListCurrentUserGroups error log operation labels now read "List" instead of stale "ListByUser".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AmanGIT07 AmanGIT07 force-pushed the migrate-group-listbyuser-to-membership branch from 42a5c50 to 9109a77 Compare May 22, 2026 09:13
@AmanGIT07 AmanGIT07 enabled auto-merge (squash) May 22, 2026 09:14
@AmanGIT07 AmanGIT07 merged commit 9e2e75a into main May 22, 2026
7 of 8 checks passed
@AmanGIT07 AmanGIT07 deleted the migrate-group-listbyuser-to-membership branch May 22, 2026 09:18
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.

3 participants