Skip to content

refactor(membership): stop writing org owner/member relations - #1812

Merged
whoAbhishekSah merged 4 commits into
mainfrom
refactor/stop-writing-ownership-relations
Jul 29, 2026
Merged

refactor(membership): stop writing org owner/member relations#1812
whoAbhishekSah merged 4 commits into
mainfrom
refactor/stop-writing-ownership-relations

Conversation

@whoAbhishekSah

@whoAbhishekSah whoAbhishekSah commented Jul 28, 2026

Copy link
Copy Markdown
Member

What this is

Part 2 of removing relation-based ownership, following #1809. That PR made the schema stop reading the owner and member links on orgs and groups; this one makes the membership paths stop writing them. The split was planned from the start: reading had to stop first so that a rollback of either PR never leaves anyone without access.

Changes

  • Adding an org member or changing their role writes only the policy — no org link. (The service-user identity link stays; the schema still reads it for the manage permission.)
  • A group member's permissions come from their group role (Team Owner or Team Member), granted through a policy like everywhere else. Joining a group also writes the member link, whatever the role. That link is not a permission grant — it is how roles granted to the group (e.g. a project role) fan out to the people in it, which is why it stays. A role change inside the group touches only the policy.
  • This fixes a bug that refactor(schema): grant org and group access through roles only #1809 surfaced: the group's own owner used to get an owner link instead of member, so roles granted to the group never reached the group's own owner. Group owners now receive group-granted roles like every other member.
  • Removal paths still delete the old links, so members from before this change leave no stale data behind.

Behavior change to be aware of: group owners gain access through roles granted to their group. They were wrongly excluded before.

🤖 Generated with Claude Code

