Use Rolify for admin and moderator roles

This commit is contained in:
Georg Gadinger 2020-04-19 22:35:58 +02:00
parent a0c9641994
commit 946bb3ae9d
21 changed files with 246 additions and 55 deletions

View File

@ -42,6 +42,8 @@ gem 'tiny-color-rails'
gem 'jquery-minicolors-rails' gem 'jquery-minicolors-rails'
gem 'colorize' gem 'colorize'
gem "rolify", "~> 5.2"
source "https://rails-assets.org" do source "https://rails-assets.org" do
gem 'rails-assets-growl' gem 'rails-assets-growl'
gem 'rails-assets-jquery', '~> 2.2.0' gem 'rails-assets-jquery', '~> 2.2.0'

View File

@ -382,6 +382,7 @@ GEM
responders (3.0.0) responders (3.0.0)
actionpack (>= 5.0) actionpack (>= 5.0)
railties (>= 5.0) railties (>= 5.0)
rolify (5.2.0)
rspec-core (3.9.1) rspec-core (3.9.1)
rspec-support (~> 3.9.1) rspec-support (~> 3.9.1)
rspec-expectations (3.9.1) rspec-expectations (3.9.1)
@ -554,6 +555,7 @@ DEPENDENCIES
rake rake
redcarpet redcarpet
redis redis
rolify (~> 5.2)
rspec-rails (~> 3.9) rspec-rails (~> 3.9)
ruby-progressbar ruby-progressbar
sanitize sanitize

View File

@ -105,44 +105,48 @@ namespace :justask do
end end
end end
desc "Gives admin status to an user." desc "Gives admin status to a user."
task :admin, [:screen_name] => :environment do |t, args| task :admin, [:screen_name] => :environment do |t, args|
fail "screen name required" if args[:screen_name].nil? abort "screen name required" if args[:screen_name].nil?
user = User.find_by_screen_name(args[:screen_name]) user = User.find_by_screen_name(args[:screen_name])
fail "user #{args[:screen_name]} not found" if user.nil? abort "user #{args[:screen_name]} not found" if user.nil?
user.admin = true
user.save! user.add_role :administrator
puts "#{user.screen_name} is now an admin." puts "#{user.screen_name} is now an administrator."
end end
desc "Removes admin status from an user." desc "Removes admin status from a user."
task :deadmin, [:screen_name] => :environment do |t, args| task :deadmin, [:screen_name] => :environment do |t, args|
fail "screen name required" if args[:screen_name].nil? abort "screen name required" if args[:screen_name].nil?
user = User.find_by_screen_name(args[:screen_name]) user = User.find_by_screen_name(args[:screen_name])
fail "user #{args[:screen_name]} not found" if user.nil? abort "user #{args[:screen_name]} not found" if user.nil?
user.admin = false
user.save! user.remove_role :administrator
puts "#{user.screen_name} is no longer an admin." puts "#{user.screen_name} is no longer an administrator."
end end
desc "Gives moderator status to an user." desc "Gives moderator status to a user."
task :mod, [:screen_name] => :environment do |t, args| task :mod, [:screen_name] => :environment do |t, args|
fail "screen name required" if args[:screen_name].nil? abort "screen name required" if args[:screen_name].nil?
user = User.find_by_screen_name(args[:screen_name]) user = User.find_by_screen_name(args[:screen_name])
fail "user #{args[:screen_name]} not found" if user.nil? abort "user #{args[:screen_name]} not found" if user.nil?
user.moderator = true
user.save! user.add_role :moderator
puts "#{user.screen_name} is now an moderator." puts "#{user.screen_name} is now a moderator."
end end
desc "Removes moderator status from an user." desc "Removes moderator status from a user."
task :demod, [:screen_name] => :environment do |t, args| task :demod, [:screen_name] => :environment do |t, args|
fail "screen name required" if args[:screen_name].nil? abort "screen name required" if args[:screen_name].nil?
user = User.find_by_screen_name(args[:screen_name]) user = User.find_by_screen_name(args[:screen_name])
fail "user #{args[:screen_name]} not found" if user.nil? abort "user #{args[:screen_name]} not found" if user.nil?
user.moderator = false
user.save! user.remove_role :moderator
puts "#{user.screen_name} is no longer an moderator." puts "#{user.screen_name} is no longer a moderator."
end end
desc "Hits an user with the banhammer." desc "Hits an user with the banhammer."

View File

