Add customizable user roles (#18641)

* Add customizable user roles

* Various fixes and improvements

* Add migration for old settings and fix tootctl role management
shrike
Eugen Rochko 2022-07-05 02:41:40 +02:00 committed by GitHub
parent 1b4054256f
commit 44b2ee3485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
187 changed files with 1945 additions and 1032 deletions

View File

@ -67,7 +67,7 @@ Lint/UselessAccessModifier:
- class_methods - class_methods
Metrics/AbcSize: Metrics/AbcSize:
Max: 100 Max: 115
Exclude: Exclude:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'
@ -84,7 +84,7 @@ Metrics/BlockNesting:
Metrics/ClassLength: Metrics/ClassLength:
CountComments: false CountComments: false
Max: 400 Max: 500
Exclude: Exclude:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'

View File

@ -5,11 +5,15 @@ module Admin
before_action :set_account before_action :set_account
def new def new
authorize @account, :show?
@account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true) @account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true)
@warning_presets = AccountWarningPreset.all @warning_presets = AccountWarningPreset.all
end end
def create def create
authorize @account, :show?
account_action = Admin::AccountAction.new(resource_params) account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account account_action.target_account = @account
account_action.current_account = current_account account_action.current_account = current_account

View File

@ -14,6 +14,8 @@ module Admin
end end
def batch def batch
authorize :account, :index?
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -4,7 +4,10 @@ module Admin
class ActionLogsController < BaseController class ActionLogsController < BaseController
before_action :set_action_logs before_action :set_action_logs
def index; end def index
authorize :audit_log, :index?
@auditable_accounts = Account.where(id: Admin::ActionLog.reorder(nil).select('distinct account_id')).select(:id, :username)
end
private private

View File

@ -7,8 +7,8 @@ module Admin
layout 'admin' layout 'admin'
before_action :require_staff!
before_action :set_body_classes before_action :set_body_classes
after_action :verify_authorized
private private

View File

@ -29,6 +29,8 @@ module Admin
end end
def batch def batch
authorize :custom_emoji, :index?
@form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::CustomEmojiBatch.new(form_custom_emoji_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -5,7 +5,9 @@ module Admin
include Redisable include Redisable
def index def index
@system_checks = Admin::SystemCheck.perform authorize :dashboard, :index?
@system_checks = Admin::SystemCheck.perform(current_user)
@time_period = (29.days.ago.to_date...Time.now.utc.to_date) @time_period = (29.days.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.count @pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count @pending_reports_count = Report.unresolved.count

View File

@ -12,6 +12,8 @@ module Admin
end end
def batch def batch
authorize :email_domain_block, :index?
@form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -12,6 +12,8 @@ module Admin
end end
def update def update
authorize :follow_recommendation, :show?
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -29,6 +29,8 @@ module Admin
end end
def batch def batch
authorize :ip_block, :index?
@form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -7,7 +7,7 @@ module Admin
PER_PAGE = 40 PER_PAGE = 40
def index def index
authorize :account, :index? authorize @account, :show?
@accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE) @accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE)
@form = Form::AccountBatch.new @form = Form::AccountBatch.new

View File

@ -2,20 +2,63 @@
module Admin module Admin
class RolesController < BaseController class RolesController < BaseController
before_action :set_user before_action :set_role, except: [:index, :new, :create]
def promote def index
authorize @user, :promote? authorize :user_role, :index?
@user.promote!
log_action :promote, @user @roles = UserRole.order(position: :desc).page(params[:page])
redirect_to admin_account_path(@user.account_id)
end end
def demote def new
authorize @user, :demote? authorize :user_role, :create?
@user.demote!
log_action :demote, @user @role = UserRole.new
redirect_to admin_account_path(@user.account_id) end
def create
authorize :user_role, :create?
@role = UserRole.new(resource_params)
@role.current_account = current_account
if @role.save
redirect_to admin_roles_path
else
render :new
end
end
def edit
authorize @role, :update?
end
def update
authorize @role, :update?
@role.current_account = current_account
if @role.update(resource_params)
redirect_to admin_roles_path
else
render :edit
end
end
def destroy
authorize @role, :destroy?
@role.destroy!
redirect_to admin_roles_path
end
private
def set_role
@role = UserRole.find(params[:id])
end
def resource_params
params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: [])
end end
end end
end end

View File

@ -14,6 +14,8 @@ module Admin
end end
def batch def batch
authorize :status, :index?
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button)) @status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
@status_batch_action.save! @status_batch_action.save!
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

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

View File

@ -2,13 +2,15 @@
class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController
def index def index
authorize :preview_card_provider, :index? authorize :preview_card_provider, :review?
@preview_card_providers = filtered_preview_card_providers.page(params[:page]) @preview_card_providers = filtered_preview_card_providers.page(params[:page])
@form = Trends::PreviewCardProviderBatch.new @form = Trends::PreviewCardProviderBatch.new
end end
def batch def batch
authorize :preview_card_provider, :review?
@form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -2,13 +2,15 @@
class Admin::Trends::LinksController < Admin::BaseController class Admin::Trends::LinksController < Admin::BaseController
def index def index
authorize :preview_card, :index? authorize :preview_card, :review?
@preview_cards = filtered_preview_cards.page(params[:page]) @preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new @form = Trends::PreviewCardBatch.new
end end
def batch def batch
authorize :preview_card, :review?
@form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -2,13 +2,15 @@
class Admin::Trends::StatusesController < Admin::BaseController class Admin::Trends::StatusesController < Admin::BaseController
def index def index
authorize :status, :index? authorize :status, :review?
@statuses = filtered_statuses.page(params[:page]) @statuses = filtered_statuses.page(params[:page])
@form = Trends::StatusBatch.new @form = Trends::StatusBatch.new
end end
def batch def batch
authorize :status, :review?
@form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

@ -2,13 +2,15 @@
class Admin::Trends::TagsController < Admin::BaseController class Admin::Trends::TagsController < Admin::BaseController
def index def index
authorize :tag, :index? authorize :tag, :review?
@tags = filtered_tags.page(params[:page]) @tags = filtered_tags.page(params[:page])
@form = Trends::TagBatch.new @form = Trends::TagBatch.new
end end
def batch def batch
authorize :tag, :review?
@form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing

View File

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

View File

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

View File

@ -1,11 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Admin::AccountActionsController < Api::BaseController class Api::V1::Admin::AccountActionsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' } before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
before_action :require_staff!
before_action :set_account before_action :set_account
after_action :verify_authorized
def create def create
authorize @account, :show?
account_action = Admin::AccountAction.new(resource_params) account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account account_action.target_account = @account
account_action.current_account = current_account account_action.current_account = current_account

View File

@ -8,11 +8,11 @@ class Api::V1::Admin::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index before_action :set_accounts, only: :index
before_action :set_account, except: :index before_action :set_account, except: :index
before_action :require_local_account!, only: [:enable, :approve, :reject] before_action :require_local_account!, only: [:enable, :approve, :reject]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i( FILTER_PARAMS = %i(
@ -119,7 +119,9 @@ class Api::V1::Admin::AccountsController < Api::BaseController
translated_params[:status] = status.to_s if params[status].present? translated_params[:status] = status.to_s if params[status].present?
end end
translated_params[:permissions] = 'staff' if params[:staff].present? if params[:staff].present?
translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id)
end
translated_params translated_params
end end

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Admin::DimensionsController < Api::BaseController class Api::V1::Admin::DimensionsController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :'admin:read' } before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_dimensions before_action :set_dimensions
after_action :verify_authorized
def create def create
authorize :dashboard, :index?
render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
end end

View File

@ -8,10 +8,10 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
before_action :require_staff!
before_action :set_domain_allows, only: :index before_action :set_domain_allows, only: :index
before_action :set_domain_allow, only: [:show, :destroy] before_action :set_domain_allow, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze PAGINATION_PARAMS = %i(limit).freeze

View File

@ -8,10 +8,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show]
before_action :require_staff!
before_action :set_domain_blocks, only: :index before_action :set_domain_blocks, only: :index
before_action :set_domain_block, only: [:show, :update, :destroy] before_action :set_domain_block, only: [:show, :update, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze PAGINATION_PARAMS = %i(limit).freeze

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Admin::MeasuresController < Api::BaseController class Api::V1::Admin::MeasuresController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :'admin:read' } before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_measures before_action :set_measures
after_action :verify_authorized
def create def create
authorize :dashboard, :index?
render json: @measures, each_serializer: REST::Admin::MeasureSerializer render json: @measures, each_serializer: REST::Admin::MeasureSerializer
end end

View File

@ -8,10 +8,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff!
before_action :set_reports, only: :index before_action :set_reports, only: :index
before_action :set_report, except: :index before_action :set_report, except: :index
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i( FILTER_PARAMS = %i(

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Admin::RetentionController < Api::BaseController class Api::V1::Admin::RetentionController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :'admin:read' } before_action -> { authorize_if_got_token! :'admin:read' }
before_action :require_staff!
before_action :set_cohorts before_action :set_cohorts
after_action :verify_authorized
def create def create
authorize :dashboard, :index?
render json: @cohorts, each_serializer: REST::Admin::CohortSerializer render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
end end

View File

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

View File

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

View File

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

View File

@ -13,10 +13,14 @@ class Api::V1::Trends::LinksController < Api::BaseController
private private
def enabled?
Setting.trends
end
def set_links def set_links
@links = begin @links = begin
if Setting.trends if enabled?
links_from_trends links_from_trends.offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT))
else else
[] []
end end
@ -24,7 +28,7 @@ class Api::V1::Trends::LinksController < Api::BaseController
end end
def links_from_trends def links_from_trends
Trends.links.query.allowed.in_locale(content_locale).offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) Trends.links.query.allowed.in_locale(content_locale)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@ -11,10 +11,14 @@ class Api::V1::Trends::StatusesController < Api::BaseController
private private
def enabled?
Setting.trends
end
def set_statuses def set_statuses
@statuses = begin @statuses = begin
if Setting.trends if enabled?
cache_collection(statuses_from_trends, Status) cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
else else
[] []
end end
@ -24,7 +28,7 @@ class Api::V1::Trends::StatusesController < Api::BaseController
def statuses_from_trends def statuses_from_trends
scope = Trends.statuses.query.allowed.in_locale(content_locale) scope = Trends.statuses.query.allowed.in_locale(content_locale)
scope = scope.filtered_for(current_account) if user_signed_in? scope = scope.filtered_for(current_account) if user_signed_in?
scope.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)) scope
end end
def insert_pagination_headers def insert_pagination_headers

View File

@ -13,16 +13,24 @@ class Api::V1::Trends::TagsController < Api::BaseController
private private
def enabled?
Setting.trends
end
def set_tags def set_tags
@tags = begin @tags = begin
if Setting.trends if enabled?
Trends.tags.query.allowed.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) tags_from_trends.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT))
else else
[] []
end end
end end
end end
def tags_from_trends
Trends.tags.query.allowed
end
def insert_pagination_headers def insert_pagination_headers
set_pagination_headers(next_path, prev_path) set_pagination_headers(next_path, prev_path)
end end

View File

@ -11,6 +11,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
email email
ip ip
invited_by invited_by
role_ids
).freeze ).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
@ -18,7 +19,17 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private private
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params).results AccountFilter.new(translated_filter_params).results
end
def translated_filter_params
translated_params = filter_params.slice(*AccountFilter::KEYS)
if params[:permissions] == 'staff'
translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id)
end
translated_params
end end
def filter_params def filter_params

View File

@ -56,14 +56,6 @@ class ApplicationController < ActionController::Base
store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym)
end end
def require_admin!
forbidden unless current_user&.admin?
end
def require_staff!
forbidden unless current_user&.staff?
end
def require_functional! def require_functional!
redirect_to edit_user_registration_path unless current_user.functional? redirect_to edit_user_registration_path unless current_user.functional?
end end

View File

@ -13,6 +13,6 @@ class CustomCssController < ApplicationController
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
request.session_options[:skip] = true request.session_options[:skip] = true
render plain: Setting.custom_css || '', content_type: 'text/css' render content_type: 'text/css'
end end
end end

View File

