Add customizable user roles (#18641)

* Add customizable user roles

* Various fixes and improvements

* Add migration for old settings and fix tootctl role management
main
Eugen Rochko 2 years ago committed by GitHub
parent 1b4054256f
commit 44b2ee3485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .rubocop.yml
  2. 4
      app/controllers/admin/account_actions_controller.rb
  3. 2
      app/controllers/admin/accounts_controller.rb
  4. 5
      app/controllers/admin/action_logs_controller.rb
  5. 2
      app/controllers/admin/base_controller.rb
  6. 2
      app/controllers/admin/custom_emojis_controller.rb
  7. 4
      app/controllers/admin/dashboard_controller.rb
  8. 2
      app/controllers/admin/email_domain_blocks_controller.rb
  9. 2
      app/controllers/admin/follow_recommendations_controller.rb
  10. 2
      app/controllers/admin/ip_blocks_controller.rb
  11. 2
      app/controllers/admin/relationships_controller.rb
  12. 65
      app/controllers/admin/roles_controller.rb
  13. 2
      app/controllers/admin/statuses_controller.rb
  14. 20
      app/controllers/admin/subscriptions_controller.rb
  15. 4
      app/controllers/admin/trends/links/preview_card_providers_controller.rb
  16. 4
      app/controllers/admin/trends/links_controller.rb
  17. 4
      app/controllers/admin/trends/statuses_controller.rb
  18. 4
      app/controllers/admin/trends/tags_controller.rb
  19. 33
      app/controllers/admin/users/roles_controller.rb
  20. 2
      app/controllers/admin/users/two_factor_authentications_controller.rb
  21. 7
      app/controllers/api/v1/admin/account_actions_controller.rb
  22. 6
      app/controllers/api/v1/admin/accounts_controller.rb
  23. 6
      app/controllers/api/v1/admin/dimensions_controller.rb
  24. 2
      app/controllers/api/v1/admin/domain_allows_controller.rb
  25. 2
      app/controllers/api/v1/admin/domain_blocks_controller.rb
  26. 6
      app/controllers/api/v1/admin/measures_controller.rb
  27. 2
      app/controllers/api/v1/admin/reports_controller.rb
  28. 6
      app/controllers/api/v1/admin/retention_controller.rb
  29. 20
      app/controllers/api/v1/admin/trends/links_controller.rb
  30. 20
      app/controllers/api/v1/admin/trends/statuses_controller.rb
  31. 20
      app/controllers/api/v1/admin/trends/tags_controller.rb
  32. 10
      app/controllers/api/v1/trends/links_controller.rb
  33. 10
      app/controllers/api/v1/trends/statuses_controller.rb
  34. 12
      app/controllers/api/v1/trends/tags_controller.rb
  35. 13
      app/controllers/api/v2/admin/accounts_controller.rb
  36. 8
      app/controllers/application_controller.rb
  37. 2
      app/controllers/custom_css_controller.rb
  38. 14
      app/helpers/accounts_helper.rb
  39. 6
      app/javascript/mastodon/components/status_action_bar.js
  40. 1
      app/javascript/mastodon/containers/mastodon.js
  41. 9
      app/javascript/mastodon/features/account/components/header.js
  42. 10
      app/javascript/mastodon/features/notifications/components/column_settings.js
  43. 6
      app/javascript/mastodon/features/status/components/action_bar.js
  44. 9
      app/javascript/mastodon/features/ui/components/link_footer.js
  45. 2
      app/javascript/mastodon/initial_state.js
  46. 3
      app/javascript/mastodon/permissions.js
  47. 3
      app/javascript/mastodon/reducers/meta.js
  48. 15
      app/javascript/styles/mastodon/admin.scss
  49. 4
      app/javascript/styles/mastodon/forms.scss
  50. 6
      app/lib/admin/system_check.rb
  51. 10
      app/lib/admin/system_check/base_check.rb
  52. 4
      app/lib/admin/system_check/database_schema_check.rb
  53. 8
      app/lib/admin/system_check/elasticsearch_check.rb
  54. 4
      app/lib/admin/system_check/rules_check.rb
  55. 4
      app/lib/admin/system_check/sidekiq_process_check.rb
  56. 9
      app/models/account.rb
  57. 27
      app/models/account_filter.rb
  58. 68
      app/models/concerns/user_roles.rb
  59. 4
      app/models/form/admin_settings.rb
  60. 2
      app/models/trends.rb
  61. 38
      app/models/user.rb
  62. 179
      app/models/user_role.rb
  63. 4
      app/policies/account_moderation_note_policy.rb
  64. 40
      app/policies/account_policy.rb
  65. 2
      app/policies/account_warning_policy.rb
  66. 8
      app/policies/account_warning_preset_policy.rb
  67. 8
      app/policies/announcement_policy.rb
  68. 8
      app/policies/appeal_policy.rb
  69. 6
      app/policies/application_policy.rb
  70. 7
      app/policies/audit_log_policy.rb
  71. 14
      app/policies/custom_emoji_policy.rb
  72. 7
      app/policies/dashboard_policy.rb
  73. 6
      app/policies/delivery_policy.rb
  74. 8
      app/policies/domain_allow_policy.rb
  75. 10
      app/policies/domain_block_policy.rb
  76. 6
      app/policies/email_domain_block_policy.rb
  77. 6
      app/policies/follow_recommendation_policy.rb
  78. 6
      app/policies/instance_policy.rb
  79. 12
      app/policies/invite_policy.rb
  80. 6
      app/policies/ip_block_policy.rb
  81. 4
      app/policies/preview_card_policy.rb
  82. 4
      app/policies/preview_card_provider_policy.rb
  83. 2
      app/policies/relay_policy.rb
  84. 4
      app/policies/report_note_policy.rb
  85. 6
      app/policies/report_policy.rb
  86. 8
      app/policies/rule_policy.rb
  87. 6
      app/policies/settings_policy.rb
  88. 8
      app/policies/status_policy.rb
  89. 8
      app/policies/tag_policy.rb
  90. 38
      app/policies/user_policy.rb
  91. 19
      app/policies/user_role_policy.rb
  92. 16
      app/policies/webhook_policy.rb
  93. 4
      app/presenters/initial_state_presenter.rb
  94. 3
      app/serializers/initial_state_serializer.rb
  95. 6
      app/serializers/rest/credential_account_serializer.rb
  96. 2
      app/serializers/rest/instance_serializer.rb
  97. 13
      app/serializers/rest/role_serializer.rb
  98. 4
      app/services/account_search_service.rb
  99. 2
      app/services/appeal_service.rb
  100. 2
      app/services/bootstrap_timeline_service.rb
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -5,11 +5,15 @@ module Admin
before_action :set_account
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)
@warning_presets = AccountWarningPreset.all
end
def create
authorize @account, :show?
account_action = Admin::AccountAction.new(resource_params)
account_action.target_account = @account
account_action.current_account = current_account

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