Since the schema stopped reading the owner and member relations on
organizations (#1809), writing them on every membership change is dead
work. Stop writing them:

- AddOrganizationMember and SetOrganizationMemberRole no longer write
  or replace org relations. The service user identity link
  (serviceuser#org@organization) is still written — the schema reads it
  to resolve the manage permission.
- Groups always write the member relation now, never owner. The member
  relation stays because role grants to a group flow through
  group#member. This also fixes a real bug: a role granted to a group
  never reached the group's own owner, because owners got an owner
  relation instead of member. Group owners now receive group-granted
  roles like every other member.
- Group role changes migrate a legacy owner relation to member.
- linkGroupToOrg no longer writes organization#member@group#member; it
  granted nothing after the schema change. The removal paths still
  delete it (and the org owner/member relations) so members and groups
  from before this change leave no stale data behind.
- Delete the orgRoleToRelation/groupRoleToRelation mapping helpers.

The relations stay declared in the schema until the existing tuples are
cleaned up; SpiceDB cannot drop a relation while data references it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 28, 2026 10:28am

@whoAbhishekSah

whoAbhishekSah commented Jul 28, 2026

Copy link
Copy Markdown
Member Author

Tested live on a local Frontier (Postgres + SpiceDB) running this branch, using a real org with users covering each path. All checks via CheckResourcePermission as the affected user, and link inspection via the admin Relations API.

1. Adding an org member writes no links. Added a fresh user as org viewer. The Relations API shows zero app/organization links for them, and they can still read the org — the role policy alone does the work.

2. Group owners now receive group-granted roles (the bug fix). New group, made the user its Team Owner, granted the group app_project_viewer on a project. The user can read the project. Before this change the same setup was denied — verified during the #1809 testing. Their only group link is member; no owner link was written.

3. Org role change works with no link step. Promoted a viewer to owner — policy replaced, nothing else written, access correct.

4. Group role change works with no link step. The member link is written on join and never changes shape; a role change replaces the policy only. Pre-existing group owner links are converted to member by the follow-up data cleanup, not by this code.

5. Removal cleans up fully. Removed the user from the org: group member link, role bindings, everything gone; org and project access denied afterwards.

Also reproduced the bug itself end to end: with the old-style owner-only link, ListProjectsByCurrentUser lists the group's project (both with and without nonInherited) while CheckResourcePermission denies opening it — the list reads policies, the check reads SpiceDB, and they disagree exactly for group owners. With the member link in place the two agree.

Unit tests updated to match: no expectations on org link writes, group add asserts a member-only write, role changes assert policy-only writes.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@whoAbhishekSah, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 26dd56f5-f628-46ce-b28f-544dd58834b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8084767 and d1aa91b.

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

Walkthrough

Organization membership flows now update policies without organization relation writes. Group memberships consistently use member relations, migrate legacy owner tuples, and simplify group-to-organization linking. Tests update relation expectations and rollback coverage.

Changes

Membership relation changes

Layer / File(s) Summary
Organization policy-based roles
core/membership/service.go, core/membership/service_test.go
Organization member creation and role changes retain role validation, policy updates, service-user identity linking, and audit behavior without organization relation CRUD.
Group member relation migration
core/membership/service.go, core/membership/service_test.go
Group membership writes use the member relation, while role changes remove legacy owner relations and update corresponding expectations.
Group hierarchy lifecycle
core/membership/service.go, core/membership/service_test.go
Group setup creates only the group-to-organization identity link, and creation rollback tests cover the revised member relations and cleanup behavior.

Estimated code review effort: 4 (Complex) | ~45 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 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.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

1316-1401: 🚀 Performance & Scalability | 🔵 Trivial

Group member relation migration looks correct.

Always writing schema.MemberRelationName on add, and migrating only the legacy OwnerRelationName to MemberRelationName on role change, matches the PR's stated intent that all group members (including owners) carry the member relation while cleaning up legacy owner tuples.

One minor note: on the promotion path (member → owner), replaceRelation will unconditionally re-Create the member relation even though the principal already holds it (only the owner relation is in oldRelations). This is presumably harmless if the underlying RelationService.Create is idempotent/touch-semantics against SpiceDB (which the tests assume), but worth a quick sanity check if Create ever surfaces a "duplicate relationship" error.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cde9e66c-340d-4508-9156-ab4a2a9c9ee8

📥 Commits

Reviewing files that changed from the base of the PR and between ab8b990 and 27eb10f.

📒 Files selected for processing (2)
  • core/membership/service.go
  • core/membership/service_test.go

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coveralls

coveralls commented Jul 28, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 30350730670

Coverage decreased (-0.04%) to 46.901%

Details

  • Coverage decreased (-0.04%) from the base build.
  • Patch coverage: 4 of 4 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 38910
Covered Lines: 18249
Line Coverage: 46.9%
Coverage Strength: 14.37 hits per line

💛 - Coveralls

The member relation is created when the principal joins the group and
never changes shape after that, so a role change has no relation work.
Existing owner relations get converted to member in the data cleanup,
not lazily per request. Delete the now-unused replaceRelation helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@whoAbhishekSah

Copy link
Copy Markdown
Member Author

Tested this live on a local server built from this branch, with a fresh org and SpiceDB tuples checked directly in the datastore after each step.

# Test Result
1 Create org — only rolebinding tuple written for creator, no organization#owner
2 Add 5 members (owner/manager/viewer roles) — policies only, zero org owner/member tuples
3 Org access by role: viewer read-only, manager can update, outsider denied
4 ListOrganizationUsers with roles — all members with correct role names
5 Org role promote/demote — permissions flip, SpiceDB tuples unchanged
6 Project members via SetProjectMemberRole — policy only; access matrix correct
7 Group add: creator and Team Owner both get member tuple, zero group#owner
8 No organization#member@group#member fan-in tuple on group creation; group#org link present
9 Bug fix: project role granted to the group reaches the group owner (denied before grant, allowed after)
10 Group role promote/demote — tuples stable, permissions flip both ways
11 Custom resource (database/instance) with custom roles — get/edit/delete check matrix exact per role
12 Org owner reaches project resource through role chain alone (no direct grant, no owner relation)
13 Remove group member — tuple, policy, and group-granted access all gone
14 Remove org member — project policies cascade away too
15 Legacy cleanup: hand-written organization#member tuple deleted by the removal path
16 Service user: serviceuser#org identity link written, nothing on the org side; manage works (token create ok for owner/manager, denied for viewer)

Listing calls exercised: ListOrganizationUsers, ListProjectUsers, ListGroupUsers, ListProjectsByCurrentUser, ListCurrentUserGroups, ListOrganizationsByCurrentUser. Check calls: CheckResourcePermission and BatchCheckPermission.

One pre-existing quirk (not from this PR): CheckResourcePermission on app/serviceuser returns not_found because no permissions are registered under that namespace.

🤖 Generated with Claude Code

@whoAbhishekSah
whoAbhishekSah merged commit 0145b65 into main Jul 29, 2026
8 checks passed
@whoAbhishekSah
whoAbhishekSah deleted the refactor/stop-writing-ownership-relations branch July 29, 2026 05:25
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