@ -61,21 +61,13 @@ module AccountsHelper
end end
end end
def account_badge(account, all: false) def account_badge(account)
if account.bot? if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif account.group? elsif account.group?
content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles')
elsif (Setting.show_staff_badge && account.user_staff?) || all elsif account.user_role&.highlighted?
content_tag(:div, class: 'roles') do content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
if all && !account.user_staff?
content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role')
elsif account.user_admin?
content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin')
elsif account.user_moderator?
content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator')
end
end
end end
end end

View File

@ -6,8 +6,9 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, isStaff } from '../initial_state'; import { me } from '../initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -55,6 +56,7 @@ class StatusActionBar extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
identity: PropTypes.object,
}; };
static propTypes = { static propTypes = {
@ -306,7 +308,7 @@ class StatusActionBar extends ImmutablePureComponent {
} }
} }
if (isStaff) { if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });

View File

@ -26,6 +26,7 @@ const createIdentityContext = state => ({
signedIn: !!state.meta.me, signedIn: !!state.meta.me,
accountId: state.meta.me, accountId: state.meta.me,
accessToken: state.meta.access_token, accessToken: state.meta.access_token,
permissions: state.role.permissions,
}); });
export default class Mastodon extends React.PureComponent { export default class Mastodon extends React.PureComponent {

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import { autoPlayGif, me } from 'mastodon/initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
@ -14,6 +14,7 @@ import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container'; import AccountNoteContainer from '../containers/account_note_container';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -64,6 +65,10 @@ const dateFormatOptions = {
export default @injectIntl export default @injectIntl
class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
identity_props: ImmutablePropTypes.list, identity_props: ImmutablePropTypes.list,
@ -241,7 +246,7 @@ class Header extends ImmutablePureComponent {
} }
} }
if (account.get('id') !== me && isStaff) { if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
} }

View File

