Add customizable user roles (#18641)

* Add customizable user roles

* Various fixes and improvements

* Add migration for old settings and fix tootctl role management
lolsob-rspec
Eugen Rochko 2022-07-05 02:41:40 +02:00 committed by GitHub
parent 6e31d2d520
commit e164d6a687
187 changed files with 1945 additions and 1032 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 demote
authorize @user, :demote?
@user.demote!
log_action :demote, @user
redirect_to admin_account_path(@user.account_id)
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 resource_params
params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: [])
end
end
end

View File

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

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

View File

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

View File

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

View File

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

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
module Admin
class TwoFactorAuthenticationsController < BaseController
class Users::TwoFactorAuthenticationsController < BaseController
before_action :set_target_user
def destroy

View File

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

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

View File

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

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

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

View File

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

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: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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')}` });

View File

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

View File

@ -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')}` });
}

View File

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

View File

@ -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')}` });

View File

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

View File

@ -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');

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,
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:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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_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])

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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