-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: use membership role as source-of-truth for organization roles #1988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -66,9 +66,15 @@ export function getEffectiveOrganizationRole({ | |||||||||||||||||||
| ownerId: string | null | undefined; | ||||||||||||||||||||
| memberRole: string | null | undefined; | ||||||||||||||||||||
| }): OrganizationRole | null { | ||||||||||||||||||||
| // Prefer organization_members.role as source-of-truth over organization.ownerId | ||||||||||||||||||||
| // to avoid drift between the two fields (see #1641). | ||||||||||||||||||||
| const normalizedRole = normalizeOrganizationRole(memberRole); | ||||||||||||||||||||
| if (normalizedRole === "owner") return "owner"; | ||||||||||||||||||||
| if (normalizedRole) return normalizedRole; | ||||||||||||||||||||
| // Fallback: if membership record is missing (e.g. legacy data), | ||||||||||||||||||||
| // derive owner from the organization.ownerId field. | ||||||||||||||||||||
| if (userId && ownerId && userId === ownerId) return "owner"; | ||||||||||||||||||||
| const role = normalizeOrganizationRole(memberRole); | ||||||||||||||||||||
| return role === "owner" ? "member" : role; | ||||||||||||||||||||
| return null; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function normalizeSpaceRole( | ||||||||||||||||||||
|
|
@@ -131,7 +137,11 @@ export function isOrganizationOwnerTarget({ | |||||||||||||||||||
| ownerId: string | null | undefined; | ||||||||||||||||||||
| targetRole: OrganizationRole | null | undefined; | ||||||||||||||||||||
| }) { | ||||||||||||||||||||
| return targetRole === "owner" || (!!targetUserId && targetUserId === ownerId); | ||||||||||||||||||||
| // Prefer membership role over ownerId (see #1641). | ||||||||||||||||||||
| if (targetRole === "owner") return true; | ||||||||||||||||||||
| if (targetRole) return false; | ||||||||||||||||||||
| // Fallback for legacy/missing membership records. | ||||||||||||||||||||
| return !!targetUserId && !!ownerId && targetUserId === ownerId; | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Context Used: AGENTS.md (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/web/lib/permissions/roles.ts
Line: 143
Comment:
**Stale OwnerId Still Wins**
When `targetRole` is already known to be `"member"` or `"admin"`, this fallback can still classify the target as owner because their id matches stale `ownerId`. In that drift state, `canChangeOrganizationMemberRole` and `canRemoveOrganizationMember` reject changes to a non-owner member, so the member can become stuck even though membership role is now the source of truth.
**Context Used:** AGENTS.md ([source](https://app.greptile.com/cap/github/capsoftware/cap/-/custom-context?memory=27801409-c24c-4476-9c6c-180f1ef0a7f2))
How can I resolve this? If you propose a fix, please make it concise.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f7d0dfd —
Comment on lines
+140
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor edge case: if
Suggested change
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied your suggestion in f7d0dfd. |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function canChangeOrganizationMemberRole({ | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
activeOrganization.memberscan ever be missing the owner membership row (legacy/migration state), this drops theownerIdfallback and can show non-owner UI for the owner. If you still want a legacy fallback without reintroducing the stale-ownerId override, only fall back when no membership record exists.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied your suggestion in f7d0dfd.
isMemberOwnernow falls back toownerIdonly when no membership record is found:member ? member.role === 'owner' : id === activeOrganization?.organization.ownerId