diff --git a/app/controllers/api/v1/roles_controller.rb b/app/controllers/api/v1/roles_controller.rb index 15c88d7..980d4f1 100644 --- a/app/controllers/api/v1/roles_controller.rb +++ b/app/controllers/api/v1/roles_controller.rb @@ -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 @@ -45,9 +138,56 @@ 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 + + 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) @@ -55,4 +195,4 @@ def apply_role_filters @roles = system_assignable_roles.or(@roles.where(system: false)) end end -end \ No newline at end of file +end diff --git a/app/controllers/concerns/role_helper.rb b/app/controllers/concerns/role_helper.rb index 3da32eb..a9baad8 100644 --- a/app/controllers/concerns/role_helper.rb +++ b/app/controllers/concerns/role_helper.rb @@ -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 } diff --git a/app/models/resource_actions_config.rb b/app/models/resource_actions_config.rb index e540332..aba3882 100644 --- a/app/models/resource_actions_config.rb +++ b/app/models/resource_actions_config.rb @@ -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' } } }, diff --git a/app/models/role.rb b/app/models/role.rb index 56a0d4e..33cb44e 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -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 @@ -126,27 +126,15 @@ def has_permission?(permission_key) # Lista todas as permissões da role no formato 'recurso.ação' # @return [Array] 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 \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e0cb489..677f438 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -149,6 +149,10 @@ resources :roles do collection do get :full + get :account_user_roles + end + member do + put :bulk_update_permissions end end diff --git a/db/seeds/rbac.rb b/db/seeds/rbac.rb index fa1043a..07e7be6 100644 --- a/db/seeds/rbac.rb +++ b/db/seeds/rbac.rb @@ -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', diff --git a/lib/tasks/rbac.rake b/lib/tasks/rbac.rake index f6ac2f2..66cfd6a 100644 --- a/lib/tasks/rbac.rake +++ b/lib/tasks/rbac.rake @@ -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..."