Add customizable user roles (#18641)
* Add customizable user roles * Various fixes and improvements * Add migration for old settings and fix tootctl role managementmain
parent
1b4054256f
commit
44b2ee3485
@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SubscriptionsController < BaseController
|
||||
def index
|
||||
authorize :subscription, :index?
|
||||
@subscriptions = ordered_subscriptions.page(requested_page)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ordered_subscriptions
|
||||
Subscription.order(id: :desc).includes(:account)
|
||||
end
|
||||
|
||||
def requested_page
|
||||
params[:page].to_i
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class Users::RolesController < BaseController
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
authorize @user, :change_role?
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @user, :change_role?
|
||||
|
||||
@user.current_account = current_account
|
||||
|
||||
if @user.update(resource_params)
|
||||
redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
|
||||
else
|
||||
render :show
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:user_id])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:user).permit(:role_id)
|
||||
end
|
||||
end
|
||||
end
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class TwoFactorAuthenticationsController < BaseController
|
||||
class Users::TwoFactorAuthenticationsController < BaseController
|
||||
before_action :set_target_user
|
||||
|
||||
def destroy
|
@ -1,17 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::Trends::LinksController < Api::BaseController
|
||||
class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_links
|
||||
|
||||
def index
|
||||
render json: @links, each_serializer: REST::Trends::LinkSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_links
|
||||
@links = Trends.links.query.limit(limit_param(10))
|
||||
def enabled?
|
||||
super || current_user&.can?(:manage_taxonomies)
|
||||
end
|
||||
|
||||
def links_from_trends
|
||||
if current_user&.can?(:manage_taxonomies)
|
||||
Trends.links.query
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::Trends::StatusesController < Api::BaseController
|
||||
class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_statuses
|
||||
|
||||
def index
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_statuses
|
||||
@statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
|
||||
def enabled?
|
||||
super || current_user&.can?(:manage_taxonomies)
|
||||
end
|
||||
|
||||
def statuses_from_trends
|
||||
if current_user&.can?(:manage_taxonomies)
|
||||
Trends.statuses.query
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,17 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||
class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_tags
|
||||
|
||||
def index
|
||||
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tags
|
||||
@tags = Trends.tags.query.limit(limit_param(10))
|
||||
def enabled?
|
||||
super || current_user&.can?(:manage_taxonomies)
|
||||
end
|
||||
|
||||
def tags_from_trends
|
||||
if current_user&.can?(:manage_taxonomies)
|
||||
Trends.tags.query
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,3 @@
|
||||
export const PERMISSION_INVITE_USERS = 0x0000000000010000;
|
||||
export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
|
||||
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;
|
@ -1,68 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module UserRoles
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :admins, -> { where(admin: true) }
|
||||
scope :moderators, -> { where(moderator: true) }
|
||||
scope :staff, -> { admins.or(moderators) }
|
||||
end
|
||||
|
||||
def staff?
|
||||
admin? || moderator?
|
||||
end
|
||||
|
||||
def role=(value)
|
||||
case value
|
||||
when 'admin'
|
||||
self.admin = true
|
||||
self.moderator = false
|
||||
when 'moderator'
|
||||
self.admin = false
|
||||
self.moderator = true
|
||||
else
|
||||
self.admin = false
|
||||
self.moderator = false
|
||||
end
|
||||
end
|
||||
|
||||
def role
|
||||
if admin?
|
||||
'admin'
|
||||
elsif moderator?
|
||||
'moderator'
|
||||
else
|
||||
'user'
|
||||
end
|
||||
end
|
||||
|
||||
def role?(role)
|
||||
case role
|
||||
when 'user'
|
||||
true
|
||||
when 'moderator'
|
||||
staff?
|
||||
when 'admin'
|
||||
admin?
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def promote!
|
||||
if moderator?
|
||||
update!(moderator: false, admin: true)
|
||||
elsif !admin?
|
||||
update!(moderator: true)
|
||||
end
|
||||
end
|
||||
|
||||
def demote!
|
||||
if admin?
|
||||
update!(admin: false, moderator: true)
|
||||
elsif moderator?
|
||||
update!(moderator: false)
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,179 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: user_roles
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# name :string default(""), not null
|
||||
# color :string default(""), not null
|
||||
# position :integer default(0), not null
|
||||
# permissions :bigint(8) default(0), not null
|
||||
# highlighted :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
|
||||
class UserRole < ApplicationRecord
|
||||
FLAGS = {
|
||||
administrator: (1 << 0),
|
||||
view_devops: (1 << 1),
|
||||
view_audit_log: (1 << 2),
|
||||
view_dashboard: (1 << 3),
|
||||
manage_reports: (1 << 4),
|
||||
manage_federation: (1 << 5),
|
||||
manage_settings: (1 << 6),
|
||||
manage_blocks: (1 << 7),
|
||||
manage_taxonomies: (1 << 8),
|
||||
manage_appeals: (1 << 9),
|
||||
manage_users: (1 << 10),
|
||||
manage_invites: (1 << 11),
|
||||
manage_rules: (1 << 12),
|
||||
manage_announcements: (1 << 13),
|
||||
manage_custom_emojis: (1 << 14),
|
||||
manage_webhooks: (1 << 15),
|
||||
invite_users: (1 << 16),
|
||||
manage_roles: (1 << 17),
|
||||
manage_user_access: (1 << 18),
|
||||
delete_user_data: (1 << 19),
|
||||
}.freeze
|
||||
|
||||
module Flags
|
||||
NONE = 0
|
||||
ALL = FLAGS.values.reduce(&:|)
|
||||
|
||||
DEFAULT = FLAGS[:invite_users]
|
||||
|
||||
CATEGORIES = {
|
||||
invites: %i(
|
||||
invite_users
|
||||
).freeze,
|
||||
|
||||
moderation: %w(
|
||||
view_dashboard
|
||||
view_audit_log
|
||||
manage_users
|
||||
manage_user_access
|
||||
delete_user_data
|
||||
manage_reports
|
||||
manage_appeals
|
||||
manage_federation
|
||||
manage_blocks
|
||||
manage_taxonomies
|
||||
manage_invites
|
||||
).freeze,
|
||||
|
||||
administration: %w(
|
||||
manage_settings
|
||||
manage_rules
|
||||
manage_roles
|
||||
manage_webhooks
|
||||
manage_custom_emojis
|
||||
manage_announcements
|
||||
).freeze,
|
||||
|
||||
devops: %w(
|
||||
view_devops
|
||||
).freeze,
|
||||
|
||||
special: %i(
|
||||
administrator
|
||||
).freeze,
|
||||
}.freeze
|
||||
end
|
||||
|
||||
attr_writer :current_account
|
||||
|
||||
validates :name, presence: true, unless: :everyone?
|
||||
validates :color, format: { with: /\A#?(?:[A-F0-9]{3}){1,2}\z/i }, unless: -> { color.blank? }
|
||||
|
||||
validate :validate_permissions_elevation
|
||||
validate :validate_position_elevation
|
||||
validate :validate_dangerous_permissions
|
||||
|
||||
before_validation :set_position
|
||||
|
||||
scope :assignable, -> { where.not(id: -99).order(position: :asc) }
|
||||
|
||||
has_many :users, inverse_of: :role, foreign_key: 'role_id', dependent: :nullify
|
||||
|
||||
def self.nobody
|
||||
@nobody ||= UserRole.new(permissions: Flags::NONE, position: -1)
|
||||
end
|
||||
|
||||
def self.everyone
|
||||
UserRole.find(-99)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
UserRole.create!(id: -99, permissions: Flags::DEFAULT)
|
||||
end
|
||||
|
||||
def self.that_can(*any_of_privileges)
|
||||
all.select { |role| role.can?(*any_of_privileges) }
|
||||
end
|
||||
|
||||
def everyone?
|
||||
id == -99
|
||||
end
|
||||
|
||||
def nobody?
|
||||
id.nil?
|
||||
end
|
||||
|
||||
def permissions_as_keys
|
||||
FLAGS.keys.select { |privilege| permissions & FLAGS[privilege] == FLAGS[privilege] }.map(&:to_s)
|
||||
end
|
||||
|
||||
def permissions_as_keys=(value)
|
||||
self.permissions = value.map(&:presence).compact.reduce(Flags::NONE) { |bitmask, privilege| FLAGS.key?(privilege.to_sym) ? (bitmask | FLAGS[privilege.to_sym]) : bitmask }
|
||||
end
|
||||
|
||||
def can?(*any_of_privileges)
|
||||
any_of_privileges.any? { |privilege| in_permissions?(privilege) }
|
||||
end
|
||||
|
||||
def overrides?(other_role)
|
||||
other_role.nil? || position > other_role.position
|
||||
end
|
||||
|
||||
def computed_permissions
|
||||
# If called on the everyone role, no further computation needed
|
||||
return permissions if everyone?
|
||||
|
||||
# If called on the nobody role, no permissions are there to be given
|
||||
return Flags::NONE if nobody?
|
||||
|
||||
# Otherwise, compute permissions based on special conditions
|
||||
@computed_permissions ||= begin
|
||||
permissions = self.class.everyone.permissions | self.permissions
|
||||
|
||||
if permissions & FLAGS[:administrator] == FLAGS[:administrator]
|
||||
Flags::ALL
|
||||
else
|
||||
permissions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def in_permissions?(privilege)
|
||||
raise ArgumentError, "Unknown privilege: #{privilege}" unless FLAGS.key?(privilege)
|
||||
computed_permissions & FLAGS[privilege] == FLAGS[privilege]
|
||||
end
|
||||
|
||||
def set_position
|
||||
self.position = -1 if everyone?
|
||||
end
|
||||
|
||||
def validate_permissions_elevation
|
||||
errors.add(:permissions_as_keys, :elevated) if defined?(@current_account) && @current_account.user_role.computed_permissions & permissions != permissions
|
||||
end
|
||||
|
||||
def validate_position_elevation
|
||||
errors.add(:position, :elevated) if defined?(@current_account) && @current_account.user_role.position < position
|
||||
end
|
||||
|
||||
def validate_dangerous_permissions
|
||||
errors.add(:permissions_as_keys, :dangerous) if everyone? && Flags::DEFAULT & permissions != permissions
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AuditLogPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:view_audit_log)
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DashboardPolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:view_dashboard)
|
||||
end
|
||||
end
|
@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserRolePolicy < ApplicationPolicy
|
||||
def index?
|
||||
role.can?(:manage_roles)
|
||||
end
|
||||
|
||||
def create?
|
||||
role.can?(:manage_roles)
|
||||
end
|
||||
|
||||
def update?
|
||||
role.can?(:manage_roles) && role.overrides?(record)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
!record.everyone? && role.can?(:manage_roles) && role.overrides?(record) && role.id != record.id
|
||||
end
|
||||
end
|
@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::RoleSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :permissions, :color, :highlighted
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def permissions
|
||||
object.computed_permissions.to_s
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue