Skip to content

feat: add service user support to membership validatePrincipal#1562

Merged
whoAbhishekSah merged 1 commit intomainfrom
feat/membership-serviceuser-support
Apr 22, 2026
Merged

feat: add service user support to membership validatePrincipal#1562
whoAbhishekSah merged 1 commit intomainfrom
feat/membership-serviceuser-support

Conversation

@whoAbhishekSah
Copy link
Copy Markdown
Member

@whoAbhishekSah whoAbhishekSah commented Apr 21, 2026

Summary

  • Add ServiceUserService interface and dependency to the membership package
  • Extend validatePrincipal() to accept app/serviceuser principals (validates existence and enabled state)
  • Update ErrInvalidPrincipal message to be type-agnostic
  • Wire serviceUserService into membership.NewService in bootstrap

Why this is needed

Today, when a service user is created, the backend only creates a bare org#member@serviceuser relation — no policy, no role. The SDK confirms this:

  1. CreateServiceUser(orgId, title) → backend creates relation only
  2. CreatePolicyForProject(projectId, app_project_owner, app/serviceuser:{id}) → project access via policy
  3. Token creation

The service user has no org-level role — just a raw SpiceDB relation. This means:

  • Service users don't appear in policy-based member listings
  • There's no role to change (SetOrganizationMemberRole can't operate on them)
  • The access model is inconsistent with regular users

The next PR will migrate serviceuser.Create() to call membershipService.AddOrganizationMember(), which creates a proper policy with an explicit role (e.g., app_organization_viewer). This PR is the prerequisite — the membership package needs to know how to validate a service user principal before it can add one.

Context

