* 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 | |||
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 |