allows super_admin user to remove super_admin role (#19590)

* Skips scheduled posts in featured post count

* Allows removing super_admin role

* adds namespace resolution op

* Adds pundit policy

* Adds rspec for role policy

* Fixes test cases and minor refactoring

* Removes dead code

---------

Co-authored-by: Lawrence <lawrence@forem.com>
This commit is contained in:
Dhurba baral 2023-06-21 21:07:25 +05:45 committed by GitHub
parent f0e99c6279
commit 4e1edc9225
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 92 additions and 70 deletions

View file

@ -90,19 +90,21 @@ module Admin
end
def destroy
role = params[:role].to_sym
role = Role.find(params[:role_id])
authorize(role, :remove_role?)
resource_type = params[:resource_type]
@user = User.find(params[:user_id])
response = ::Users::RemoveRole.call(user: @user,
role: role,
role: role.name,
resource_type: resource_type,
admin: current_user)
if response.success
flash[:success] =
I18n.t("admin.users_controller.role_removed",
role: role.to_s.humanize.titlecase) # TODO: [@yheuhtozr] need better role i18n
role: role.name.to_s.humanize.titlecase) # TODO: [@yheuhtozr] need better role i18n
else
flash[:danger] = response.error_message
end

View file

@ -19,6 +19,12 @@ class Role < ApplicationRecord
workshop_pass
].freeze
ROLES.each do |role|
define_method("#{role}?") do
name == role
end
end
has_and_belongs_to_many :users, join_table: :users_roles # rubocop:disable Rails/HasAndBelongsToMany
belongs_to :resource,

View file

@ -0,0 +1,11 @@
class RolePolicy < ApplicationPolicy
def remove_role?
return false if record.suspended?
if user.super_admin?
true
else
user.admin? && !record.super_admin?
end
end
end

View file

@ -15,7 +15,6 @@ module Users
end
def call
return response if super_admin_role?(role)
return response if user_current_user?(user)
if resource_type && user.remove_role(role, resource_type)
@ -33,13 +32,6 @@ module Users
attr_reader :user, :role, :resource_type, :admin, :response
def super_admin_role?(role)
return false if role != :super_admin
response.error_message = I18n.t("services.users.remove_role.remove_super")
true
end
def user_current_user?(user)
return false if user.id != admin.id

View file

@ -17,9 +17,9 @@
</div>
<% else %>
<ul class="flex flex-wrap gap-2">
<% @user.roles.reject { |role| role.name == "tag_moderator" }.each do |role| %>
<% @user.roles.reject(&:tag_moderator?).each do |role| %>
<li>
<% if role.name == "banned" || role.name == "suspended" || role.name == "super_admin" || @user.id == current_user.id %>
<% if !policy(role).remove_role? || @user.id == current_user.id %>
<button
class="c-pill c-pill--description-icon crayons-tooltip__activator cursor-help"
type="button"
@ -29,7 +29,12 @@
<span data-testid="tooltip" class="crayons-tooltip__content"><%= t("views.admin.users.overview.roles.locked") %></span>
</button>
<% else %>
<%= button_to url_for(action: :destroy, user_id: @user.id, role: role.name.to_sym, resource_type: role.resource_type), method: :delete, data: { confirm: t("views.admin.users.overview.roles.remove_confirm") }, class: "c-pill c-pill--action-icon" do %>
<% confirm_dialog = if role.super_admin?
t("views.admin.users.overview.roles.remove_confirm_super_admin")
else
t("views.admin.users.overview.roles.remove_confirm")
end %>
<%= button_to url_for(action: :destroy, user_id: @user.id, role_id: role.id, resource_type: role.resource_type), method: :delete, data: { confirm: confirm_dialog }, class: "c-pill c-pill--action-icon" do %>
<span class="screen-reader-only"><%= t("views.admin.users.overview.roles.remove") %></span>
<%= t("views.admin.users.overview.roles.name.#{role.name}", default: role.resource_name ? role.resource_name.to_s : role_display_name(role)) %>
<%= crayons_icon_tag(:x, class: "c-pill__action-icon", aria_hidden: true, width: 18, height: 18) %>

View file

@ -9,7 +9,7 @@
</p>
</header>
<% moderated_tags = @user.roles.select { |role| role.name == "tag_moderator" } %>
<% moderated_tags = @user.roles.select(&:tag_moderator?) %>
<% if moderated_tags.any? %>
<ul class="flex flex-wrap gap-2 mb-4">

View file

@ -110,5 +110,4 @@ en:
users:
remove_role:
remove_self: Admins cannot remove roles from themselves.
remove_super: Super Admin roles cannot be removed.
error: There was an issue removing this role. %{e_message}

View file

@ -108,5 +108,4 @@ fr:
users:
remove_role:
remove_self: Les administrateurs ne peuvent pas se retirer des rôles.
remove_super: Les rôles de Super Admin ne peuvent pas être supprimés.
error: Un problème est survenu lors de la suppression de ce rôle. %{e_message}

View file

@ -265,6 +265,7 @@ en:
locked: You can't remove this role.
remove: "Remove role:"
remove_confirm: Are you sure?
remove_confirm_super_admin: You are removing super admin access from this account. Do this carefully and always ensure you retain access to one or more super admin accounts
name:
admin: Admin
codeland_admin: Codeland Admin

View file

@ -265,6 +265,7 @@ fr:
locked: You can't remove this role.
remove: "Remove role:"
remove_confirm: Are you sure?
remove_confirm_super_admin: You are removing super admin access from this account. Do this carefully and always ensure you retain access to one or more super admin accounts
name:
admin: Admin
codeland_admin: Codeland Admin

