Skip to content

feat: add SetOrganizationMemberRole RPC#1471

Merged
whoAbhishekSah merged 24 commits intomainfrom
feat/set-organization-member-role
Mar 25, 2026
Merged

feat: add SetOrganizationMemberRole RPC#1471
whoAbhishekSah merged 24 commits intomainfrom
feat/set-organization-member-role

Conversation

@whoAbhishekSah
Copy link
Copy Markdown
Member

@whoAbhishekSah whoAbhishekSah commented Mar 23, 2026

Summary

Adds SetOrganizationMemberRole RPC for atomic role changes on org members. Replaces the SDK's multi-step listPolicies → deletePolicy x N → createPolicy flow.

  • Deletes existing org-level policies, creates new one with specified role
  • Enforces minimum owner constraint (cannot remove last owner)
  • Validates role is valid for organization scope (global org roles or org-specific roles)
  • Authorization: policymanage permission on org

How it works

  1. Validate request - Check that the organization, user, and role all exist. If org is disabled, return error. Validates role is valid for organization scope.

  2. Get user's current org-level policies - Fetch policies for this user in this org. Project and group policies are intentionally not included because org owner/admin get implicit project access via SpiceDB relations (org->project_get). Explicit project policies are for users who need project-specific access.

  3. Check minimum owner constraint - Before changing roles, verify this won't remove the last owner. We look up the owner role by name to get its UUID, then count how many owners exist. If the user is currently an owner and would be the last one, reject with ErrLastOwnerRole.

  4. Replace policies - Delete all existing org-level policies for the user, then create a new policy with the specified role. This ensures one role per user per org.

Note: This assumes one role per user per org. If multiple roles need to be supported, consider accepting a list of roles or providing separate Add/Remove methods.

Testing

E2E tested with 31 test cases covering:

Permission & Authorization (Tests 1-21)

  • Owner can promote/demote members ✅
  • Member/Admin cannot self-promote or demote owners ❌ (permission_denied)
  • Admin can add users but cannot use SetOrganizationMemberRole ❌ (permission_denied)
  • Last owner cannot self-demote ❌ (failed_precondition)
  • Invalid/empty IDs return appropriate errors ❌ (invalid_argument/not_found)

Role Scope Validation (Tests 22-31)

  • Global org-scoped roles accepted: Member, Admin, Owner (app/organization scope) ✅
  • Project-scoped roles rejected: Project Owner, Project Viewer (app/project scope) ❌
  • Group-scoped roles rejected: Team Member, Team Owner (app/group scope) ❌
  • Null-scoped roles rejected: Access Manager, Billing Manager, Project Manager ❌

All 31 tests pass.

Proto

Requires proton PR: raystack/proton#453

Related

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 23, 2026

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Mar 25, 2026 8:12am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a new SetOrganizationMemberRole RPC endpoint to handle organization member role changes server-side. The implementation spans service-layer logic (including minimum owner validation and atomic policy replacement), API handler with error mapping, authorization middleware updates, and comprehensive test coverage. The Makefile PROTON_COMMIT is also updated.

Changes

Cohort / File(s) Summary
Build & Proto Generation
Makefile
Updated PROTON_COMMIT to new commit hash for proto artifact source.
Core Organization Service
core/organization/errors.go, core/organization/service.go
Added ErrLastOwnerRole error; introduced RoleService interface dependency; implemented SetMemberRole(ctx, orgID, userID, newRoleID) with org/user/role existence validation, owner count enforcement, and atomic policy replacement (delete existing, create new).
Core Organization Mocks
core/organization/mocks/role_service.go
Added autogenerated mock for RoleService with Get method expectation support.
API Interfaces & Implementation
internal/api/v1beta1connect/interfaces.go, internal/api/v1beta1connect/organization.go, internal/api/v1beta1connect/mocks/organization_service.go
Added SetMemberRole method to OrganizationService interface; implemented handler with input validation, service delegation, and error-to-Connect-code mapping (ErrLastOwnerRoleCodeFailedPrecondition); added comprehensive mock with call expectations.
Authorization & Access Control
pkg/server/connect_interceptors/authorization.go
Added authorization check for SetOrganizationMemberRole RPC, enforcing PolicyManagePermission on target organization resource.
Proto Definitions
proto/v1beta1/frontier.pb.validate.go, proto/v1beta1/frontierv1beta1connect/frontier.connect.go
Added validation methods and error types for SetOrganizationMemberRoleRequest/Response; added RPC procedure constant, client/handler implementation, and routing dispatch.
Server Initialization
cmd/serve.go
Updated organization.NewService(...) call to pass roleService dependency.
Tests
core/organization/service_test.go, internal/api/v1beta1connect/organization_test.go
Added TestService_SetMemberRole covering org/user/role existence, owner demotion rejection, and successful role assignment; added TestHandler_SetOrganizationMemberRole validating error codes and success path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • rohilsurana
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed PR implements core objectives from #1459: single atomic SetOrganizationMemberRole RPC [#1459], minimum-owner constraint [#1459], service-layer role validation [#1459], and authorization via policymanage permission [#1459].
Out of Scope Changes check ✅ Passed All changes scope to SetOrganizationMemberRole implementation. Proto dependency version bump (Makefile PROTON_COMMIT) and test mocks are supporting changes for the feature.