@ -4,7 +4,10 @@ module Admin
class ActionLogsController < BaseController
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

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

@ -29,6 +29,8 @@ module Admin
end
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.save
rescue ActionController::ParameterMissing

@ -5,7 +5,9 @@ module Admin
include Redisable
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)
@pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count

@ -12,6 +12,8 @@ module Admin
end
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.save
rescue ActionController::ParameterMissing

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

@ -29,6 +29,8 @@ module Admin
end
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.save
rescue ActionController::ParameterMissing

@ -7,7 +7,7 @@ module Admin
PER_PAGE = 40
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)
@form = Form::AccountBatch.new

@ -2,20 +2,63 @@
module Admin
class RolesController < BaseController
before_action :set_user
before_action :set_role, except: [:index, :new, :create]
def promote
authorize @user, :promote?
@user.promote!
log_action :promote, @user
redirect_to admin_account_path(@user.account_id)
def index
authorize :user_role, :index?
@roles = UserRole.order(position: :desc).page(params[:page])
end
def new
authorize :user_role, :create?
@role = UserRole.new
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 demote
authorize @user, :demote?
@user.demote!
log_action :demote, @user
redirect_to admin_account_path(@user.account_id)
def resource_params
params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: [])
end
end
end

@ -14,6 +14,8 @@ module Admin
end
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.save!
rescue ActionController::ParameterMissing

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