View file

@ -97,29 +97,6 @@ describe('Manage User Roles', () => {
);
checkUserStatus('Good standing');
});
it('should not remove the Super Admin role', () => {
checkUserStatus('Trusted');
openRolesModal().within(() => {
cy.findByRole('combobox', { name: 'Role' }).select('Super Admin');
cy.findByRole('textbox', { name: 'Add a note to this action:' }).type(
'some reason',
);
cy.findByRole('button', { name: 'Add' }).click();
});
cy.findByRole('button', {
name: `Super Admin You can't remove this role.`,
})
.as('superAdminButton')
.click()
.within(() => {
cy.findByText(`You can't remove this role.`).should('exist');
});
cy.get('@superAdminButton').should('exist');
});
});
describe('Adding Roles', () => {

5
spec/factories/roles.rb Normal file
View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :role do
name { "admin" }
end
end

View file

@ -15,4 +15,25 @@ RSpec.describe Role do
expect(described_class::ROLES).to match_array(expected_roles)
end
end
describe "#super_admin?" do
let(:role) { create(:role, name: role_name) }
let(:role_name) { "super_admin" }
it "has a visible method" do
expect(role.respond_to?(:super_admin?)).to be true
end
it "returns true" do
expect(role.super_admin?).to be true
end
context "with different role" do
let(:role_name) { "admin" }
it "returns false" do
expect(role.super_admin?).to be false
end
end
end
end

View file

@ -0,0 +1,25 @@
require "rails_helper"
describe RolePolicy do
subject(:role) { described_class }
let(:role_suspended) { build(:role, name: "suspended") }
let(:role_trusted) { build(:role, name: "trusted") }
let(:role_super_admin) { build(:role, name: "super_admin") }
let(:super_admin_user) { build(:user, :super_admin) }
let(:admin_user) { build(:user, :admin) }
permissions :remove_role? do
it "denies access if current role is suspended" do
expect(role).not_to permit(super_admin_user, role_suspended)
end
it "grants access if user is super admin" do
expect(role).to permit(super_admin_user, role_trusted)
end
it "denies access if user is admin and role is super_admin" do
expect(role).not_to permit(admin_user, role_super_admin)
end
end
end

View file

@ -1,6 +1,7 @@
require "rails_helper"
RSpec.describe "Admin::Users" do
# rubocop:disable RSpec/IndexedLet
let!(:user) { create(:user, twitter_username: nil, old_username: "username") }
let!(:user2) { create(:user, twitter_username: "Twitter") }
let(:user3) { create(:user) }
@ -174,10 +175,10 @@ RSpec.describe "Admin::Users" do
end
it "removes non-admin roles from non-super_admin users", :aggregate_failures do
user.add_role(:trusted)
role = user.add_role(:trusted)
expect do
delete admin_user_path(user.id), params: { user_id: user.id, role: :trusted }
delete admin_user_path(user.id), params: { user_id: user.id, role_id: role.id }
end.to change(user.roles, :count).by(-1)
expect(user.has_trusted_role?).to be false
@ -185,12 +186,12 @@ RSpec.describe "Admin::Users" do
end
it "removes the correct resource_admin_role from non-super_admin users", :aggregate_failures do
user.add_role(:single_resource_admin, Comment)
role = user.add_role(:single_resource_admin, Comment)
user.add_role(:single_resource_admin, Broadcast)
expect do
delete admin_user_path(user.id),
params: { user_id: user.id, role: :single_resource_admin, resource_type: Comment }
params: { user_id: user.id, role_id: role.id, resource_type: Comment }
end.to change(user.roles, :count).by(-1)
expect(user.single_resource_admin_for?(Comment)).to be false
@ -198,22 +199,11 @@ RSpec.describe "Admin::Users" do
expect(request.flash["success"]).to include("successfully removed from the user!")
end
it "does not allow super_admin roles to be removed", :aggregate_failures do
user.add_role(:super_admin)
expect do
delete admin_user_path(user.id), params: { user_id: user.id, role: :super_admin }
end.not_to change(user.roles, :count)
expect(user.super_admin?).to be true
expect(request.flash["danger"]).to include("cannot be removed.")
end
it "does not allow a admins to remove a role from themselves", :aggregate_failures do
super_admin.add_role(:trusted)
role = super_admin.add_role(:trusted)
expect do
delete admin_user_path(super_admin.id), params: { user_id: super_admin.id, role: :trusted }
delete admin_user_path(super_admin.id), params: { user_id: super_admin.id, role_id: role.id }
end.not_to change(super_admin.roles, :count)
expect(super_admin.trusted?).to be true
@ -308,4 +298,5 @@ RSpec.describe "Admin::Users" do
expect(super_admin.reload.unspent_credits_count).to eq 5
end
end
# rubocop:enable RSpec/IndexedLet
end

View file

@ -3,19 +3,6 @@ require "rails_helper"
RSpec.describe Users::RemoveRole, type: :service do
let(:current_user) { create(:user, :admin) }
context "when user is a super_admin" do
it "does not remove super_admin roles and raises an error", :aggregate_failures do
super_admin = create(:user, :super_admin)
role = super_admin.roles.first.name.to_sym
resource_type = nil
args = { user: super_admin, role: role, resource_type: resource_type, admin: current_user }
role_removal = described_class.call(**args)
expect(role_removal.success).to be false
expect(role_removal.error_message).to eq "Super Admin roles cannot be removed."
end
end
context "when current_user" do
it "does not remove roles and raises an error", :aggregate_failures do
role = current_user.roles.first