✏️ 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.

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.

🧹 Nitpick comments (5)
core/organization/service.go (1)

407-423: Non-atomic delete-then-create leaves user without a role on partial failure.

If any policyService.Delete succeeds but the subsequent policyService.Create fails (e.g., invalid roleID, SpiceDB unavailable), the user will be left with fewer or no org-level policies. This is a known architectural limitation (no distributed transaction between app DB and SpiceDB), but worth documenting in the method's godoc or adding a TODO for future transactional support.

Consider adding a comment acknowledging this limitation:

// Note: Delete+Create is not atomic. If Create fails after Delete succeeds,
// the user may be left without a policy. This is a known limitation until
// cross-store transaction control is available.
docs/analysis-role-change-bugs.md (2)

29-32: Add language identifiers to fenced code blocks.

Static analysis flagged several code blocks without language specifiers. Adding language hints improves syntax highlighting and accessibility:

-```
+```text
 1. DeletePolicy(policyId)                        ← for each existing policy

Similar changes needed for blocks at lines 38, 54, 105, 160, and 178.


363-369: Add blank line before table.

Per markdownlint MD058, tables should be surrounded by blank lines for consistent rendering:

 **Files to modify**:
+
 | File | Change |
 |------|--------|
internal/api/v1beta1connect/organization.go (1)

543-543: Add request validation to match established codebase pattern.

The SetOrganizationMemberRoleRequest has proto validation rules defined (minimum length of 3 runes for OrgId, UserId, and RoleId). Other handlers across the codebase (e.g., user_pat.go, session.go, audit_record.go) consistently call request.Msg.Validate() before processing. Add the same validation here:

Example validation call
if err := request.Msg.Validate(); err != nil {
    return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
docs/sdk-iam-rpc-profile.md (1)

21-25: Add language specifier to fenced code block.

The fenced code block is missing a language specifier. Add ```text or ```plaintext to resolve the markdown linting warning.

📝 Proposed fix
-```
+```text
 1. listPolicies(resource, userId)       → fetch existing policies
 2. deletePolicy(policyId) x N           → delete all existing policies
 3. createPolicy(roleId, resource, principal) → create new policy with desired role
</details>

</blockquote></details>

</blockquote></details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Path: .coderabbit.yaml

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `7d54427f-5739-4888-abcf-3fa71ef32f33`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fec853b74ce91188250703dd517285b2cfdd9a22 and 26448595e7ce99a672f7dc0696fdb9983137de3e.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `proto/v1beta1/frontier.pb.go` is excluded by `!**/*.pb.go`

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `Makefile`
* `core/organization/errors.go`
* `core/organization/service.go`
* `docs/analysis-role-change-bugs.md`
* `docs/sdk-iam-rpc-profile.md`
* `internal/api/v1beta1connect/interfaces.go`
* `internal/api/v1beta1connect/mocks/organization_service.go`
* `internal/api/v1beta1connect/organization.go`
* `pkg/server/connect_interceptors/authorization.go`
* `proto/v1beta1/frontier.pb.validate.go`
* `proto/v1beta1/frontierv1beta1connect/frontier.connect.go`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4671ca65-6dcf-4d0f-90cb-ea106044118b

📥 Commits

Reviewing files that changed from the base of the PR and between 2644859 and daab379.

⛔ Files ignored due to path filters (1)
  • proto/v1beta1/frontier.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (3)
  • Makefile
  • proto/v1beta1/frontier.pb.validate.go
  • proto/v1beta1/frontierv1beta1connect/frontier.connect.go
✅ Files skipped from review due to trivial changes (1)
  • Makefile

@coveralls
Copy link
Copy Markdown

coveralls commented Mar 23, 2026

Pull Request Test Coverage Report for Build 23531223348

Details

  • 116 of 135 (85.93%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.2%) to 41.238%

Changes Missing Coverage Covered Lines Changed/Added Lines %
cmd/serve.go 0 1 0.0%
pkg/server/connect_interceptors/authorization.go 0 4 0.0%
core/organization/service.go 82 96 85.42%
Totals Coverage Status
Change from base Build 23530934127: 0.2%
Covered Lines: 14699
Relevant Lines: 35644

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5656938d-84e8-42cc-9689-a6bdd6117882

📥 Commits

Reviewing files that changed from the base of the PR and between 4122021 and c27e11e.

📒 Files selected for processing (6)
  • cmd/serve.go
  • core/organization/mocks/role_service.go
  • core/organization/mocks/role_service_func.go
  • core/organization/service.go
  • core/organization/service_test.go
  • internal/api/v1beta1connect/organization.go
✅ Files skipped from review due to trivial changes (1)
  • core/organization/mocks/role_service.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/organization/service.go

@whoAbhishekSah
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

♻️ Duplicate comments (1)
internal/api/v1beta1connect/organization.go (1)

543-545: ⚠️ Potential issue | 🟠 Major

Reject malformed user_id values up front.

Only the empty string is rejected here. A non-empty bad UUID still falls through to organization.Service.SetMemberRole and gets surfaced as a downstream 404/500 instead of the invalid_argument contract implied by ErrInvalidUserID.

Suggested validation tightening
-	// Validate user_id is not empty
-	if userID == "" {
+	// Validate user_id format
+	if userID == "" || !utils.IsValidUUID(userID) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidUserID)
 	}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c546ad1-6cbe-43a5-b511-36d071b69224