@ -2,13 +2,15 @@
class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController
def index
authorize :preview_card_provider, :index?
authorize :preview_card_provider, :review?
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
@form = Trends::PreviewCardProviderBatch.new
end
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.save
rescue ActionController::ParameterMissing

@ -2,13 +2,15 @@
class Admin::Trends::LinksController < Admin::BaseController
def index
authorize :preview_card, :index?
authorize :preview_card, :review?
@preview_cards = filtered_preview_cards.page(params[:page])
@form = Trends::PreviewCardBatch.new
end
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.save
rescue ActionController::ParameterMissing

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

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

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

@ -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:write', :'admin:write:accounts' }, except: [:index, :show]
before_action :require_staff!
before_action :set_accounts, only: :index
before_action :set_account, except: :index
before_action :require_local_account!, only: [:enable, :approve, :reject]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(
@ -119,7 +119,9 @@ class Api::V1::Admin::AccountsController < Api::BaseController
translated_params[:status] = status.to_s if params[status].present?
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
end

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

@ -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:write', :'admin:write:domain_allows' }, except: [:index, :show]
before_action :require_staff!
before_action :set_domain_allows, only: :index
before_action :set_domain_allow, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze

@ -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:write', :'admin:write:domain_blocks' }, except: [:index, :show]
before_action :require_staff!
before_action :set_domain_blocks, only: :index
before_action :set_domain_block, only: [:show, :update, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze

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

@ -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:write', :'admin:write:reports' }, except: [:index, :show]
before_action :require_staff!
before_action :set_reports, only: :index
before_action :set_report, except: :index
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
FILTER_PARAMS = %i(

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

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

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

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

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

@ -11,6 +11,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
email
ip
invited_by
role_ids
).freeze
PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze
@ -18,7 +19,17 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private
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
def filter_params

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

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

@ -61,21 +61,13 @@ module AccountsHelper
end
end
def account_badge(account, all: false)
def account_badge(account)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
elsif account.group?
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
content_tag(:div, class: 'roles') do
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
elsif account.user_role&.highlighted?
content_tag(:div, content_tag(:div, account.user_role.name, class: "account-role user-role-#{account.user_role.id}"), class: 'roles')
end
end

@ -6,8 +6,9 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me, isStaff } from '../initial_state';
import { me } from '../initial_state';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -55,6 +56,7 @@ class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};
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({ 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')}` });

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

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Button from 'mastodon/components/button';
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 Icon from 'mastodon/components/icon';
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 DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -64,6 +65,10 @@ const dateFormatOptions = {
export default @injectIntl
class Header extends ImmutablePureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = {
account: ImmutablePropTypes.map,
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({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
}

@ -5,10 +5,14 @@ import { FormattedMessage } from 'react-intl';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
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 {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
pushSettings: ImmutablePropTypes.map.isRequired,
@ -166,7 +170,7 @@ export default class ColumnSettings extends React.PureComponent {
</div>
</div>
{isStaff && (
{(this.context.identity.permissions & PERMISSION_MANAGE_USERS === PERMISSION_MANAGE_USERS) && (
<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>
@ -179,7 +183,7 @@ export default class ColumnSettings extends React.PureComponent {
</div>
)}
{isStaff && (
{(this.context.identity.permissions & PERMISSION_MANAGE_REPORTS === PERMISSION_MANAGE_REPORTS) && (
<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>

@ -5,8 +5,9 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me, isStaff } from '../../../initial_state';
import { me } from '../../../initial_state';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
@ -50,6 +51,7 @@ class ActionBar extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};
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({ 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')}` });

@ -3,9 +3,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
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 { openModal } from 'mastodon/actions/modal';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
const messages = defineMessages({
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
@ -27,6 +28,10 @@ export default @injectIntl
@connect(null, mapDispatchToProps)
class LinkFooter extends React.PureComponent {
static contextTypes = {
identity: PropTypes.object,
};
static propTypes = {
withHotkeys: PropTypes.bool,
onLogout: PropTypes.func.isRequired,
@ -48,7 +53,7 @@ class LinkFooter extends React.PureComponent {
return (
<div className='getting-started__footer'>
<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>}
<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>}

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

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

@ -7,12 +7,13 @@ const initialState = ImmutableMap({
streaming_api_base_url: null,
access_token: null,
layout: layoutFromWindow(),
permissions: '0',
});
export default function meta(state = initialState, action) {
switch(action.type) {
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:
return state.set('layout', action.layout);
default:

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

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

@ -8,11 +8,11 @@ class Admin::SystemCheck
Admin::SystemCheck::ElasticsearchCheck,
].freeze
def self.perform
def self.perform(current_user)
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
else
arr << check.message

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

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

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

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

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

@ -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_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 :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_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_or_pending?,
:role,
:admin?,
:moderator?,
:staff?,
:locale,
:shows_application?,
to: :user,
@ -454,7 +451,7 @@ class Account < ApplicationRecord
DeliveryFailureTracker.without_unavailable(urls)
end
def search_for(terms, limit = 10, offset = 0)
def search_for(terms, limit: 10, offset: 0)
tsquery = generate_query_for_search(terms)
sql = <<-SQL.squish
@ -476,7 +473,7 @@ class Account < ApplicationRecord
records
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)
sql = advanced_search_for_sql_template(following)
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])

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

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

@ -15,10 +15,8 @@ class Form::AdminSettings
closed_registrations_message
open_deletion
timeline_preview
show_staff_badge
bootstrap_timeline_accounts
theme
min_invite_role
activity_api_enabled
peers_api_enabled
show_known_fediverse_at_about_page
@ -39,7 +37,6 @@ class Form::AdminSettings
BOOLEAN_KEYS = %i(
open_deletion
timeline_preview
show_staff_badge
activity_api_enabled
peers_api_enabled
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_extended_description, :site_terms, :closed_registrations_message, html: true
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_username, existing_username: true
validates :bootstrap_timeline_accounts, existing_username: { multiple: true }

@ -34,7 +34,7 @@ module Trends
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?
end
end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,52 +2,38 @@
class UserPolicy < ApplicationPolicy
def reset_password?
staff? && !record.staff?
role.can?(:manage_user_access) && role.overrides?(record.role)
end
def change_email?
staff? && !record.staff?
role.can?(:manage_user_access) && role.overrides?(record.role)
end
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
def confirm?
staff? && !record.confirmed?
role.can?(:manage_user_access) && !record.confirmed?
end
def enable?
staff?
role.can?(:manage_users)
end
def approve?
staff? && !record.approved?
role.can?(:manage_users) && !record.approved?
end
def reject?
staff? && !record.approved?
role.can?(:manage_users) && !record.approved?
end
def disable?
staff? && !record.admin?
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?
role.can?(:manage_users) && role.overrides?(record.role)
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

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

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

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

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

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

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

@ -61,11 +61,11 @@ class AccountSearchService < BaseService
end
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
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
def from_elasticsearch

@ -22,7 +22,7 @@ class AppealService < BaseService
end
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?
end
end

@ -17,7 +17,7 @@ class BootstrapTimelineService < BaseService
end
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')
end
end

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

Loading…
Cancel
Save