forked from berserker/microblog
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
187 changed files with 1952 additions and 1039 deletions
@ -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