Skip to content

fix(security): members can only grant permissions they hold - #6624

Merged
mikeh-lago merged 8 commits into
mainfrom
security/gl-004-role-permission-subset
Oct 12, 2026
Merged

mikeh-lago merged 8 commits into
mainfrom
security/gl-004-role-permission-subset

Conversation

@mikeh-lago

@mikeh-lago mikeh-lago commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Context

A member allowed to manage roles or members could grant permissions they don't hold themselves: create or update a role with extra permissions, then assign it to themselves or someone else through a membership update or an invite.

A custom role could also be named like a predefined role (Admin, Finance, Manager), which made it look like the real one in the UI.

Changes

  • Membership#can_grant_roles?: admins can grant any role; other members only roles whose permissions they all hold.
  • Roles::CreateService / Roles::UpdateService: refuse to add a permission the acting member doesn't hold (cannot_grant_permissions).
  • Memberships::UpdateService, Invites::CreateService, Invites::UpdateService: refuse to assign a role the acting member can't grant.
  • Role.grantable GraphQL field, so the front end can disable roles the current member can't grant.
  • Role: names admin, finance and manager are reserved (case-insensitive) on create and rename. Existing roles stay valid until renamed.

Tests

Model, service and GraphQL specs for each path, including a non-admin member trying to grant a permission they don't hold, and reserved names in any case.

@mikeh-lago
mikeh-lago force-pushed the security/gl-004-role-permission-subset branch from a095fc5 to 2db9506 Compare October 9, 2026 23:08
@mikeh-lago
mikeh-lago marked this pull request as ready for review October 10, 2026 14:04
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T14:09:14.005014Z a47e254 Draft marked ready
🔒 Security Review ✅ Completed 2026-10-10T14:11:53.678272Z a47e254 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a47e254048

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread spec/models/role_spec.rb Outdated
Comment thread spec/services/invites/update_service_spec.rb Outdated
Comment thread app/models/membership.rb Outdated
Comment thread app/graphql/types/role_type.rb
Comment thread app/models/role.rb Outdated
Comment thread app/services/roles/update_service.rb Outdated
Comment thread app/services/roles/create_service.rb
Comment thread app/models/role.rb Outdated
Comment thread app/services/invites/create_service.rb
Comment thread app/services/memberships/update_service.rb
mikeh-lago and others added 7 commits October 12, 2026 22:15
Creating a role, or adding permissions to one, now requires the acting
member to hold every permission being granted. Admins are not affected.
Renaming a role or removing permissions from it stays allowed.
… does not hold

A non-admin member with members:update or members:create could assign
or invite any non-admin role, including roles carrying permissions they
do not hold, and assign such a role to themselves.

- Add Membership#can_grant_roles?: admins can grant any role, other
  members only roles whose permissions they hold.
- Check it in Memberships::UpdateService and Invites::UpdateService for
  added roles, and in Invites::CreateService for the invite roles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
Add a grantable field on Role, computed with Membership#can_grant_roles?,
so the front end can disable roles the member is not allowed to assign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
A custom role can no longer be created or renamed to admin, finance or
manager (case-insensitive), so it cannot be mistaken for a predefined
role. Existing roles stay valid until their name changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
can_grant_roles? compared permissions only, so a non-admin holding every
permission saw the predefined Admin role as grantable while the services
reject that assignment. Admin roles are now grantable by admins only.

Specs use a named subject and declare fixtures outside examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
- Membership#can_grant_permissions? holds the grant rule once; role
  services and can_grant_roles? use it.
- Roles::UpdateService refuses any update of a role the member could not
  grant, not only added permissions.
- Reserved names use a format validation, and normalize_name collapses
  Unicode spaces and format characters.
- Spec for a member update keeping a stronger role already held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
Reload the shared membership between examples and give the grantable
contexts their own user, since main now creates a default user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
@mikeh-lago
mikeh-lago force-pushed the security/gl-004-role-permission-subset branch from 99ac41b to ced67d8 Compare October 12, 2026 20:20
Saving an existing role no longer rewrites its name, so a legacy name with
a Unicode space stays updatable. Normalization also removes default
ignorable code points (e.g. U+034F) before the reserved name check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQ5fPejwJTr99LvF5zYRbw
@mikeh-lago
mikeh-lago merged commit 0ac3da9 into main Oct 12, 2026
13 checks passed
@mikeh-lago
mikeh-lago deleted the security/gl-004-role-permission-subset branch October 12, 2026 20:48
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.

2 participants