@ -125,9 +125,9 @@ class Ajax::ModerationController < ApplicationController
unban = params[:ban] == "0" unban = params[:ban] == "0"
perma = params[:permaban] == "1" perma = params[:permaban] == "1"
buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban or perma buntil = DateTime.strptime params[:until], "%m/%d/%Y %I:%M %p" unless unban || perma
if not unban and target.admin? if !unban && target.has_role?(:administrator)
@status = :nopriv @status = :nopriv
@message = I18n.t('messages.moderation.ban.nopriv') @message = I18n.t('messages.moderation.ban.nopriv')
@success = false @success = false
@ -166,7 +166,7 @@ class Ajax::ModerationController < ApplicationController
@message = I18n.t('messages.moderation.privilege.nope') @message = I18n.t('messages.moderation.privilege.nope')
return unless %w(blogger supporter moderator admin contributor translator).include? params[:type].downcase return unless %w(blogger supporter moderator admin contributor translator).include? params[:type].downcase
if %w(supporter moderator admin).include?(params[:type].downcase) and !current_user.admin? if %w(supporter moderator admin).include?(params[:type].downcase) && !current_user.has_role?(:administrator)
@status = :nopriv @status = :nopriv
@message = I18n.t('messages.moderation.privilege.nopriv') @message = I18n.t('messages.moderation.privilege.nopriv')
@success = false @success = false
@ -174,7 +174,9 @@ class Ajax::ModerationController < ApplicationController
end end
@checked = status @checked = status
case params[:type].downcase type = params[:type].downcase
target_role = {"admin" => "administrator"}.fetch(type, type).to_sym
case type
when 'blogger' when 'blogger'
target_user.blogger = status target_user.blogger = status
when 'contributor' when 'contributor'
@ -183,10 +185,12 @@ class Ajax::ModerationController < ApplicationController
target_user.translator = status target_user.translator = status
when 'supporter' when 'supporter'
target_user.supporter = status target_user.supporter = status
when 'moderator' when 'moderator', 'admin'
target_user.moderator = status if status
when 'admin' target_user.add_role target_role
target_user.admin = status else
target_user.remove_role target_role
end
end end
target_user.save! target_user.save!

15
app/models/role.rb Normal file
View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class Role < ApplicationRecord
has_and_belongs_to_many :users, join_table: :users_roles
belongs_to :resource,
polymorphic: true,
optional: true
validates :resource_type,
inclusion: { in: Rolify.resource_types },
allow_nil: true
scopify
end

View File

@ -5,6 +5,8 @@ class User < ApplicationRecord
:recoverable, :rememberable, :trackable, :recoverable, :rememberable, :trackable,
:validatable, :confirmable, :authentication_keys => [:login] :validatable, :confirmable, :authentication_keys => [:login]
rolify
# attr_accessor :login # attr_accessor :login
has_many :questions, dependent: :destroy has_many :questions, dependent: :destroy
@ -183,7 +185,7 @@ class User < ApplicationRecord
# @return [Boolean] is the user a moderator? # @return [Boolean] is the user a moderator?
def mod? def mod?
self.moderator? || self.admin? has_role?(:moderator) || has_role?(:administrator)
end end
# region stuff used for reporting/moderation # region stuff used for reporting/moderation
@ -258,4 +260,10 @@ class User < ApplicationRecord
end end
!self.export_processing !self.export_processing
end end
# %w[admin moderator].each do |m|
# define_method(m) { raise "not allowed: #{m}" }
# define_method(m+??) { raise "not allowed: #{m}?"}
# define_method(m+?=) { |*a| raise "not allowed: #{m}="}
# end
end end

View File

@ -15,7 +15,7 @@
%i.fa.fa-fw.fa-cog %i.fa.fa-fw.fa-cog
= t('views.navigation.settings') = t('views.navigation.settings')
%li.divider %li.divider
- if current_user.admin? - if current_user.has_role?(:administrator)
%li %li
%a{href: rails_admin_path} %a{href: rails_admin_path}
%i.fa.fa-fw.fa-cogs %i.fa.fa-fw.fa-cogs

View File

@ -33,7 +33,7 @@
%a{href: '#', data: { target: "#modal-privileges", toggle: :modal }} %a{href: '#', data: { target: "#modal-privileges", toggle: :modal }}
%i.fa.fa-wrench %i.fa.fa-wrench
= raw t('views.actions.privilege', user: user.screen_name) = raw t('views.actions.privilege', user: user.screen_name)
- unless user.admin? - unless user.has_role?(:administrator)
%li %li
%a{href: '#', data: { target: "#modal-ban", toggle: :modal }} %a{href: '#', data: { target: "#modal-ban", toggle: :modal }}
%i.fa.fa-ban %i.fa.fa-ban

