Skip to content

refactor: move project member mutations into membership package#1557

Merged
whoAbhishekSah merged 5 commits intomainfrom
feat/project-member-mutations-to-membership
Apr 22, 2026
Merged

refactor: move project member mutations into membership package#1557
whoAbhishekSah merged 5 commits intomainfrom
feat/project-member-mutations-to-membership

Conversation

@whoAbhishekSah
Copy link
Copy Markdown
Member

@whoAbhishekSah whoAbhishekSah commented Apr 21, 2026

Summary

Moves project member mutations (SetProjectMemberRole, RemoveProjectMember) from core/project/ into core/membership/, continuing the centralized membership pattern from #1478.

After this change, all member access mutations (org + project) go through core/membership/, and core/project/ becomes pure entity CRUD with no access management logic.

Key decisions

  • Project fetch before role validationSetProjectMemberRole now fetches the project first so the parent org ID is available for role org-scoping and org membership checks in one pass.
  • Org-scoping on project rolesvalidateProjectRole rejects custom roles from other orgs (mirrors validateOrgRole). Platform-wide roles and roles belonging to the project's parent org are accepted.
  • Disabled user rejectionvalidateOrgMembership now checks user.Disabled state, consistent with org-level validatePrincipal.
  • Audit records at service layer — Project member mutations now emit AuditRecord objects via the repository (matching the org-level pattern), in addition to the existing handler-level audit logs.
  • Audit target types — Audit records correctly map principal type to entity type (user/serviceuser/group) instead of hardcoding UserType.

Test plan

  • All 25 membership tests pass (11 new project member tests added)
  • All project service tests pass
  • Handler tests pass
  • go build ./... clean
  • gofmt clean

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

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

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@whoAbhishekSah has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 57 minutes and 37 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 57 minutes and 37 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13884c15-d0ac-46e6-a846-674831d0c61c

📥 Commits

Reviewing files that changed from the base of the PR and between aa0eab1 and 041c3d8.

📒 Files selected for processing (13)
  • cmd/serve.go
  • core/membership/errors.go
  • core/membership/mocks/group_service.go
  • core/membership/mocks/project_service.go
  • core/membership/mocks/serviceuser_service.go
  • core/membership/service.go
  • core/membership/service_test.go
  • internal/api/v1beta1connect/errors.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/membership_service.go
  • internal/api/v1beta1connect/mocks/project_service.go
  • internal/api/v1beta1connect/project.go
  • pkg/auditrecord/consts.go
📝 Walkthrough

Walkthrough

Refactors project-scoped member management functionality from ProjectService to MembershipService. MembershipService gains two new methods: SetProjectMemberRole and RemoveProjectMember, with corresponding validation logic, audit event emission, and a new ServiceuserService dependency. Supporting changes include new error types, mock implementations, audit event constants, and handler error translation updates.

Changes

Cohort / File(s) Summary
Core Membership Service
core/membership/service.go, core/membership/service_test.go
Added SetProjectMemberRole and RemoveProjectMember methods with project-scoped validation, policy management, and audit logging. Extended ProjectService and GroupService interfaces with Get() method. Introduced new ServiceuserService interface and dependency. Updated constructor to accept serviceuserService parameter.
Error Definitions
core/membership/errors.go, internal/api/v1beta1connect/errors.go
Added new membership error constants ErrNotOrgMember and ErrInvalidProjectRole. Updated error messages for ErrAlreadyMember and ErrNotMember to reference "principal" instead of "user".
Mock Services
core/membership/mocks/group_service.go, core/membership/mocks/project_service.go, core/membership/mocks/serviceuser_service.go, internal/api/v1beta1connect/mocks/membership_service.go
Added mock implementations for GroupService.Get(), ProjectService.Get(), and new ServiceuserService with full Get() method support. Extended MembershipService mock with new SetProjectMemberRole and RemoveProjectMember methods and corresponding expectation builders.
Service Interfaces
internal/api/v1beta1connect/interfaces.go
Moved SetMemberRole and RemoveMember methods from ProjectService to MembershipService, renaming them to SetProjectMemberRole and RemoveProjectMember respectively.
API Handler
internal/api/v1beta1connect/project.go
Delegated SetProjectMemberRole and RemoveProjectMember calls to membershipService instead of projectService. Updated error translation to map membership package error sentinels instead of project package ones. Added import for membership package.
Audit Events
pkg/auditrecord/consts.go
Added two new audit event constants: ProjectMemberRoleChangedEvent and ProjectMemberRemovedEvent.
Dependency Injection
cmd/serve.go
Updated membership.NewService(...) call in buildAPIDependencies to include new serviceUserService parameter in constructor arguments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rsbh
🚥 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 Apr 21, 2026