@ -5,10 +5,14 @@ import { FormattedMessage } from 'react-intl';
import ClearColumnButton from './clear_column_button'; import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button'; import GrantPermissionButton from './grant_permission_button';
import SettingToggle from './setting_toggle'; import SettingToggle from './setting_toggle';
import { isStaff } from 'mastodon/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions';
export default class ColumnSettings extends React.PureComponent { export default class ColumnSettings extends React.PureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = { static propTypes = {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
pushSettings: ImmutablePropTypes.map.isRequired, pushSettings: ImmutablePropTypes.map.isRequired,
@ -166,7 +170,7 @@ export default class ColumnSettings extends React.PureComponent {
</div> </div>
</div> </div>
{isStaff && ( {(this.context.identity.permissions & PERMISSION_MANAGE_USERS === PERMISSION_MANAGE_USERS) && (
<div role='group' aria-labelledby='notifications-admin-sign-up'> <div role='group' aria-labelledby='notifications-admin-sign-up'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span> <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
@ -179,7 +183,7 @@ export default class ColumnSettings extends React.PureComponent {
</div> </div>
)} )}
{isStaff && ( {(this.context.identity.permissions & PERMISSION_MANAGE_REPORTS === PERMISSION_MANAGE_REPORTS) && (
<div role='group' aria-labelledby='notifications-admin-report'> <div role='group' aria-labelledby='notifications-admin-report'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span> <span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>

View File

@ -5,8 +5,9 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state'; import { me } from '../../../initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -50,6 +51,7 @@ class ActionBar extends React.PureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
identity: PropTypes.object,
}; };
static propTypes = { static propTypes = {
@ -248,7 +250,7 @@ class ActionBar extends React.PureComponent {
} }
} }
if (isStaff) { if ((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` }); menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });

View File

@ -3,9 +3,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { invitesEnabled, limitedFederationMode, version, repository, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state'; import { limitedFederationMode, version, repository, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out'; import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
const messages = defineMessages({ const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
@ -27,6 +28,10 @@ export default @injectIntl
@connect(null, mapDispatchToProps) @connect(null, mapDispatchToProps)
class LinkFooter extends React.PureComponent { class LinkFooter extends React.PureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = { static propTypes = {
withHotkeys: PropTypes.bool, withHotkeys: PropTypes.bool,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
@ -48,7 +53,7 @@ class LinkFooter extends React.PureComponent {
return ( return (
<div className='getting-started__footer'> <div className='getting-started__footer'>
<ul> <ul>
{invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>} {((this.context.identity.permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS) && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
{!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>} {!limitedFederationMode && <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>}

View File

@ -12,14 +12,12 @@ export const boostModal = getMeta('boost_modal');
export const deleteModal = getMeta('delete_modal'); export const deleteModal = getMeta('delete_modal');
export const me = getMeta('me'); export const me = getMeta('me');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const invitesEnabled = getMeta('invites_enabled');
export const limitedFederationMode = getMeta('limited_federation_mode'); export const limitedFederationMode = getMeta('limited_federation_mode');
export const repository = getMeta('repository'); export const repository = getMeta('repository');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const version = getMeta('version'); export const version = getMeta('version');
export const mascot = getMeta('mascot'); export const mascot = getMeta('mascot');
export const profile_directory = getMeta('profile_directory'); export const profile_directory = getMeta('profile_directory');
export const isStaff = getMeta('is_staff');
export const forceSingleColumn = !getMeta('advanced_layout'); export const forceSingleColumn = !getMeta('advanced_layout');
export const useBlurhash = getMeta('use_blurhash'); export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items'); export const usePendingItems = getMeta('use_pending_items');

View File

@ -0,0 +1,3 @@
export const PERMISSION_INVITE_USERS = 0x0000000000010000;
export const PERMISSION_MANAGE_USERS = 0x0000000000000400;
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010;

View File

@ -7,12 +7,13 @@ const initialState = ImmutableMap({
streaming_api_base_url: null, streaming_api_base_url: null,
access_token: null, access_token: null,
layout: layoutFromWindow(), layout: layoutFromWindow(),
permissions: '0',
}); });
export default function meta(state = initialState, action) { export default function meta(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
return state.merge(action.state.get('meta')); return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
case APP_LAYOUT_CHANGE: case APP_LAYOUT_CHANGE:
return state.set('layout', action.layout); return state.set('layout', action.layout);
default: default:

View File

@ -924,6 +924,10 @@ a.name-tag,
margin-top: 15px; margin-top: 15px;
} }
.user-role {
color: var(--user-role-accent);
}
.announcements-list, .announcements-list,
.filters-list { .filters-list {
border: 1px solid lighten($ui-base-color, 4%); border: 1px solid lighten($ui-base-color, 4%);
@ -960,6 +964,17 @@ a.name-tag,
&__meta { &__meta {
padding: 0 15px; padding: 0 15px;
color: $dark-text-color; color: $dark-text-color;
a {
color: inherit;
text-decoration: underline;
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
} }
&__action-bar { &__action-bar {

View File

@ -256,6 +256,10 @@ code {
} }
} }
.input.with_block_label.user_role_permissions_as_keys ul {
columns: unset;
}
.input.datetime .label_input select { .input.datetime .label_input select {
display: inline-block; display: inline-block;
width: auto; width: auto;

View File

@ -8,11 +8,11 @@ class Admin::SystemCheck
Admin::SystemCheck::ElasticsearchCheck, Admin::SystemCheck::ElasticsearchCheck,
].freeze ].freeze
def self.perform def self.perform(current_user)
ACTIVE_CHECKS.each_with_object([]) do |klass, arr| ACTIVE_CHECKS.each_with_object([]) do |klass, arr|
check = klass.new check = klass.new(current_user)
if check.pass? if check.skip? || check.pass?
arr arr
else else
arr << check.message arr << check.message

View File

@ -1,6 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::SystemCheck::BaseCheck class Admin::SystemCheck::BaseCheck
attr_reader :current_user
def initialize(current_user)
@current_user = current_user
end
def skip?
false
end
def pass? def pass?
raise NotImplementedError raise NotImplementedError
end end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::SystemCheck::DatabaseSchemaCheck < Admin::SystemCheck::BaseCheck class Admin::SystemCheck::DatabaseSchemaCheck < Admin::SystemCheck::BaseCheck
def skip?
!current_user.can?(:view_devops)
end
def pass? def pass?
!ActiveRecord::Base.connection.migration_context.needs_migration? !ActiveRecord::Base.connection.migration_context.needs_migration?
end end

View File

@ -1,6 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def skip?
!current_user.can?(:view_devops)
end
def pass? def pass?
return true unless Chewy.enabled? return true unless Chewy.enabled?
@ -32,8 +36,4 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
def compatible_version? def compatible_version?
Gem::Version.new(running_version) >= Gem::Version.new(required_version) Gem::Version.new(running_version) >= Gem::Version.new(required_version)
end end
def missing_queues
@missing_queues ||= Sidekiq::ProcessSet.new.reduce(SIDEKIQ_QUEUES) { |queues, process| queues - process['queues'] }
end
end end

View File

@ -3,6 +3,10 @@
class Admin::SystemCheck::RulesCheck < Admin::SystemCheck::BaseCheck class Admin::SystemCheck::RulesCheck < Admin::SystemCheck::BaseCheck
include RoutingHelper include RoutingHelper
def skip?
!current_user.can?(:manage_rules)
end
def pass? def pass?
Rule.kept.exists? Rule.kept.exists?
end end

View File

@ -9,6 +9,10 @@ class Admin::SystemCheck::SidekiqProcessCheck < Admin::SystemCheck::BaseCheck
scheduler scheduler
).freeze ).freeze
def skip?
!current_user.can?(:view_devops)
end
def pass? def pass?
missing_queues.empty? missing_queues.empty?
end end

View File

@ -116,7 +116,7 @@ class Account < ApplicationRecord
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) } scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) } scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') } scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) } scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches("%.#{domain}"))) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) } scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
@ -132,9 +132,6 @@ class Account < ApplicationRecord
:unconfirmed?, :unconfirmed?,
:unconfirmed_or_pending?, :unconfirmed_or_pending?,
:role, :role,
:admin?,
:moderator?,
:staff?,
:locale, :locale,
:shows_application?, :shows_application?,
to: :user, to: :user,
@ -454,7 +451,7 @@ class Account < ApplicationRecord
DeliveryFailureTracker.without_unavailable(urls) DeliveryFailureTracker.without_unavailable(urls)
end end
def search_for(terms, limit = 10, offset = 0) def search_for(terms, limit: 10, offset: 0)
tsquery = generate_query_for_search(terms) tsquery = generate_query_for_search(terms)
sql = <<-SQL.squish sql = <<-SQL.squish
@ -476,7 +473,7 @@ class Account < ApplicationRecord
records records
end end
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0) def advanced_search_for(terms, account, limit: 10, following: false, offset: 0)
tsquery = generate_query_for_search(terms) tsquery = generate_query_for_search(terms)
sql = advanced_search_for_sql_template(following) sql = advanced_search_for_sql_template(following)
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery]) records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])

View File

@ -4,7 +4,7 @@ class AccountFilter
KEYS = %i( KEYS = %i(
origin origin
status status
permissions role_ids
username username
by_domain by_domain
display_name display_name
@ -26,7 +26,7 @@ class AccountFilter
params.each do |key, value| params.each do |key, value|
next if key.to_s == 'page' next if key.to_s == 'page'
scope.merge!(scope_for(key, value.to_s.strip)) if value.present? scope.merge!(scope_for(key, value)) if value.present?
end end
scope scope
@ -38,18 +38,18 @@ class AccountFilter
case key.to_s case key.to_s
when 'origin' when 'origin'
origin_scope(value) origin_scope(value)
when 'permissions' when 'role_ids'
permissions_scope(value) role_scope(value)
when 'status' when 'status'
status_scope(value) status_scope(value)
when 'by_domain' when 'by_domain'
Account.where(domain: value) Account.where(domain: value.to_s)
when 'username' when 'username'
Account.matches_username(value) Account.matches_username(value.to_s)
when 'display_name' when 'display_name'
Account.matches_display_name(value) Account.matches_display_name(value.to_s)
when 'email' when 'email'
accounts_with_users.merge(User.matches_email(value)) accounts_with_users.merge(User.matches_email(value.to_s))
when 'ip' when 'ip'
valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group('users.id, accounts.id')) : Account.none valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value).group('users.id, accounts.id')) : Account.none
when 'invited_by' when 'invited_by'
@ -104,13 +104,8 @@ class AccountFilter
Account.left_joins(user: :invite).merge(Invite.where(user_id: value.to_s)) Account.left_joins(user: :invite).merge(Invite.where(user_id: value.to_s))
end end
def permissions_scope(value) def role_scope(value)
case value.to_s accounts_with_users.merge(User.where(role_id: Array(value).map(&:to_s)))
when 'staff'
accounts_with_users.merge(User.staff)
else
raise "Unknown permissions: #{value}"
end
end end
def accounts_with_users def accounts_with_users
@ -118,7 +113,7 @@ class AccountFilter
end end
def valid_ip?(value) def valid_ip?(value)
IPAddr.new(value) && true IPAddr.new(value.to_s) && true
rescue IPAddr::InvalidAddressError rescue IPAddr::InvalidAddressError
false false
end end

View File

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

View File

@ -15,10 +15,8 @@ class Form::AdminSettings
closed_registrations_message closed_registrations_message
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge
bootstrap_timeline_accounts bootstrap_timeline_accounts
theme theme
min_invite_role
activity_api_enabled activity_api_enabled
peers_api_enabled peers_api_enabled
show_known_fediverse_at_about_page show_known_fediverse_at_about_page
@ -39,7 +37,6 @@ class Form::AdminSettings
BOOLEAN_KEYS = %i( BOOLEAN_KEYS = %i(
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge
activity_api_enabled activity_api_enabled
peers_api_enabled peers_api_enabled
show_known_fediverse_at_about_page show_known_fediverse_at_about_page
@ -62,7 +59,6 @@ class Form::AdminSettings
validates :site_short_description, :site_description, html: { wrap_with: :p } validates :site_short_description, :site_description, html: { wrap_with: :p }
validates :site_extended_description, :site_terms, :closed_registrations_message, html: true validates :site_extended_description, :site_terms, :closed_registrations_message, html: true
validates :registrations_mode, inclusion: { in: %w(open approved none) } validates :registrations_mode, inclusion: { in: %w(open approved none) }
validates :min_invite_role, inclusion: { in: %w(disabled user moderator admin) }
validates :site_contact_email, :site_contact_username, presence: true validates :site_contact_email, :site_contact_username, presence: true
validates :site_contact_username, existing_username: true validates :site_contact_username, existing_username: true
validates :bootstrap_timeline_accounts, existing_username: { multiple: true } validates :bootstrap_timeline_accounts, existing_username: { multiple: true }

View File

@ -34,7 +34,7 @@ module Trends
return if links_requiring_review.empty? && tags_requiring_review.empty? && statuses_requiring_review.empty? return if links_requiring_review.empty? && tags_requiring_review.empty? && statuses_requiring_review.empty?
User.staff.includes(:account).find_each do |user| User.those_who_can(:manage_taxonomies).includes(:account).find_each do |user|
AdminMailer.new_trends(user.account, links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails? AdminMailer.new_trends(user.account, links_requiring_review, tags_requiring_review, statuses_requiring_review).deliver_later! if user.allows_trends_review_emails?
end end
end end

View File

@ -37,6 +37,7 @@
# sign_in_token_sent_at :datetime # sign_in_token_sent_at :datetime
# webauthn_id :string # webauthn_id :string
# sign_up_ip :inet # sign_up_ip :inet
# role_id :bigint(8)
# #
class User < ApplicationRecord class User < ApplicationRecord
@ -50,7 +51,6 @@ class User < ApplicationRecord
) )
include Settings::Extend include Settings::Extend
include UserRoles
include Redisable include Redisable
include LanguagesHelper include LanguagesHelper
@ -79,6 +79,7 @@ class User < ApplicationRecord
belongs_to :account, inverse_of: :user belongs_to :account, inverse_of: :user
belongs_to :invite, counter_cache: :uses, optional: true belongs_to :invite, counter_cache: :uses, optional: true
belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true belongs_to :created_by_application, class_name: 'Doorkeeper::Application', optional: true
belongs_to :role, class_name: 'UserRole', optional: true
accepts_nested_attributes_for :account accepts_nested_attributes_for :account
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@ -103,6 +104,7 @@ class User < ApplicationRecord
validates_with RegistrationFormTimeValidator, on: :create validates_with RegistrationFormTimeValidator, on: :create
validates :website, absence: true, on: :create validates :website, absence: true, on: :create
validates :confirm_password, absence: true, on: :create validates :confirm_password, absence: true, on: :create
validate :validate_role_elevation
scope :recent, -> { order(id: :desc) } scope :recent, -> { order(id: :desc) }
scope :pending, -> { where(approved: false) } scope :pending, -> { where(approved: false) }
@ -117,6 +119,7 @@ class User < ApplicationRecord
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) } scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
before_validation :sanitize_languages before_validation :sanitize_languages
before_validation :sanitize_role
before_create :set_approved before_create :set_approved
after_commit :send_pending_devise_notifications after_commit :send_pending_devise_notifications
after_create_commit :trigger_webhooks after_create_commit :trigger_webhooks
@ -135,8 +138,28 @@ class User < ApplicationRecord
:disable_swiping, :always_send_emails, :disable_swiping, :always_send_emails,
to: :settings, prefix: :setting, allow_nil: false to: :settings, prefix: :setting, allow_nil: false
delegate :can?, to: :role
attr_reader :invite_code attr_reader :invite_code
attr_writer :external, :bypass_invite_request_check attr_writer :external, :bypass_invite_request_check, :current_account
def self.those_who_can(*any_of_privileges)
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
if matching_role_ids.empty?
none
else
where(role_id: matching_role_ids)
end
end
def role
if role_id.nil?
UserRole.everyone
else
super
end
end
def confirmed? def confirmed?
confirmed_at.present? confirmed_at.present?
@ -441,6 +464,11 @@ class User < ApplicationRecord
self.chosen_languages = nil if chosen_languages.empty? self.chosen_languages = nil if chosen_languages.empty?
end end
def sanitize_role
return if role.nil?
self.role = nil if role.everyone?
end
def prepare_new_user! def prepare_new_user!
BootstrapTimelineWorker.perform_async(account_id) BootstrapTimelineWorker.perform_async(account_id)
ActivityTracker.increment('activity:accounts:local') ActivityTracker.increment('activity:accounts:local')
@ -453,7 +481,7 @@ class User < ApplicationRecord
end end
def notify_staff_about_pending_account! def notify_staff_about_pending_account!
User.staff.includes(:account).find_each do |u| User.those_who_can(:manage_users).includes(:account).find_each do |u|
next unless u.allows_pending_account_emails? next unless u.allows_pending_account_emails?
AdminMailer.new_pending_account(u.account, self).deliver_later AdminMailer.new_pending_account(u.account, self).deliver_later
end end
@ -471,6 +499,10 @@ class User < ApplicationRecord
email_changed? && !external? && !(Rails.env.test? || Rails.env.development?) email_changed? && !external? && !(Rails.env.test? || Rails.env.development?)
end end
def validate_role_elevation
errors.add(:role_id, :elevated) if defined?(@current_account) && role&.overrides?(@current_account&.user_role)
end
def invite_text_required? def invite_text_required?
Setting.require_invite_text && !invited? && !external? && !bypass_invite_request_check? Setting.require_invite_text && !invited? && !external? && !bypass_invite_request_check?
end end

179
app/models/user_role.rb Normal file
View File

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

View File

@ -2,11 +2,11 @@
class AccountModerationNotePolicy < ApplicationPolicy class AccountModerationNotePolicy < ApplicationPolicy
def create? def create?
staff? role.can?(:manage_reports)
end end
def destroy? def destroy?
admin? || owner? owner? || (role.can?(:manage_reports) && role.overrides?(record.account.user_role))
end end
private private

View File

@ -2,74 +2,66 @@
class AccountPolicy < ApplicationPolicy class AccountPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_users)
end end
def show? def show?
staff? role.can?(:manage_users)
end end
def warn? def warn?
staff? && !record.user&.staff? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role)
end end
def suspend? def suspend?
staff? && !record.user&.staff? && !record.instance_actor? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role) && !record.instance_actor?
end end
def destroy? def destroy?
record.suspended_temporarily? && admin? record.suspended_temporarily? && role.can?(:delete_user_data)
end end
def unsuspend? def unsuspend?
staff? && record.suspension_origin_local? role.can?(:manage_users) && record.suspension_origin_local?
end end
def sensitive? def sensitive?
staff? && !record.user&.staff? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role)
end end
def unsensitive? def unsensitive?
staff? role.can?(:manage_users)
end end
def silence? def silence?
staff? && !record.user&.staff? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role)
end end
def unsilence? def unsilence?
staff? role.can?(:manage_users)
end end
def redownload? def redownload?
admin? role.can?(:manage_federation)
end end
def remove_avatar? def remove_avatar?
staff? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role)
end end
def remove_header? def remove_header?
staff? role.can?(:manage_users, :manage_reports) && role.overrides?(record.user_role)
end
def subscribe?
admin?
end
def unsubscribe?
admin?
end end
def memorialize? def memorialize?
admin? && !record.user&.admin? && !record.instance_actor? role.can?(:delete_user_data) && role.overrides?(record.user_role) && !record.instance_actor?
end end
def unblock_email? def unblock_email?
staff? role.can?(:manage_users)
end end
def review? def review?
staff? role.can?(:manage_taxonomies)
end end
end end

View File

@ -2,7 +2,7 @@
class AccountWarningPolicy < ApplicationPolicy class AccountWarningPolicy < ApplicationPolicy
def show? def show?
target? || staff? target? || role.can?(:manage_appeals)
end end
def appeal? def appeal?

View File

@ -2,18 +2,18 @@
class AccountWarningPresetPolicy < ApplicationPolicy class AccountWarningPresetPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_settings)
end end
def create? def create?
staff? role.can?(:manage_settings)
end end
def update? def update?
staff? role.can?(:manage_settings)
end end
def destroy? def destroy?
staff? role.can?(:manage_settings)
end end
end end

View File

@ -2,18 +2,18 @@
class AnnouncementPolicy < ApplicationPolicy class AnnouncementPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_announcements)
end end
def create? def create?
admin? role.can?(:manage_announcements)
end end
def update? def update?
admin? role.can?(:manage_announcements)
end end
def destroy? def destroy?
admin? role.can?(:manage_announcements)
end end
end end

View File

@ -2,12 +2,14 @@
class AppealPolicy < ApplicationPolicy class AppealPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_appeals)
end end
def approve? def approve?
record.pending? && staff? record.pending? && role.can?(:manage_appeals)
end end
alias reject? approve? def reject?
record.pending? && role.can?(:manage_appeals)
end
end end

View File

@ -8,8 +8,6 @@ class ApplicationPolicy
@record = record @record = record
end end
delegate :admin?, :moderator?, :staff?, to: :current_user, allow_nil: true
private private
def current_user def current_user
@ -19,4 +17,8 @@ class ApplicationPolicy
def user_signed_in? def user_signed_in?
!current_user.nil? !current_user.nil?
end end
def role
current_user&.role || UserRole.nobody
end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AuditLogPolicy < ApplicationPolicy
def index?
role.can?(:view_audit_log)
end
end

View File

@ -2,30 +2,30 @@
class CustomEmojiPolicy < ApplicationPolicy class CustomEmojiPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_custom_emojis)
end end
def create? def create?
admin? role.can?(:manage_custom_emojis)
end end
def update? def update?
admin? role.can?(:manage_custom_emojis)
end end
def copy? def copy?
admin? role.can?(:manage_custom_emojis)
end end
def enable? def enable?
staff? role.can?(:manage_custom_emojis)
end end
def disable? def disable?
staff? role.can?(:manage_custom_emojis)
end end
def destroy? def destroy?
admin? role.can?(:manage_custom_emojis)
end end
end end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class DashboardPolicy < ApplicationPolicy
def index?
role.can?(:view_dashboard)
end
end

View File

@ -2,14 +2,14 @@
class DeliveryPolicy < ApplicationPolicy class DeliveryPolicy < ApplicationPolicy
def clear_delivery_errors? def clear_delivery_errors?
admin? role.can?(:manage_federation)
end end
def restart_delivery? def restart_delivery?
admin? role.can?(:manage_federation)
end end
def stop_delivery? def stop_delivery?
admin? role.can?(:manage_federation)
end end
end end

View File

@ -2,18 +2,18 @@
class DomainAllowPolicy < ApplicationPolicy class DomainAllowPolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_federation)
end end
def show? def show?
admin? role.can?(:manage_federation)
end end
def create? def create?
admin? role.can?(:manage_federation)
end end
def destroy? def destroy?
admin? role.can?(:manage_federation)
end end
end end

View File

@ -2,22 +2,22 @@
class DomainBlockPolicy < ApplicationPolicy class DomainBlockPolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_federation)
end end
def show? def show?
admin? role.can?(:manage_federation)
end end
def create? def create?
admin? role.can?(:manage_federation)
end end
def update? def update?
admin? role.can?(:manage_federation)
end end
def destroy? def destroy?
admin? role.can?(:manage_federation)
end end
end end

View File

@ -2,14 +2,14 @@
class EmailDomainBlockPolicy < ApplicationPolicy class EmailDomainBlockPolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_blocks)
end end
def create? def create?
admin? role.can?(:manage_blocks)
end end
def destroy? def destroy?
admin? role.can?(:manage_blocks)
end end
end end

View File

@ -2,14 +2,14 @@
class FollowRecommendationPolicy < ApplicationPolicy class FollowRecommendationPolicy < ApplicationPolicy
def show? def show?
staff? role.can?(:manage_taxonomies)
end end
def suppress? def suppress?
staff? role.can?(:manage_taxonomies)
end end
def unsuppress? def unsuppress?
staff? role.can?(:manage_taxonomies)
end end
end end

View File

@ -2,14 +2,14 @@
class InstancePolicy < ApplicationPolicy class InstancePolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_federation)
end end
def show? def show?
admin? role.can?(:manage_federation)
end end
def destroy? def destroy?
admin? role.can?(:manage_federation)
end end
end end

View File

@ -2,19 +2,19 @@
class InvitePolicy < ApplicationPolicy class InvitePolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_invites)
end end
def create? def create?
min_required_role? role.can?(:invite_users)
end end
def deactivate_all? def deactivate_all?
admin? role.can?(:manage_invites)
end end
def destroy? def destroy?
owner? || (Setting.min_invite_role == 'admin' ? admin? : staff?) owner? || role.can?(:manage_invites)
end end
private private
@ -22,8 +22,4 @@ class InvitePolicy < ApplicationPolicy
def owner? def owner?
record.user_id == current_user&.id record.user_id == current_user&.id
end end
def min_required_role?
current_user&.role?(Setting.min_invite_role)
end
end end

View File

@ -2,14 +2,14 @@
class IpBlockPolicy < ApplicationPolicy class IpBlockPolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_blocks)
end end
def create? def create?
admin? role.can?(:manage_blocks)
end end
def destroy? def destroy?
admin? role.can?(:manage_blocks)
end end
end end

View File

@ -2,10 +2,10 @@
class PreviewCardPolicy < ApplicationPolicy class PreviewCardPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_taxonomies)
end end
def review? def review?
staff? role.can?(:manage_taxonomies)
end end
end end

View File

@ -2,10 +2,10 @@
class PreviewCardProviderPolicy < ApplicationPolicy class PreviewCardProviderPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_taxonomies)
end end
def review? def review?
staff? role.can?(:manage_taxonomies)
end end
end end

View File

@ -2,6 +2,6 @@
class RelayPolicy < ApplicationPolicy class RelayPolicy < ApplicationPolicy
def update? def update?
admin? role.can?(:manage_federation)
end end
end end

View File

@ -2,11 +2,11 @@
class ReportNotePolicy < ApplicationPolicy class ReportNotePolicy < ApplicationPolicy
def create? def create?
staff? role.can?(:manage_reports)
end end
def destroy? def destroy?
admin? || owner? owner? || (role.can?(:manage_reports) && role.overrides?(record.account.user_role))
end end
private private

View File

@ -2,14 +2,14 @@
class ReportPolicy < ApplicationPolicy class ReportPolicy < ApplicationPolicy
def update? def update?
staff? role.can?(:manage_reports)
end end
def index? def index?
staff? role.can?(:manage_reports)
end end
def show? def show?
staff? role.can?(:manage_reports)
end end
end end

View File

@ -2,18 +2,18 @@
class RulePolicy < ApplicationPolicy class RulePolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_rules)
end end
def create? def create?
admin? role.can?(:manage_rules)
end end
def update? def update?
admin? role.can?(:manage_rules)
end end
def destroy? def destroy?
admin? role.can?(:manage_rules)
end end
end end

View File

@ -2,14 +2,14 @@
class SettingsPolicy < ApplicationPolicy class SettingsPolicy < ApplicationPolicy
def update? def update?
admin? role.can?(:manage_settings)
end end
def show? def show?
admin? role.can?(:manage_settings)
end end
def destroy? def destroy?
admin? role.can?(:manage_settings)
end end
end end

View File

@ -8,7 +8,7 @@ class StatusPolicy < ApplicationPolicy
end end
def index? def index?
staff? role.can?(:manage_reports, :manage_users)
end end
def show? def show?
@ -32,17 +32,17 @@ class StatusPolicy < ApplicationPolicy
end end
def destroy? def destroy?
staff? || owned? role.can?(:manage_reports) || owned?
end end
alias unreblog? destroy? alias unreblog? destroy?
def update? def update?
staff? || owned? role.can?(:manage_reports) || owned?
end end
def review? def review?
staff? role.can?(:manage_taxonomies)
end end
private private

View File

@ -2,18 +2,18 @@
class TagPolicy < ApplicationPolicy class TagPolicy < ApplicationPolicy
def index? def index?
staff? role.can?(:manage_taxonomies)
end end
def show? def show?
staff? role.can?(:manage_taxonomies)
end end
def update? def update?
staff? role.can?(:manage_taxonomies)
end end
def review? def review?
staff? role.can?(:manage_taxonomies)
end end
end end

View File

@ -2,52 +2,38 @@
class UserPolicy < ApplicationPolicy class UserPolicy < ApplicationPolicy
def reset_password? def reset_password?
staff? && !record.staff? role.can?(:manage_user_access) && role.overrides?(record.role)
end end
def change_email? def change_email?
staff? && !record.staff? role.can?(:manage_user_access) && role.overrides?(record.role)
end end
def disable_2fa? def disable_2fa?
admin? && !record.staff? role.can?(:manage_user_access) && role.overrides?(record.role)
end
def change_role?
role.can?(:manage_roles) && role.overrides?(record.role)
end end
def confirm? def confirm?
staff? && !record.confirmed? role.can?(:manage_user_access) && !record.confirmed?
end end
def enable? def enable?
staff? role.can?(:manage_users)
end end
def approve? def approve?
staff? && !record.approved? role.can?(:manage_users) && !record.approved?
end end
def reject? def reject?
staff? && !record.approved? role.can?(:manage_users) && !record.approved?
end end
def disable? def disable?
staff? && !record.admin? role.can?(:manage_users) && role.overrides?(record.role)
end
def promote?
admin? && promotable?
end
def demote?
admin? && !record.admin? && demoteable?
end
private
def promotable?
record.approved? && (!record.staff? || !record.admin?)
end
def demoteable?
record.staff?
end end
end end

View File

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

View File

@ -2,34 +2,34 @@
class WebhookPolicy < ApplicationPolicy class WebhookPolicy < ApplicationPolicy
def index? def index?
admin? role.can?(:manage_webhooks)
end end
def create? def create?
admin? role.can?(:manage_webhooks)
end end
def show? def show?
admin? role.can?(:manage_webhooks)
end end
def update? def update?
admin? role.can?(:manage_webhooks)
end end
def enable? def enable?
admin? role.can?(:manage_webhooks)
end end
def disable? def disable?
admin? role.can?(:manage_webhooks)
end end
def rotate_secret? def rotate_secret?
admin? role.can?(:manage_webhooks)
end end
def destroy? def destroy?
admin? role.can?(:manage_webhooks)
end end
end end

View File

@ -3,4 +3,8 @@
class InitialStatePresenter < ActiveModelSerializers::Model class InitialStatePresenter < ActiveModelSerializers::Model
attributes :settings, :push_subscription, :token, attributes :settings, :push_subscription, :token,
:current_account, :admin, :text, :visibility :current_account, :admin, :text, :visibility
def role
current_account&.user_role
end
end end

View File

@ -6,6 +6,7 @@ class InitialStateSerializer < ActiveModel::Serializer
:languages :languages
has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer
has_one :role, serializer: REST::RoleSerializer
def meta def meta
store = { store = {
@ -19,7 +20,6 @@ class InitialStateSerializer < ActiveModel::Serializer
repository: Mastodon::Version.repository, repository: Mastodon::Version.repository,
source_url: Mastodon::Version.source_url, source_url: Mastodon::Version.source_url,
version: Mastodon::Version.to_s, version: Mastodon::Version.to_s,
invites_enabled: Setting.min_invite_role == 'user',
limited_federation_mode: Rails.configuration.x.whitelist_mode, limited_federation_mode: Rails.configuration.x.whitelist_mode,
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory, profile_directory: Setting.profile_directory,
@ -39,7 +39,6 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:advanced_layout] = object.current_account.user.setting_advanced_layout store[:advanced_layout] = object.current_account.user.setting_advanced_layout
store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_blurhash] = object.current_account.user.setting_use_blurhash
store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:use_pending_items] = object.current_account.user.setting_use_pending_items
store[:is_staff] = object.current_account.user.staff?
store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:trends] = Setting.trends && object.current_account.user.setting_trends
store[:crop_images] = object.current_account.user.setting_crop_images store[:crop_images] = object.current_account.user.setting_crop_images
else else

View File

@ -3,6 +3,8 @@
class REST::CredentialAccountSerializer < REST::AccountSerializer class REST::CredentialAccountSerializer < REST::AccountSerializer
attributes :source attributes :source
has_one :role, serializer: REST::RoleSerializer
def source def source
user = object.user user = object.user
@ -15,4 +17,8 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer
follow_requests_count: FollowRequest.where(target_account: object).limit(40).count, follow_requests_count: FollowRequest.where(target_account: object).limit(40).count,
} }
end end
def role
object.user_role
end
end end

View File

@ -93,7 +93,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
end end
def invites_enabled def invites_enabled
Setting.min_invite_role == 'user' UserRole.everyone.can?(:invite_users)
end end
private private

View File

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

View File

@ -61,11 +61,11 @@ class AccountSearchService < BaseService
end end
def advanced_search_results def advanced_search_results
Account.advanced_search_for(terms_for_query, account, limit_for_non_exact_results, options[:following], offset) Account.advanced_search_for(terms_for_query, account, limit: limit_for_non_exact_results, following: options[:following], offset: offset)
end end
def simple_search_results def simple_search_results
Account.search_for(terms_for_query, limit_for_non_exact_results, offset) Account.search_for(terms_for_query, limit: limit_for_non_exact_results, offset: offset)
end end
def from_elasticsearch def from_elasticsearch

View File

@ -22,7 +22,7 @@ class AppealService < BaseService
end end
def notify_staff! def notify_staff!
User.staff.includes(:account).each do |u| User.those_who_can(:manage_appeals).includes(:account).each do |u|
AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails? AdminMailer.new_appeal(u.account, @appeal).deliver_later if u.allows_appeal_emails?
end end
end end

View File

@ -17,7 +17,7 @@ class BootstrapTimelineService < BaseService
end end
def notify_staff! def notify_staff!
User.staff.includes(:account).find_each do |user| User.those_who_can(:manage_users).includes(:account).find_each do |user|
LocalNotificationWorker.perform_async(user.account_id, @source_account.id, 'Account', 'admin.sign_up') LocalNotificationWorker.perform_async(user.account_id, @source_account.id, 'Account', 'admin.sign_up')
end end
end end

Some files were not shown because too many files have changed in this diff Show More