# 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