Coverage Report for CI Build 24770992888

Coverage increased (+0.1%) to 42.369%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 48 uncovered changes across 3 files (112 of 160 lines covered, 70.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
core/membership/service.go 149 112 75.17%
internal/api/v1beta1connect/project.go 10 0 0.0%
cmd/serve.go 1 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37046
Covered Lines: 15696
Line Coverage: 42.37%
Coverage Strength: 11.91 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: 3

🧹 Nitpick comments (2)
core/membership/service.go (2)

57-59: Naming: ServiceuserServiceServiceUserService.

The codebase consistently uses ServiceUser camel-casing (e.g., schema.ServiceUserPrincipal, serviceuser.ServiceUser, and the existing ServiceUserService interface at internal/api/v1beta1connect/interfaces.go:264). The lowercase u in Serviceuser here (and the serviceuserService field/parameter) breaks that convention.

🧹 Proposed rename
-type ServiceuserService interface {
+type ServiceUserService interface {
 	Get(ctx context.Context, id string) (serviceuser.ServiceUser, error)
 }

And the corresponding struct field, constructor parameter, and mock file name should be renamed accordingly.


520-560: validateOrgMembership does not check principal Disabled/State.

validatePrincipal (line 362) rejects disabled users with user.ErrDisabled. The new validateOrgMembership only fetches the user/service-user/group and checks org ID or org policy presence, so a disabled user, disabled service user, or disabled group could still have their project role mutated via SetProjectMemberRole. If the intent is "must be an active org member", consider mirroring the disabled-state checks from validatePrincipal across all three branches.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ed5abf41-c440-4e4c-8045-cdca5d548d90

📥 Commits

Reviewing files that changed from the base of the PR and between 901863f and f445345.

📒 Files selected for processing (13)
  • cmd/serve.go
  • core/membership/errors.go
  • core/membership/mocks/group_service.go
  • core/membership/mocks/project_service.go
  • core/membership/mocks/serviceuser_service.go
  • core/membership/service.go
  • core/membership/service_test.go
  • core/project/mocks/role_service.go
  • core/project/service.go
  • core/project/service_test.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/membership_service.go
  • internal/api/v1beta1connect/project.go
💤 Files with no reviewable changes (1)
  • core/project/mocks/role_service.go

Comment thread core/membership/service.go
Comment thread core/membership/service.go Outdated
Comment thread internal/api/v1beta1connect/project.go Outdated
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: 2

🧹 Nitpick comments (3)
core/membership/service.go (1)

78-102: Constructor parameter list is growing — consider grouping into a dependencies struct.

NewService now takes 10 positional arguments (3 added in this PR), and each new principal type added to membership will extend it further. A functional-options or a Dependencies struct would make call sites (cmd/serve.go, tests) more self-documenting and less error-prone — note how tests have to pass nil, nil, nil trailing args for org-only flows (Lines 260, 451 in service_test.go). Not blocking for this PR.

core/membership/service_test.go (1)

470-788: Good coverage; consider a few additional cases.

Nice range across the three principal types. A couple of gaps worth adding later:

  • validateProjectRole has two success branches (platform-wide null-UUID role vs. custom role owned by the project's parent org). Only one is indirectly exercised — an explicit test for a custom role with OrgID: someOtherOrgID returning ErrInvalidProjectRole would lock in the new org-scoping check.
  • SetProjectMemberRole failure path when replacePolicy fails (Delete or Create error) isn't covered.
  • RemoveProjectMember best-effort audit behavior when projectService.Get fails after successful deletion (Lines 479-481 of service.go) isn't asserted — currently the test passes because the mocks allow the audit to be skipped, but an explicit test would document intent.
internal/api/v1beta1connect/project.go (1)

381-402: If validateOrgMembership starts rejecting disabled users, add a user.ErrDisabled case here.

See the related comment on core/membership/service.go Lines 546-583. If ErrDisabled is surfaced from SetProjectMemberRole, it will currently fall through to CodeInternal and leak as a 500. Consider adding:

 		case errors.Is(err, user.ErrNotExist):
 			return nil, connect.NewError(connect.CodeNotFound, ErrUserNotExist)
+		case errors.Is(err, user.ErrDisabled):
+			return nil, connect.NewError(connect.CodeFailedPrecondition, ErrUserDisabled)

(assuming a local ErrUserDisabled/similar exists; otherwise map to an appropriate existing error).


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 436f601d-9095-41a9-b4f7-14db6275f13f

📥 Commits

Reviewing files that changed from the base of the PR and between f445345 and 1ccd768.

📒 Files selected for processing (4)
  • core/membership/service.go
  • core/membership/service_test.go
  • internal/api/v1beta1connect/project.go
  • pkg/auditrecord/consts.go

Comment thread core/membership/service.go
Comment thread core/membership/service.go Outdated
@whoAbhishekSah
Copy link
Copy Markdown
Member Author

Manual test report (Frontier local)

Tested against a fresh setup: 2 orgs, 3 projects, 4 users + 1 cross-org user, 3 service users, 3 groups, custom project-scoped roles in each org.

Happy paths — all pass

Principal Set (upsert) Idempotent re-set (no-op) Remove
app/user ✓ viewer → manager
app/serviceuser ✓ viewer → manager
app/group ✓ viewer → manager
Same-org custom project role

PR-specific claims verified

  • Org-scoping on project roles: Org B's custom project role rejected on Org A project → invalid_argument: role is not valid for project scope (all three principal kinds).
  • Org-scoped role rejected on projectinvalid_argument: role is not valid for project scope.
  • Cross-org SU / group rejectedfailed_precondition: user is not a member of the organization.
  • Audit records at service layer (verified via AdminService/ListAuditRecords with RQL filter):
    • project.member.role_changed and project.member.removed events emitted.
    • Target type correctly reflects the principal: app/useruser, app/serviceuserserviceuser, app/groupgroup.
    • Resource is the project (id + title), OrgID set to parent org, metadata carries principal_type and role_id.
    • Idempotent re-sets and failed calls do not emit duplicate/spurious audit records.

Unhappy paths

  • Unknown project → permission_denied (authz middleware short-circuits before service).
  • Unknown user / service user / group → not_found with correct entity label (user doesn't exist, service user not found, group doesn't exist).
  • Unknown role UUID → not_found: role id is invalid.
  • Bad principal type (app/bogus, bare user) → invalid_argument.
  • Empty principal_type / malformed UUIDs → protovalidate violations on string.min_len / string.uuid.
  • Remove non-member / double-remove → not_found (ErrNotMember), no audit emitted.

Overall: the mutation paths, validation, cross-org guards, and audit emission for all three principal types behave as the PR describes.

🤖 Tested with Claude Code

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: 2

🧹 Nitpick comments (1)
core/membership/service_test.go (1)

694-859: Add success coverage for service-user and group project membership paths.

The new service supports user, serviceuser, and group principals, but these tests only exercise a user success path. A service-user/group success case with audit record assertions would protect the principal-type mapping that this PR specifically changes.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fc26c86-670e-4313-908c-87352bbc3605

📥 Commits

Reviewing files that changed from the base of the PR and between 1ccd768 and aa0eab1.

📒 Files selected for processing (12)
  • cmd/serve.go
  • core/membership/errors.go
  • core/membership/mocks/group_service.go
  • core/membership/mocks/project_service.go
  • core/membership/mocks/serviceuser_service.go
  • core/membership/service.go
  • core/membership/service_test.go
  • internal/api/v1beta1connect/errors.go
  • internal/api/v1beta1connect/interfaces.go
  • internal/api/v1beta1connect/mocks/membership_service.go
  • internal/api/v1beta1connect/project.go
  • pkg/auditrecord/consts.go
✅ Files skipped from review due to trivial changes (3)
  • pkg/auditrecord/consts.go
  • internal/api/v1beta1connect/errors.go
  • core/membership/mocks/serviceuser_service.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/membership/errors.go
  • cmd/serve.go
  • internal/api/v1beta1connect/project.go

Comment thread core/membership/service.go Outdated
Comment thread internal/api/v1beta1connect/mocks/membership_service.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR continues the membership centralization effort by moving project member mutation logic (set role/remove member) out of core/project and into core/membership, aligning project membership writes with the same centralized pattern used for org membership.

Changes:

  • Add project member mutation APIs to core/membership (SetProjectMemberRole, RemoveProjectMember) with org-scoped role validation and org-membership validation.
  • Update v1beta1 Connect handlers/interfaces/mocks to call membershipService for project member mutations.
  • Add audit record events for project member role changes/removals and expand membership tests/mocks accordingly.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/auditrecord/consts.go Adds new audit event constants for project member mutations.
internal/api/v1beta1connect/project.go Routes project member mutations through membershipService and updates error mapping.
internal/api/v1beta1connect/mocks/project_service.go Removes project service mock methods no longer used by Connect handlers.
internal/api/v1beta1connect/mocks/membership_service.go Adds mock methods for new project membership APIs.
internal/api/v1beta1connect/interfaces.go Moves project member mutation methods from ProjectService to MembershipService interface.
internal/api/v1beta1connect/errors.go Updates already-member/not-member error text to use “principal”.
core/membership/service.go Implements project member role set/remove, org-scoped project role validation, and org membership validation.
core/membership/service_test.go Adds tests covering project member mutations (user/serviceuser/group) and audit record creation.
core/membership/mocks/serviceuser_service.go Adds serviceuser service mock used by membership service tests.
core/membership/mocks/project_service.go Extends membership project mock with Get.
core/membership/mocks/group_service.go Extends membership group mock with Get.
core/membership/errors.go Adds membership-level errors used by new project membership paths.
cmd/serve.go Updates membership service wiring to include serviceUserService dependency.
Comments suppressed due to low confidence (1)

internal/api/v1beta1connect/project.go:433

  • RemoveProjectMember no longer maps project.ErrNotExist (or other project fetch errors) and will return CodeInternal when the project doesn't exist. Add an errors.Is(err, project.ErrNotExist) case (and return the same not-found error used elsewhere in this handler/file).
		switch {
		case errors.Is(err, membership.ErrNotMember):
			return nil, connect.NewError(connect.CodeNotFound, ErrNotMember)
		case errors.Is(err, membership.ErrInvalidPrincipalType):
			return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest)
		default:
			return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/api/v1beta1connect/project.go
Comment thread internal/api/v1beta1connect/errors.go Outdated
Comment thread internal/api/v1beta1connect/project.go
Comment thread pkg/auditrecord/consts.go Outdated
Comment thread core/membership/service.go Outdated
whoAbhishekSah and others added 5 commits April 22, 2026 14:59
Move SetProjectMemberRole and RemoveProjectMember from core/project/
into core/membership/ for consistency with the centralized membership
pattern. Handlers now call membershipService instead of projectService
for project member mutations.

Key changes:
- Add validateProjectRole with org-scoping (rejects cross-org custom roles)
- Add validateOrgMembership with disabled user rejection
- Add ServiceuserService dependency for org membership validation
- Add audit records at service layer for project member mutations
- Use principal-agnostic error messages and audit target types
- Reuse principalTypeToAuditType from RemoveOrganizationMember

Ref: #1478

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validate project existence and capture context for audit before
performing any destructive operations. This ensures stale policies
for invalid project IDs are not silently deleted and audit records
are always emitted on success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ations

Cover all principal types in SetProjectMemberRole and RemoveProjectMember
tests to protect the principal-type audit mapping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Handle user.ErrDisabled in SetProjectMemberRole handler
2. Handle project.ErrNotExist in RemoveProjectMember handler
3. Make ErrAlreadyMember/ErrNotMember resource-agnostic
4. Follow {resource}.{action} pattern for audit events
5. Merge auditProjectMemberRoleChanged/Removed into single
   auditProjectMember helper with event + metadata params

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@whoAbhishekSah whoAbhishekSah force-pushed the feat/project-member-mutations-to-membership branch from a5b4ac9 to 041c3d8 Compare April 22, 2026 09:30
@whoAbhishekSah whoAbhishekSah merged commit 905317d into main Apr 22, 2026
8 checks passed
@whoAbhishekSah whoAbhishekSah deleted the feat/project-member-mutations-to-membership branch April 22, 2026 09:57
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.

4 participants