View File

@ -11,7 +11,7 @@
= render 'user/modal_privileges_item', privilege: 'blogger', description: t('views.modal.privilege.blogger'), user: @user = render 'user/modal_privileges_item', privilege: 'blogger', description: t('views.modal.privilege.blogger'), user: @user
= render 'user/modal_privileges_item', privilege: 'contributor', description: t('views.modal.privilege.contributor'), user: @user = render 'user/modal_privileges_item', privilege: 'contributor', description: t('views.modal.privilege.contributor'), user: @user
= render 'user/modal_privileges_item', privilege: 'translator', description: t('views.modal.privilege.translator'), user: @user = render 'user/modal_privileges_item', privilege: 'translator', description: t('views.modal.privilege.translator'), user: @user
- if current_user.admin? - if current_user.has_role?(:administrator)
= render 'user/modal_privileges_item', privilege: 'supporter', description: t('views.modal.privilege.supporter'), user: @user = render 'user/modal_privileges_item', privilege: 'supporter', description: t('views.modal.privilege.supporter'), user: @user
= render 'user/modal_privileges_item', privilege: 'moderator', description: t('views.modal.privilege.moderator'),user: @user = render 'user/modal_privileges_item', privilege: 'moderator', description: t('views.modal.privilege.moderator'),user: @user
= render 'user/modal_privileges_item', privilege: 'admin', description: t('views.modal.privilege.admin'), user: @user = render 'user/modal_privileges_item', privilege: 'admin', description: t('views.modal.privilege.admin'), user: @user

View File

@ -1,8 +1,11 @@
- description ||= '' - description ||= ''
- role_mapping = {"admin" => "administrator"}
- requires_role = %w[admin moderator].include?(privilege)
- checked = requires_role ? user.has_role?(role_mapping.fetch(privilege, privilege).to_sym) : user.public_send("#{privilege}?")
%li.list-group-item{id: "privilege-#{privilege}"} %li.list-group-item{id: "privilege-#{privilege}"}
.media .media
.pull-left.j2-table .pull-left.j2-table
%input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: user.send("#{privilege}?"), autocomplete: 'off'} %input.input--center{type: :checkbox, name: 'check-your-privileges', data: { type: privilege, user: user.screen_name }, checked: checked, autocomplete: 'off'}
.media-body .media-body
.list-group-item-heading= privilege.capitalize .list-group-item-heading= privilege.capitalize
- unless description.blank? - unless description.blank?

View File

@ -1,11 +1,11 @@
.panel.panel-default#profile .panel.panel-default#profile
%img.profile--avatar{src: @user.profile_picture.url(:large)} %img.profile--avatar{src: @user.profile_picture.url(:large)}
- if user_signed_in? && current_user.admin? - if user_signed_in? && current_user.has_role?(:administrator)
- if @user.admin? - if @user.has_role?(:administrator)
.profile--panel-badge.panel-badge-danger .profile--panel-badge.panel-badge-danger
%i.fa.fa-flask %i.fa.fa-flask
= t 'views.user.title.admin' = t 'views.user.title.admin'
- if @user.moderator? - if @user.has_role?(:moderator)
.profile--panel-badge.panel-badge-success .profile--panel-badge.panel-badge-success
%i.fa.fa-users %i.fa.fa-users
= t 'views.user.title.moderator' = t 'views.user.title.moderator'

View File

@ -95,7 +95,7 @@
%p.data-heading Admin %p.data-heading Admin
%p %p
- if current_user.admin? - if current_user.has_role?(:administrator)
%span.label.label-success %span.label.label-success
%i.fa.fa-fw.fa-check %i.fa.fa-fw.fa-check
- else - else

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# workaround to get pagination right # workaround to get pagination right
if defined? WillPaginate if defined? WillPaginate
Kaminari.configure do |config| Kaminari.configure do |config|
@ -6,12 +8,11 @@ if defined? WillPaginate
end end
RailsAdmin.config do |config| RailsAdmin.config do |config|
config.main_app_name = ['justask', 'Kontrollzentrum'] config.main_app_name = ['justask', 'Kontrollzentrum']
## == Authentication == ## == Authentication ==
config.authenticate_with do config.authenticate_with do
redirect_to main_app.root_path unless current_user.try :admin? redirect_to main_app.root_path unless current_user&.has_role?(:administrator)
end end
config.current_user_method(&:current_user) config.current_user_method(&:current_user)

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
Rolify.configure do |config|
# By default ORM adapter is ActiveRecord. uncomment to use mongoid
# config.use_mongoid
# Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false
# config.use_dynamic_shortcuts
# Configuration to remove roles from database once the last resource is removed. Default is: true
config.remove_role_if_empty = false
end