📥 Commits

Reviewing files that changed from the base of the PR and between c27e11e and 3b3b437.

📒 Files selected for processing (6)
  • cmd/serve.go
  • core/organization/mocks/role_service.go
  • core/organization/service.go
  • core/organization/service_test.go
  • internal/api/v1beta1connect/organization.go
  • internal/api/v1beta1connect/organization_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/serve.go
  • core/organization/service.go

whoAbhishekSah and others added 23 commits March 25, 2026 13:40
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds atomic role assignment for organization members.

Changes:
- Add SetMemberRole method to organization service
- Add ErrLastOwnerRole error for minimum owner constraint
- Add SetOrganizationMemberRole handler (thin, delegates to service)
- Add authorization check (policymanage permission)
- Regenerate mocks

The service handles:
- Validate org, user exist
- Check minimum owner constraint before demotion
- Delete existing policies atomically
- Create new policy with new role

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Explains why project/group policies are not touched during org role change.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add role existence validation in handler (returns invalid_argument)
- Add empty user_id validation in handler
- Fix owner role comparison to use UUID instead of role name
- Add RoleService dependency to organization service for role lookups
- Ensure min 1 owner check works correctly with UUID comparisons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove adapter pattern and use role.Role type directly in organization
service, avoiding unnecessary type conversion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract helper functions for better readability:
- getUserOrgPolicies: fetches user's org-level policies
- validateMinOwnerConstraint: ensures at least 1 owner remains
- replaceUserOrgPolicies: deletes old policies, creates new one

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Role existence check is business logic and belongs in the service layer,
not the handler.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use service Get() method for org check (checks disabled state too)
- Package org, user, role validation in one helper function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cover all error branches:
- empty user_id (invalid argument)
- org not found
- org disabled
- user not found
- role not found
- role invalid id
- last owner constraint (failed precondition)
- unknown error (internal)
- success case

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reject malformed user_id values with CodeInvalidArgument instead of
letting them fall through to downstream 404/500 errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ErrLastOwnerRole is a different invariant from RemoveOrganizationUser's
last-admin check. Using separate errors ensures clients receive the
correct failure reason.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update proton commit to use UUID validation rules
- Replace manual user_id UUID check with proto Validate()
- All three fields (org_id, user_id, role_id) now validated as UUIDs
- Update tests to use valid UUIDs and check error codes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unnecessary ErrNotExist check in getUserOrgPolicies (policy
  service returns empty list, not error)
- Return early if user is not currently an owner (optimization)
- Simplify owner count check to len(ownerPolicies) <= 1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SetOrganizationMemberRole should only change roles for existing members.
Adding new members should go through AddOrganizationUsers.

- Add ErrNotMember error
- Check user has existing org policies before proceeding
- Return FailedPrecondition if user is not a member

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Role must be either:
- A global org role (OrgID="" and Scopes contains "app/organization")
- An org-specific role for this org (OrgID matches target org)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Global roles have org_id as either empty string or nil UUID
"00000000-0000-0000-0000-000000000000". Check for both.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update PROTON_COMMIT to ad22478c67a543d2359304e4cc2704a02a2bfc55
- Use utils.IsNullUUID() for cleaner global role check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@whoAbhishekSah whoAbhishekSah force-pushed the feat/set-organization-member-role branch from 9f34814 to eae912d Compare March 25, 2026 08:11
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@whoAbhishekSah whoAbhishekSah enabled auto-merge (squash) March 25, 2026 08:14
@whoAbhishekSah whoAbhishekSah merged commit d589e02 into main Mar 25, 2026
8 checks passed
@whoAbhishekSah whoAbhishekSah deleted the feat/set-organization-member-role branch March 25, 2026 08: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.

Add SetOrganizationMemberRole RPC to replace client-side policy manipulation

3 participants