* Add customizable user roles * Various fixes and improvements * Add migration for old settings and fix tootctl role managementclosed-social-glitch-2
@ -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 | |||||
@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 | ||||
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 | |||||
@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 | ||||
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 | |||||
@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 | ||||
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 |