View File

@ -2,19 +2,19 @@ require 'sidekiq/web'
Rails.application.routes.draw do Rails.application.routes.draw do
start = Time.now start = Time.now
# Admin panel
mount RailsAdmin::Engine => '/justask_admin', as: 'rails_admin'
# Sidekiq # Sidekiq
constraints ->(req) { req.env["warden"].authenticate?(scope: :user) && constraints ->(req) { req.env["warden"].authenticate?(scope: :user) &&
req.env['warden'].user.admin? } do req.env["warden"].user.has_role?(:administrator) } do
# Admin panel
mount RailsAdmin::Engine => "/justask_admin", as: "rails_admin"
mount Sidekiq::Web, at: "/sidekiq" mount Sidekiq::Web, at: "/sidekiq"
mount PgHero::Engine, at: "/pghero", as: 'pghero' mount PgHero::Engine, at: "/pghero", as: "pghero"
end end
# Moderation panel # Moderation panel
constraints ->(req) { req.env['warden'].authenticate?(scope: :user) && constraints ->(req) { req.env["warden"].authenticate?(scope: :user) &&
(req.env['warden'].user.mod?) } do req.env["warden"].user.mod? } do
match '/moderation/priority(/:user_id)', to: 'moderation#priority', via: :get, as: :moderation_priority match '/moderation/priority(/:user_id)', to: 'moderation#priority', via: :get, as: :moderation_priority
match '/moderation/ip/:user_id', to: 'moderation#ip', via: :get, as: :moderation_ip match '/moderation/ip/:user_id', to: 'moderation#ip', via: :get, as: :moderation_ip
match '/moderation(/:type)', to: 'moderation#index', via: :get, as: :moderation, defaults: {type: 'all'} match '/moderation(/:type)', to: 'moderation#index', via: :get, as: :moderation, defaults: {type: 'all'}

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class RolifyCreateRoles < ActiveRecord::Migration[5.2]
def change
create_table(:roles) do |t|
t.string :name
t.references :resource, polymorphic: true
t.timestamps
end
create_table(:users_roles, id: false) do |t|
t.references :user
t.references :role
end
add_index(:roles, %i[name resource_type resource_id])
add_index(:users_roles, %i[user_id role_id])
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class CreateInitialRoles < ActiveRecord::Migration[5.2]
def up
%w[Administrator Moderator].each do |role|
Role.where(name: role.parameterize).first_or_create
end
{
admin: :administrator,
moderator: :moderator
}.each do |legacy_role, new_role|
User.where(legacy_role => true).each do |u|
puts "-- migrating #{u.screen_name} (#{u.id}) from field:#{legacy_role} to role:#{new_role}"
u.add_role new_role
u.public_send("#{legacy_role}=", false)
u.save!
end
end
end
def down
{
administrator: :admin,
moderator: :moderator
}.each do |new_role, legacy_role|
User.with_role(new_role).each do |u|
puts "-- migrating #{u.screen_name} (#{u.id}) from role:#{new_role} to field:#{legacy_role}"
u.public_send("#{legacy_role}=", true)
u.save!
end
end
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2016_01_05_165913) do ActiveRecord::Schema.define(version: 2020_04_19_185535) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -137,6 +137,16 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do
t.string "reason" t.string "reason"
end end
create_table "roles", force: :cascade do |t|
t.string "name"
t.string "resource_type"
t.bigint "resource_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"
t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id"
end
create_table "services", id: :serial, force: :cascade do |t| create_table "services", id: :serial, force: :cascade do |t|
t.string "type", null: false t.string "type", null: false
t.integer "user_id", null: false t.integer "user_id", null: false
@ -270,4 +280,12 @@ ActiveRecord::Schema.define(version: 2016_01_05_165913) do
t.index ["screen_name"], name: "index_users_on_screen_name", unique: true t.index ["screen_name"], name: "index_users_on_screen_name", unique: true
end end
create_table "users_roles", id: false, force: :cascade do |t|
t.bigint "user_id"
t.bigint "role_id"
t.index ["role_id"], name: "index_users_roles_on_role_id"
t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
t.index ["user_id"], name: "index_users_roles_on_user_id"
end
end end

