Skip to content

feat(roles): add full CRUD API with account_owner scoping (EVO-1061)#21

Merged
dpaes merged 3 commits into
developfrom
fix/EVO-1061
May 14, 2026
Merged

feat(roles): add full CRUD API with account_owner scoping (EVO-1061)#21
dpaes merged 3 commits into
developfrom
fix/EVO-1061

Conversation

@marcelogorutuba
Copy link
Copy Markdown
Member

@marcelogorutuba marcelogorutuba commented May 11, 2026

Summary

  • Close privilege escalation via delegation: enforce_role_scope! and bulk_update_permissions now apply their guards to all non-super-admin callers, not only account_owner_only? — blocking the cross-tenant escalation vector via delegated roles (H1 residual)
  • Force type='account' on create for all non-super-admin callers regardless of delegation (M4/H1 class)
  • Block name changes on system roles via update API, not only key changes (M3)
  • Reject role names that produce a blank key with a user-friendly validation error (M2)

Validation

  • ruby -c app/controllers/api/v1/roles_controller.rb → Syntax OK

Changed Files

  • app/controllers/api/v1/roles_controller.rb

Related PRs

Linked Issue

  • EVO-1061

- Extend ResourceActionsConfig with create/update/delete/bulk_update_permissions actions for roles resource
- Grant account_owner roles.create/update/delete/bulk_update_permissions (removed from exclusive list)
- Implement index/show/create/update/destroy/bulk_update_permissions controller actions
- Scope account_owner to type='account' roles only; 403 on user-type role mutations
- Add account_user_roles collection route and bulk_update_permissions member route

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 11, 2026

Reviewer's Guide

Implements full CRUD and bulk permission update APIs for roles with account-owner scoping, wires them into routing and authorization, and extends the RBAC configuration to expose the new role actions while removing those permissions from the account_owner seed role.

Sequence diagram for role show with account_owner scoping

sequenceDiagram
  actor AccountOwner
  participant RolesController as ApiV1RolesController
  participant Role

  AccountOwner->>RolesController: show(id)
  activate RolesController
  RolesController->>RolesController: check_authorization
  RolesController->>RolesController: authorize_resource!(roles, read)
  RolesController->>Role: find(id)
  Role-->>RolesController: role
  RolesController->>RolesController: enforce_role_scope!
  alt [account_owner_only? and role.type != account]
    RolesController-->>AccountOwner: 403 Cannot access or modify user-type roles
  else [allowed]
    RolesController-->>AccountOwner: success_response(role_serializer(role))
  end
  deactivate RolesController
Loading

Sequence diagram for roles bulk_update_permissions

sequenceDiagram
  actor AccountOwner
  participant RolesController as ApiV1RolesController
  participant ResourceActionsConfig
  participant Role
  participant RolePermissionsAction as RolePermissionsActions

  AccountOwner->>RolesController: bulk_update_permissions(id, permission_keys)
  activate RolesController
  RolesController->>RolesController: check_authorization
  RolesController->>RolesController: authorize_resource!(roles, bulk_update_permissions)
  RolesController->>Role: find(id)
  Role-->>RolesController: role
  RolesController->>RolesController: enforce_role_scope!
  alt [permission_keys is not Array]
    RolesController-->>AccountOwner: error_response(VALIDATION_ERROR)
  else [permission_keys is Array]
    loop each permission_key
      RolesController->>ResourceActionsConfig: valid_permission?(permission_key)
      ResourceActionsConfig-->>RolesController: true or false
    end
    alt [any invalid_keys]
      RolesController-->>AccountOwner: error_response(VALIDATION_ERROR)
    else [all valid]
      RolesController->>RolePermissionsAction: transaction destroy_all
      RolesController->>RolePermissionsAction: create!(permission_key) * valid_keys
      RolesController->>Role: reload
      Role-->>RolesController: role
      RolesController-->>AccountOwner: success_response(role_serializer(role))
    end
  end
  deactivate RolesController
Loading

File-Level Changes

Change Details Files
Add full CRUD and bulk permission update actions to RolesController with account-owner scoping and authorization mapping.
  • Introduce index, show, create, update, destroy, and bulk_update_permissions actions that return serialized role data and standardized success/error responses.
  • Enforce scoping so account_owner (without super_admin) can only see and mutate roles of type 'account', including index filtering and per-role access checks.
  • Add strong params, record loading with graceful 404 handling, and transactionally replace role_permissions_actions when bulk updating permissions.
  • Expand the authorization map so each roles action, including bulk_update_permissions and account_user_roles, is protected by the appropriate roles.* permission key while keeping the existing full endpoint behavior.
