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
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class TwoFactorAuthenticationsController < BaseController
|
class Users::TwoFactorAuthenticationsController < BaseController
|
||||||
before_action :set_target_user
|
before_action :set_target_user
|
||||||
|
|
||||||
def destroy
|
def destroy
|
@ -1,17 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# 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 -> { 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
|
private
|
||||||
|
|
||||||
def set_links
|
def enabled?
|
||||||
@links = Trends.links.query.limit(limit_param(10))
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# 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 -> { 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
|
private
|
||||||
|
|
||||||
def set_statuses
|
def enabled?
|
||||||
@statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
# frozen_string_literal: true
|
# 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 -> { 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
|
private
|
||||||
|
|
||||||
def set_tags
|
def enabled?
|
||||||
@tags = Trends.tags.query.limit(limit_param(10))
|
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
|
||||||
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