View File

@ -5,3 +5,7 @@
# #
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first) # Mayor.create(name: 'Emanuel', city: cities.first)
%w[Administrator Moderator].each do |role|
Role.where(name: role.parameterize).first_or_create
end

View File

@ -1,9 +1,13 @@
# frozen_string_literal: true
require 'json' require 'json'
require 'yaml' require 'yaml'
require 'httparty' require 'httparty'
require 'securerandom' require 'securerandom'
class Exporter class Exporter
EXPORT_ROLES = [:administrator, :moderator].freeze
def initialize(user) def initialize(user)
@user = user @user = user
@obj = {} @obj = {}
@ -30,10 +34,10 @@ class Exporter
private private
def collect_user_info def collect_user_info
%i(admin answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count %i(answered_count asked_count ban_reason banned_until bio blogger comment_smiled_count commented_count
confirmation_sent_at confirmed_at contributor created_at crop_h crop_h_h crop_h_w crop_h_x crop_h_y confirmation_sent_at confirmed_at contributor created_at crop_h crop_h_h crop_h_w crop_h_x crop_h_y
crop_w crop_x crop_y current_sign_in_at current_sign_in_ip display_name email follower_count friend_count crop_w crop_x crop_y current_sign_in_at current_sign_in_ip display_name email follower_count friend_count
id last_sign_in_at last_sign_in_ip locale location moderator motivation_header permanently_banned id last_sign_in_at last_sign_in_ip locale location motivation_header permanently_banned
privacy_allow_anonymous_questions privacy_allow_public_timeline privacy_allow_stranger_answers privacy_allow_anonymous_questions privacy_allow_public_timeline privacy_allow_stranger_answers
privacy_show_in_search profile_header_content_type profile_header_file_name profile_header_file_size privacy_show_in_search profile_header_content_type profile_header_file_name profile_header_file_size
profile_header_updated_at profile_picture_content_type profile_picture_file_name profile_picture_file_size profile_header_updated_at profile_picture_content_type profile_picture_file_name profile_picture_file_size
@ -41,6 +45,10 @@ class Exporter
updated_at website).each do |f| updated_at website).each do |f|
@obj[f] = @user.send f @obj[f] = @user.send f
end end
EXPORT_ROLES.each do |role|
@obj[role] = @user.has_role?(role)
end
end end
def collect_questions def collect_questions
@ -221,11 +229,16 @@ class Exporter
def user_stub(user) def user_stub(user)
uobj = {} uobj = {}
%i(admin answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at %i(answered_count asked_count bio blogger comment_smiled_count commented_count contributor created_at
display_name follower_count friend_count id location moderator motivation_header permanently_banned screen_name display_name follower_count friend_count id location motivation_header permanently_banned screen_name
smiled_count supporter translator website).each do |f| smiled_count supporter translator website).each do |f|
uobj[f] = user.send f uobj[f] = user.send f
end end
EXPORT_ROLES.each do |role|
uobj[role] = user.has_role?(role)
end
uobj uobj
end end
end end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'role-constrained routes', type: :request do
shared_examples_for 'fails to access route' do
it 'fails to access route' do
# 404 = no user found -- we have a fallback route if something could not be matched
expect(subject).to eq 404
end
end
shared_examples_for 'routes for' do |roles, subject_block, skip_reason: nil|
before { skip(skip_reason) } if skip_reason
subject(&subject_block)
context 'not signed in' do
include_examples 'fails to access route'
end
roles.each do |role|
context "signed in user without #{role} role" do
let(:user) { FactoryBot.create(:user, password: 'test1234') }
before(:each) do
post '/sign_in', params: { user: { login: user.email, password: user.password } }
end
include_examples 'fails to access route'
end
context "signed in user with #{role} role" do
let(:user) { FactoryBot.create(:user, password: 'test1234', roles: [role]) }
before(:each) do
post '/sign_in', params: { user: { login: user.email, password: user.password } }
end
it 'can access route' do
expect(subject).to be_in 200..299
end
end
end
end
it_behaves_like('routes for', [:administrator], -> { get('/justask_admin') })
it_behaves_like('routes for', [:administrator], -> { get('/sidekiq') })
it_behaves_like('routes for', [:administrator], -> { get('/pghero') }, skip_reason: 'PG::InFailedSqlTransaction due to 5.1 upgrade, works fine outside specs though')
it_behaves_like('routes for', %i[administrator moderator], -> { get('/moderation') })
end