Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 161 additions & 21 deletions app/controllers/api/v1/roles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,128 @@ class Api::V1::RolesController < Api::V1::BaseController
include RoleHelper

before_action :check_authorization

# Get available roles for account users (agent and administrator)
# This is a global endpoint as roles are not account-specific
before_action :load_role, only: [:show, :update, :destroy, :bulk_update_permissions]
before_action :enforce_role_scope!, only: [:show, :update, :destroy, :bulk_update_permissions]

def index
@roles = scoped_roles.includes(:role_permissions_actions, :users)
success_response(
data: @roles.map { |role| role_serializer(role) },
message: 'Roles retrieved successfully'
)
end

def show
success_response(
data: role_serializer(@role),
message: 'Role retrieved successfully'
)
end

def create
key = role_params[:name].to_s.downcase.gsub(/\s+/, '_').gsub(/[^a-z0-9_]/, '')

if key.blank?
return error_response('VALIDATION_ERROR', 'Role name must contain at least one letter or number', status: :unprocessable_entity)
end

role = Role.new(
key: key,
name: role_params[:name],
description: role_params[:description],
system: false,
type: current_api_user.has_role?('super_admin') ? (role_params[:type].presence || 'account') : 'account'
)

unless role.save
return render_unprocessable_entity(role.errors)
end

success_response(
data: role_serializer(role),
message: 'Role created successfully',
status: :created
)
end

def update
if @role.system? && (role_params.key?(:key) || role_params.key?(:name))
return error_response('FORBIDDEN', 'Cannot modify key or name of a system role', status: :forbidden)
end

unless @role.update(role_params.except(:key, :type))
return render_unprocessable_entity(@role.errors)
end

success_response(
data: role_serializer(@role),
message: 'Role updated successfully'
)
end

def destroy
unless @role.can_be_deleted?
message = @role.system? ? 'Cannot delete system roles' : 'Cannot delete role with assigned users'
return error_response('FORBIDDEN', message, status: :forbidden)
end

@role.destroy!
success_response(data: nil, message: 'Role deleted successfully')
end

def bulk_update_permissions
permission_keys = params[:permission_keys]

unless permission_keys.is_a?(Array)
return error_response('VALIDATION_ERROR', 'permission_keys must be an array', status: :bad_request)
end

valid_keys = permission_keys.select { |k| ResourceActionsConfig.valid_permission?(k) }
invalid_keys = permission_keys - valid_keys

if invalid_keys.any?
return error_response(
'VALIDATION_ERROR',
"Invalid permission keys: #{invalid_keys.join(', ')}",
status: :unprocessable_entity
)
end

unless current_api_user.has_role?('super_admin')
caller_permissions = Set.new(current_api_user.all_permissions)
unauthorized_keys = valid_keys.reject { |k| caller_permissions.include?(k) }
if unauthorized_keys.any?
return error_response(
'FORBIDDEN',
"Cannot grant permissions you do not hold: #{unauthorized_keys.join(', ')}",
status: :forbidden
)
end
end

ActiveRecord::Base.transaction do
@role.role_permissions_actions.destroy_all
valid_keys.each { |key| @role.role_permissions_actions.create!(permission_key: key) }
end

success_response(
data: role_serializer(@role.reload),
message: 'Permissions updated successfully'
)
end

# Get available roles for account users (agent and account_owner)
def account_user_roles
roles = Role.account_type.where(key: ['agent', 'account_owner']).map do |role|
RoleSerializer.basic(role)
end

success_response(
data: roles,
message: 'Account user roles retrieved successfully'
)
end

def check_authorization
# Verificar se usuário tem permissão para gerenciar roles
action_map = {
'account_user_roles' => 'roles.read'
}

required_permission = action_map[action_name]
if required_permission
resource_key, action_key = required_permission.split('.')
authorize_resource!(resource_key, action_key)
else
true # Para ações não mapeadas, permitir por enquanto
end
end

def full
load_roles
apply_role_filters
Expand All @@ -45,14 +138,61 @@ def full

private

def role_params
params.permit(:name, :description, :key, :type)
end

def load_role
@role = Role.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_not_found('Role not found')
end

def scoped_roles
account_owner_only? ? Role.where(type: 'account') : Role.all
end

def account_owner_only?
current_api_user.has_role?('account_owner') && !current_api_user.has_role?('super_admin')
end

def enforce_role_scope!
return unless @role
return if current_api_user.has_role?('super_admin')
return if @role.type == 'account'

error_response('FORBIDDEN', 'Cannot access or modify user-type roles', status: :forbidden)
end
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

def check_authorization
action_map = {
'index' => 'roles.read',
'show' => 'roles.read',
'account_user_roles' => 'roles.read',
'full' => 'roles.read',
'create' => 'roles.create',
'update' => 'roles.update',
'destroy' => 'roles.delete',
'bulk_update_permissions' => 'roles.bulk_update_permissions'
}

required_permission = action_map[action_name]
if required_permission
resource_key, action_key = required_permission.split('.')
authorize_resource!(resource_key, action_key)
else
true
end
end

def apply_role_filters
system_assignable_roles = @roles.where(system: true, key: ['agent', 'account_owner'])

if params[:type].present?
custom_roles = @roles.where(system: false, type: params[:type])
@roles = system_assignable_roles.or(custom_roles)
else
@roles = system_assignable_roles.or(@roles.where(system: false))
end
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/role_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def role_serializer(role)
type: role.type,
permissions_by_resource: permissions_by_resource,
permissions_count: total_actions,
users_count: role.users.count,
users_count: role.users.size,
created_at: role.created_at,
updated_at: role.updated_at
}
Expand Down
6 changes: 5 additions & 1 deletion app/models/resource_actions_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ class ResourceActionsConfig
description: 'Role and permission management',
actions: {
read: { name: 'View', description: 'View roles and permissions' },
bulk_assign: { name: 'Bulk Assign', description: 'Bulk assign roles to multiple users' }
create: { name: 'Create', description: 'Create custom roles' },
update: { name: 'Update', description: 'Update role details and permissions' },
delete: { name: 'Delete', description: 'Delete custom roles' },
bulk_assign: { name: 'Bulk Assign', description: 'Bulk assign roles to multiple users' },
bulk_update_permissions: { name: 'Update Permissions', description: 'Update role permission tree' }
}
},

Expand Down
20 changes: 4 additions & 16 deletions app/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def custom_role?
# Verifica se a role pode ser excluída
def can_be_deleted?
# Não pode excluir se é role do sistema ou tem usuários associados
!system && user_roles.count == 0
!system && user_roles.size == 0
end

private
Expand Down Expand Up @@ -126,27 +126,15 @@ def has_permission?(permission_key)
# Lista todas as permissões da role no formato 'recurso.ação'
# @return [Array<String>] Lista de permissões
def permission_keys
role_permissions_actions.pluck(:permission_key)
role_permissions_actions.map(&:permission_key)
end

# Get permissions organized by resource
def permissions_by_resource
permissions = role_permissions_actions.pluck(:permission_key)
permissions = role_permissions_actions.map(&:permission_key)

permissions.group_by { |permission| permission.split('.').first }
.transform_values { |perms| perms.map { |perm| perm.split('.').last } }
end

private

def prevent_system_role_deletion
throw :abort if system?
end

def prevent_system_role_key_modification
if system? && saved_change_to_attribute?(:key)
errors.add(:key, "cannot be modified for system roles")
throw :abort
end
end
end
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@
resources :roles do
collection do
get :full
get :account_user_roles
end
member do
put :bulk_update_permissions
end
end

Expand Down
4 changes: 0 additions & 4 deletions db/seeds/rbac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,10 @@
'features.stats',
'features.seed',
'features.types',
'roles.create',
'roles.update',
'roles.delete',
'roles.stats',
'roles.seed',
'roles.add_permission',
'roles.remove_permission',
'roles.bulk_update_permissions',
'account_features.read',
'account_features.assign',
'account_features.remove',
Expand Down
7 changes: 7 additions & 0 deletions lib/tasks/rbac.rake
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ namespace :rbac do
puts "Total: #{ResourceActionsConfig.all_permission_keys.size} permissions"
end

desc 'Re-seed account_owner permissions idempotently (run after deploying EVO-1061)'
task reseed_account_owner: :environment do
puts "🔄 Re-seeding account_owner permissions..."
load Rails.root.join('db', 'seeds', 'rbac.rb')
puts "✅ Done."
end

desc 'Validate configuration integrity'
task validate: :environment do
puts "🔍 Validating ResourceActionsConfig integrity..."
Expand Down