app/controllers/api/v1/roles_controller.rb
Extend roles resource action definitions to cover full CRUD and bulk permission updates.
  • Add create, update, delete, and bulk_update_permissions entries to the roles resource actions with appropriate display names and descriptions.
  • Keep existing read and bulk_assign actions intact so permission checks can rely on a unified configuration.
app/models/resource_actions_config.rb
Expose new routes for roles CRUD and bulk permission update, including account_user_roles collection and bulk_update_permissions member routes.
  • Add account_user_roles as a collection route under roles to expose assignable account user roles via GET.
  • Add bulk_update_permissions as a member PUT route on roles so clients can update a single role's permission tree in bulk.
config/routes.rb
Adjust RBAC seeding so account_owner no longer has explicit roles management permissions by default.
  • Remove roles.create, roles.update, roles.delete, and roles.bulk_update_permissions from the account_owner seed permission list, so those are now granted via the new configuration/flows instead of being hard-coded.
  • Retain other roles-related and feature permissions to preserve existing behavior outside of direct role management.
db/seeds/rbac.rb

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In create, the generated key from name doesn’t account for collisions with existing role keys or reserved/system keys; consider validating and surfacing a clearer error if the derived key is already taken or invalid.
  • enforce_role_scope! returns a raw JSON error payload instead of using error_response, which is used elsewhere in the controller; aligning on a single error-response helper would keep API responses consistent.
  • The scoped_roles/account_owner_only? logic is duplicated conceptually in multiple places (e.g., index, enforce_role_scope!); consider centralizing role-type scoping into a single helper or model scope to reduce drift and make the behavior easier to reason about.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `create`, the generated `key` from `name` doesn’t account for collisions with existing role keys or reserved/system keys; consider validating and surfacing a clearer error if the derived key is already taken or invalid.
- `enforce_role_scope!` returns a raw JSON error payload instead of using `error_response`, which is used elsewhere in the controller; aligning on a single error-response helper would keep API responses consistent.
- The `scoped_roles`/`account_owner_only?` logic is duplicated conceptually in multiple places (e.g., `index`, `enforce_role_scope!`); consider centralizing role-type scoping into a single helper or model scope to reduce drift and make the behavior easier to reason about.

## Individual Comments

### Comment 1
<location path="app/controllers/api/v1/roles_controller.rb" line_range="142-148" />
<code_context>
+    current_api_user.has_role?('account_owner') && !current_api_user.has_role?('super_admin')
+  end
+
+  def enforce_role_scope!
+    return unless @role
+    return unless account_owner_only?
+    return if @role.type == 'account'
+
+    render json: { error: 'Cannot access or modify user-type roles' }, status: :forbidden
+  end
+
</code_context>
<issue_to_address>
**suggestion:** Align the forbidden response with the other controller error helpers for consistency.

Other controller actions (like `destroy`/`update`) use `error_response`/`render_unprocessable_entity`, which likely wrap errors in a consistent envelope (`code`, `message`, etc.). This path returns a raw `{ error: ... }` hash, which changes the response shape and may force clients to special-case it. Consider calling something like `error_response('FORBIDDEN', 'Cannot access or modify user-type roles', status: :forbidden)` here to keep error responses consistent.

```suggestion
  def enforce_role_scope!
    return unless @role
    return unless account_owner_only?
    return if @role.type == 'account'

    error_response('FORBIDDEN', 'Cannot access or modify user-type roles', status: :forbidden)
  end
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/controllers/api/v1/roles_controller.rb
marcelogorutuba and others added 2 commits May 13, 2026 16:09
- Prevent privilege escalation: intersect bulk_update_permissions keys
  with caller's own permissions when account_owner_only?
- Add rbac:reseed_account_owner rake task for idempotent re-seed (M1)
- Fix N+1: use .size and .map(&:permission_key) in role_helper,
  role.rb#permissions_by_resource, permission_keys, can_be_deleted? (M2)
- Unify forbidden error shape in enforce_role_scope! via error_response (M3)
- Remove duplicate private callbacks in role.rb

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ler (EVO-1061)

- enforce_role_scope! now blocks all non-super-admin callers on user-type
  roles instead of only account_owner_only? callers, closing the
  delegation escalation vector (H1 residual)
- bulk_update_permissions intersection guard now applies to every
  non-super-admin caller, not only account_owner_only? (H1 residual)
- create forces type='account' for all non-super-admin callers regardless
  of role delegation (M4/H1 class)
- update blocks name changes on system roles via API, not only key (M3)
- create rejects names that produce a blank key with a friendly error (M2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dpaes dpaes merged commit eb1a006 into develop May 14, 2026
1 check passed
@dpaes dpaes deleted the fix/EVO-1061 branch May 14, 2026 17:09
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