Part of the membership package migration (#1478). Follows #1561 which removes the unused serviceuser#user relation.

Test plan

  • Build passes
  • All membership unit tests pass (updated to pass new dependency)
  • Existing "unsupported principal type" tests updated to use app/unknown instead of app/serviceuser

🤖 Generated with Claude Code

@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 25 minutes and 15 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 25 minutes and 15 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: 3b2b091b-4a24-494d-9cd8-1803d6a8c70e

📥 Commits

Reviewing files that changed from the base of the PR and between ac2bf4b and 57896c5.

📒 Files selected for processing (3)
  • core/membership/errors.go
  • core/membership/service.go
  • core/membership/service_test.go
📝 Walkthrough

Walkthrough

The changes extend the membership service to support service user principals by introducing a new ServiceUserService dependency, updating the principal validation logic to handle service users, and wiring this dependency throughout the codebase via dependency injection.

Changes

Cohort / File(s) Summary
Dependency Injection Wiring
cmd/serve.go
Updated membershipService constructor to inject serviceUserService as a new dependency argument before auditRecordRepository.
Error Message Updates
core/membership/errors.go
Changed ErrInvalidPrincipal error message from "only user principals are supported" to "unsupported principal type".
Service Interface & Logic
core/membership/service.go
Added ServiceUserService interface with Get(ctx, id) method. Injected serviceUserService field into Service struct. Updated NewService constructor signature to accept the new dependency. Extended validatePrincipal to fetch and validate service user principals via serviceUserService.Get, mapping disabled states to user.ErrDisabled.
Testing Infrastructure
core/membership/mocks/service_user_service.go, core/membership/service_test.go
Added autogenerated mock implementation for ServiceUserService interface with Get method and expectation helpers. Updated test constructors to inject mock service user service and adjusted test case expectations for unsupported principal type validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
🚥 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.

@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 10:03am

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 21, 2026

Coverage Report for CI Build 24769902733

Coverage increased (+0.03%) to 42.258%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 3 uncovered changes across 2 files (36 of 39 lines covered, 92.31%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
core/membership/service.go 38 36 94.74%
cmd/serve.go 1 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 36914
Covered Lines: 15599
Line Coverage: 42.26%
Coverage Strength: 11.94 hits per line

💛 - Coveralls

@whoAbhishekSah
Copy link
Copy Markdown
Member Author

Manual end-to-end test report

Tested against branch feat/membership-serviceuser-support @ 2f107e7a. Simulated the full SDK flow (create SU → project policy → token → authed calls) and verified the membership-package wiring doesn't regress any existing path.

SDK flow (matches the 1561 test shape)

Step Action Result
1 CreateServiceUser(org_id, title) ✅ SU created; SpiceDB writes serviceuser#member@organization + back-ref organization#org@serviceuser; no policies row (parity with SDK — no org role assigned)
2 CreatePolicyForProject with app_project_owner, principal app/serviceuser:<id> policies row inserted (app/project → app_project_owner → app/serviceuser)
3 CreateServiceUserToken (fields: id, title, org_id) ✅ Opaque token returned; serviceuser_credentials row with type=opaque_token
4a GetOrganization with Authorization: Basic base64(credId:secret) ✅ 200
4b Same call with Authorization: Bearer <opaque> ✅ 401 unauthenticated (confirms the #1561 note — opaque tokens require Basic)
4c GetCurrentUser authed as SU ✅ returns serviceuser{id, title, org_id}
4d GetProject(granted) ✅ 200
4e GetProject(not-granted sibling) ✅ 403 permission_denied
4f ListOrganizationProjects ✅ 403 (expected — SU has no org role, only the bare member relation; matches pre-PR behaviour)
4g CreateProject in its own org ✅ 403 (no org-level role)
4h GetOrganization on unrelated org ✅ 403
4i ListOrganizationServiceUsers (as owner) ✅ SU appears
4j ListOrganizationUsers (as owner) ✅ SU does not appear (users-only listing)

Regression: user-principal path through membership.AddOrganizationMember

Critical no-regression check for this PR, since validatePrincipal grew a new case.

Check Result
AdminService/AddOrganizationMembers adds bob as viewer (role UUID) ✅ 200 success:true
Same call again ✅ 200 with error:"principal is already a member of this resource"ErrAlreadyMember still surfaces correctly
ListOrganizationUsers reflects the new member

PR surface area — what's actually reachable

Aspect Status
DI wiring (cmd/serve.go) — server boots with the new serviceUserService arg ✅ observed live (running debug binary came up on this branch)
Error message (core/membership/errors.go) — ErrInvalidPrincipal now "unsupported principal type" (was "only user principals are supported") ✅ confirmed in diff
validatePrincipal(app/serviceuser) (core/membership/service.go:361-373) — reachable via any public RPC today ⚠️ not yet — every current caller of AddOrganizationMember hardcodes schema.UserPrincipal (admin handler organization.go:561, invitation/service.go:316, domain/service.go:161, organization/service.go:468; the principal.Type path in organization/service.go:212 is guarded by ErrUserPrincipalOnly). Pure prerequisite wiring — the new branch starts firing in the follow-up PR that migrates serviceuser.Create to go through membership. Unit tests in core/membership/service_test.go cover the branch directly.

Delete flow

Check Result
DeleteServiceUser → 200
serviceusers row hard-deleted
serviceuser_credentials cascaded
Deleted SU's token rejected on next call ✅ 401
policies row for SU (project policy) ⚠️ leaks — pre-existing (noted in #1561), orthogonal to this PR; will be addressed by the upcoming core/membership migration (#1478)

Verdict

LGTM. The validatePrincipal change is a well-scoped prerequisite: the SU branch is fully unit-tested, the DI wiring is exercised by every server start, and the user-principal path is regression-clean end-to-end. The error-message rewording is consistent with the new behaviour. The new SU code path remains dormant in RPCs until the follow-up lands, exactly as advertised.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/membership/service.go (1)

591-593: ⚠️ Potential issue | 🟠 Major

Record service users as service-user audit targets.

Now that validatePrincipal can return schema.ServiceUserPrincipal, add/role-change audit records still persist pkgAuditRecord.UserType. The remove path already maps principal type to audit type; do the same here to avoid misclassifying service-user membership changes.

Suggested audit target mapping
 func (s *Service) auditOrgMemberRoleChanged(ctx context.Context, org organization.Organization, p principalInfo, roleID string) {
+	targetType := auditRecordTargetType(p)
 	s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{
@@
 		Target: &auditrecord.Target{
 			ID:   p.ID,
-			Type: pkgAuditRecord.UserType,
+			Type: targetType,
 			Name: p.Name,
 func (s *Service) auditOrgMemberAdded(ctx context.Context, org organization.Organization, p principalInfo, roleID string) {
+	targetType := auditRecordTargetType(p)
 	s.auditRecordRepository.Create(ctx, auditrecord.AuditRecord{
@@
 		Target: &auditrecord.Target{
 			ID:   p.ID,
-			Type: pkgAuditRecord.UserType,
+			Type: targetType,
 			Name: p.Name,
func auditRecordTargetType(p principalInfo) pkgAuditRecord.EntityType {
	switch p.Type {
	case schema.ServiceUserPrincipal:
		return pkgAuditRecord.ServiceUserType
	default:
		return pkgAuditRecord.UserType
	}
}

Also applies to: 620-622


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 07d2cd7d-201c-44a2-8430-d80e4d463188

📥 Commits

Reviewing files that changed from the base of the PR and between 79267cb and ac2bf4b.

📒 Files selected for processing (5)
  • cmd/serve.go
  • core/membership/errors.go
  • core/membership/mocks/service_user_service.go
  • core/membership/service.go
  • core/membership/service_test.go

Comment thread core/membership/service_test.go
Comment thread core/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

Adds service-user awareness to the core/membership package so membership operations can validate app/serviceuser principals (existence, enabled state, org ownership) as a prerequisite for migrating service-user creation to go through membership.

Changes:

  • Introduce ServiceUserService interface and inject it into membership.NewService.
  • Extend validatePrincipal() to accept schema.ServiceUserPrincipal and validate org ownership + enabled state.
  • Update membership unit tests and bootstrap wiring to include the new dependency.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
core/membership/service.go Adds ServiceUserService dependency and extends principal validation to include service users.
core/membership/service_test.go Updates constructor calls and adds tests covering service-user membership addition validation.
core/membership/mocks/service_user_service.go Adds generated mock for the new ServiceUserService dependency.
core/membership/errors.go Updates ErrInvalidPrincipal message.
cmd/serve.go Wires serviceUserService into membership.NewService during bootstrap.

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

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

@whoAbhishekSah whoAbhishekSah left a comment

Choose a reason for hiding this comment

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

/review

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 extends the core/membership package to support app/serviceuser principals in validatePrincipal, enabling membership mutations to validate service users (existence + enabled state + org ownership) and wiring the needed dependency through bootstrap and tests as a prerequisite for migrating service user creation to use the membership service.

Changes:

  • Add ServiceUserService interface + dependency injection into membership.NewService, and wire it in cmd/serve.go.
  • Extend validatePrincipal() to handle schema.ServiceUserPrincipal (org match + disabled check) and add service-user-specific membership tests.
  • Update principal-related error messaging to be type-agnostic and adjust audit target typing/metadata to support non-user principals.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
core/membership/service.go Adds ServiceUserService dependency and service-user-aware principal validation + audit target typing changes.
core/membership/errors.go Updates principal error message(s) and introduces ErrPrincipalNotInOrg.
core/membership/service_test.go Updates constructor usage for new dependency and adds AddOrganizationMember coverage for service users.
core/membership/mocks/service_user_service.go Adds mockery-generated mock for ServiceUserService.
cmd/serve.go Wires serviceUserService into membership.NewService during API dependency construction.

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

Comment thread core/membership/service.go
Comment thread core/membership/errors.go
Comment thread core/membership/service.go
Extend the membership package to validate service user principals
alongside regular users. This is a prerequisite for migrating service
user org membership from direct relation creation to the membership
package.

Changes:
- Add orgID param to validatePrincipal for cross-org validation
- Implement ServiceUserPrincipal case with org ownership + disabled checks
- Add ErrPrincipalNotInOrg for cross-org rejection (distinct from ErrInvalidPrincipal)
- Use serviceuser.ErrDisabled instead of user.ErrDisabled
- Fix audit helpers to use principalTypeToAuditType and conditional email
- Update docstrings for AddOrganizationMember and SetOrganizationMemberRole

Ref: #1478

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@whoAbhishekSah whoAbhishekSah merged commit 6e470ce into main Apr 22, 2026
8 checks passed
@whoAbhishekSah whoAbhishekSah deleted the feat/membership-serviceuser-support branch April 22, 2026 10:14
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