Add customizable user roles (#18641)

* Add customizable user roles

* Various fixes and improvements

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

View File

@ -67,7 +67,7 @@ Lint/UselessAccessModifier:
- class_methods
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

View File

@ -76,7 +76,7 @@ class NotifyService < BaseService
end
def from_staff?
@notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user.staff?
@notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user_role&.overrides?(@recipient.user_role)
end
def optional_non_following_and_direct?

View File

@ -38,7 +38,7 @@ class ReportService < BaseService
def notify_staff!
return if @report.unresolved_siblings?
User.staff.includes(:account).each do |u|
User.those_who_can(:manage_reports).includes(:account).each do |u|
LocalNotificationWorker.perform_async(u.account_id, @report.id, 'Report', 'admin.report')
AdminMailer.new_report(u.account, @report).deliver_later if u.allows_report_emails?
end

View File

@ -4,45 +4,36 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
.filters
.filter-subset
%strong= t('admin.accounts.location.title')
%ul
%li= filter_link_to t('generic.all'), origin: nil
%li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
%li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
.filter-subset
%strong= t('admin.accounts.moderation.title')
%ul
%li= filter_link_to t('generic.all'), status: nil
%li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
%li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
%li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
.filter-subset
%strong= t('admin.accounts.role')
%ul
%li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
%li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
.filter-subset
%strong= t 'generic.order_by'
%ul
%li= filter_link_to t('relationships.most_recent'), order: nil
%li= filter_link_to t('relationships.last_active'), order: 'active'
= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
.fields-group
- (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
.filters
.filter-subset.filter-subset--with-select
%strong= t('admin.accounts.location.title')
.input.select.optional
= select_tag :origin, options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), prompt: I18n.t('generic.all')
.filter-subset.filter-subset--with-select
%strong= t('admin.accounts.moderation.title')
.input.select.optional
= select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
.filter-subset.filter-subset--with-select
%strong= t('admin.accounts.role')
.input.select.optional
= select_tag :role_ids, options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), prompt: I18n.t('admin.accounts.moderation.all')
.filter-subset.filter-subset--with-select
%strong= t 'generic.order_by'
.input.select
= select_tag :order, options_for_select([[t('relationships.most_recent'), nil], [t('relationships.last_active'), 'active']], params[:order])
.fields-group
- %i(username by_domain display_name email ip).each do |key|
- unless key == :by_domain && params[:origin] != 'remote'
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
.actions
%button.button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
.actions
%button.button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
%hr.spacer/
= form_for(@form, url: batch_admin_accounts_path) do |f|
= hidden_field_tag :page, params[:page] || 1

View File

@ -92,10 +92,13 @@
%tr
%th= t('admin.accounts.role')
%td= t("admin.accounts.roles.#{@account.user&.role}")
%td
= table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
= table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
- if @account.user_role&.everyone?
= t('admin.accounts.no_role_assigned')
- else
= @account.user_role&.name
%td
= table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(@account.user) if can?(:change_role, @account.user)
%tr
%th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')

View File

@ -11,7 +11,7 @@
.filter-subset.filter-subset--with-select
%strong= t('admin.action_logs.filter_by_user')
.input.select.optional
= select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
= select_tag :account_id, options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
.filter-subset.filter-subset--with-select
%strong= t('admin.action_logs.filter_by_action')

View File

@ -4,32 +4,33 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
- content_for :heading_actions do
= l(@time_period.first)
= ' - '
= l(@time_period.last)
- if current_user.can?(:view_dashboard)
- content_for :heading_actions do
= l(@time_period.first)
= ' - '
= l(@time_period.last)
%p
= fa_icon 'info fw'
= t('admin.instances.totals_time_period_hint_html')
%p
= fa_icon 'info fw'
= t('admin.instances.totals_time_period_hint_html')
.dashboard
.dashboard__item
= react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
.dashboard__item
= react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
.dashboard__item
= react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
.dashboard__item
= react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
.dashboard
.dashboard__item
= react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
.dashboard__item
= react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
.dashboard__item
= react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
.dashboard__item
= react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
.dashboard__item
= react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
%hr.spacer/

View File

@ -0,0 +1,37 @@
= simple_form_for @role, url: @role.new_record? ? admin_roles_path : admin_role_path(@role) do |f|
= render 'shared/error_messages', object: @role
- if @role.everyone?
.flash-message.info
= t('admin.roles.everyone_full_description_html')
- else
.fields-group
= f.input :name, wrapper: :with_label
.fields-group
= f.input :position, wrapper: :with_label
.fields-group
= f.input :color, wrapper: :with_label, input_html: { placeholder: '#000000' }
%hr.spacer/
.fields-group
= f.input :highlighted, wrapper: :with_label
%hr.spacer/
.field-group
.input.with_block_label
%label= t('simple_form.labels.user_role.permissions_as_keys')
%span.hint= t('simple_form.hints.user_role.permissions_as_keys')
- (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
%h4= t(category, scope: 'admin.roles.categories')
= f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: lambda { |privilege| safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false
%hr.spacer/
.actions
= f.button :button, @role.new_record? ? t('admin.roles.add_new') : t('generic.save_changes'), type: :submit

View File

@ -0,0 +1,18 @@
.announcements-list__item
= link_to edit_admin_role_path(role), class: 'announcements-list__item__title' do
%span.user-role{ class: "user-role-#{role.id}" }
= fa_icon 'users fw'
- if role.everyone?
= t('admin.roles.everyone')
- else
= role.name
.announcements-list__item__action-bar
.announcements-list__item__meta
- if role.everyone?
= t('admin.roles.everyone_full_description_html')
- else
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_id: role.id)
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)

View File

@ -0,0 +1,8 @@
- content_for :page_title do
= t('admin.roles.edit', name: @role.everyone? ? t('admin.roles.everyone') : @role.name)
- content_for :heading_actions do
= link_to t('admin.roles.delete'), admin_role_path(@role), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:destroy, @role)
= render partial: 'form'

View File

@ -0,0 +1,17 @@
- content_for :page_title do
= t('admin.roles.title')
- content_for :heading_actions do
= link_to t('admin.roles.add_new'), new_admin_role_path, class: 'button' if can?(:create, :user_role)
%p= t('admin.roles.description_html')
%hr.spacer/
.applications-list
= render partial: 'role', collection: @roles.select(&:everyone?)
%hr.spacer/
.applications-list
= render partial: 'role', collection: @roles.reject(&:everyone?)

View File

@ -0,0 +1,4 @@
- content_for :page_title do
= t('admin.roles.add_new')
= render partial: 'form'

View File

@ -61,9 +61,6 @@
.fields-group
= f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
.fields-group
= f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
.fields-group
= f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
@ -91,9 +88,6 @@
%hr.spacer/
.fields-group
= f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'

View File

@ -4,49 +4,50 @@
- content_for :page_title do
= "##{@tag.name}"
- content_for :heading_actions do
= l(@time_period.first)
= ' - '
= l(@time_period.last)
- if current_user.can?(:view_dashboard)
- content_for :heading_actions do
= l(@time_period.first)
= ' - '
= l(@time_period.last)
.dashboard
.dashboard__item
= react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
.dashboard__item
= react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
.dashboard__item
= react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
.dashboard__item
= react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
.dashboard__item
= react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
.dashboard__item
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
- if @tag.usable?
%span= t('admin.trends.tags.usable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_usable')
= fa_icon 'lock fw'
.dashboard
.dashboard__item
= react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
.dashboard__item
= react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
.dashboard__item
= react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
.dashboard__item
= react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
.dashboard__item
= react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
.dashboard__item
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
- if @tag.usable?
%span= t('admin.trends.tags.usable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_usable')
= fa_icon 'lock fw'
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
- if @tag.trendable?
%span= t('admin.trends.tags.trendable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_trendable')
= fa_icon 'lock fw'
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
- if @tag.trendable?
%span= t('admin.trends.tags.trendable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_trendable')
= fa_icon 'lock fw'
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
- if @tag.listable?
%span= t('admin.trends.tags.listable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_listable')
= fa_icon 'lock fw'
= link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
- if @tag.listable?
%span= t('admin.trends.tags.listable')
= fa_icon 'check fw'
- else
%span= t('admin.trends.tags.not_listable')
= fa_icon 'lock fw'
%hr.spacer/
%hr.spacer/
= simple_form_for @tag, url: admin_tag_path(@tag.id) do |f|
= render 'shared/error_messages', object: @tag

View File

@ -0,0 +1,9 @@
- content_for :page_title do
= t('admin.accounts.change_role.title', username: @user.account.username)
= simple_form_for @user, url: admin_user_role_path(@user) do |f|
.fields-group
= f.association :role, wrapper: :with_block_label, collection: UserRole.assignable, label_method: :name, include_blank: I18n.t('admin.accounts.change_role.no_role')
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View File

@ -0,0 +1,10 @@
<%- if Setting.custom_css.present? %>
<%= Setting.custom_css %>
<%- end %>
<%- UserRole.where(highlighted: true).select { |role| role.color.present? }.each do |role| %>
.user-role-<%= role.id %> {
--user-role-accent: <%= role.color %>;
}
<%- end %>

View File

@ -34,9 +34,7 @@
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
= stylesheet_link_tag '/inert.css', skip_pipeline: true, media: 'all', id: 'inert-style'
- if Setting.custom_css.present?
= stylesheet_link_tag custom_css_path, host: request.host, media: 'all'
= stylesheet_link_tag custom_css_path, host: default_url_options[:host], media: 'all'
= yield :header_tags

View File

@ -18,12 +18,10 @@
= ff.input :reblog, as: :boolean, wrapper: :with_label
= ff.input :favourite, as: :boolean, wrapper: :with_label
= ff.input :mention, as: :boolean, wrapper: :with_label
- if current_user.staff?
= ff.input :report, as: :boolean, wrapper: :with_label
= ff.input :appeal, as: :boolean, wrapper: :with_label
= ff.input :pending_account, as: :boolean, wrapper: :with_label
= ff.input :trending_tag, as: :boolean, wrapper: :with_label
= ff.input :report, as: :boolean, wrapper: :with_label if current_user.can?(:manage_reports)
= ff.input :appeal, as: :boolean, wrapper: :with_label if current_user.can?(:manage_appeals)
= ff.input :pending_account, as: :boolean, wrapper: :with_label if current_user.can?(:manage_users)
= ff.input :trending_tag, as: :boolean, wrapper: :with_label if current_user.can?(:manage_taxonomies)
.fields-group
= f.input :setting_always_send_emails, as: :boolean, wrapper: :with_label

View File

@ -44,6 +44,7 @@ require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'
Dotenv::Railtie.load

View File

@ -38,3 +38,12 @@ en:
email:
blocked: uses a disallowed e-mail provider
unreachable: does not seem to exist
role_id:
elevated: cannot be higher than your current role
user_role:
attributes:
permissions_as_keys:
dangerous: include permissions that are not safe for the base role
elevated: cannot include permissions your current role does not possess
position:
elevated: cannot be higher than your current role

View File

@ -83,10 +83,8 @@ en:
posts_tab_heading: Posts
posts_with_replies: Posts and replies
roles:
admin: Admin
bot: Bot
group: Group
moderator: Mod
unavailable: Profile unavailable
unfollow: Unfollow
admin:
@ -105,12 +103,17 @@ en:
avatar: Avatar
by_domain: Domain
change_email:
changed_msg: Account email successfully changed!
changed_msg: Email successfully changed!
current_email: Current email
label: Change email
new_email: New email
submit: Change email
title: Change email for %{username}
change_role:
changed_msg: Role successfully changed!
label: Change role
no_role: No role
title: Change role for %{username}
confirm: Confirm
confirmed: Confirmed
confirming: Confirming
@ -154,6 +157,7 @@ en:
active: Active
all: All
pending: Pending
silenced: Limited
suspended: Suspended
title: Moderation
moderation_notes: Moderation notes
@ -161,6 +165,7 @@ en:
most_recent_ip: Most recent IP
no_account_selected: No accounts were changed as none were selected
no_limits_imposed: No limits imposed
no_role_assigned: No role assigned
not_subscribed: Not subscribed
pending: Pending review
perform_full_suspension: Suspend
@ -187,12 +192,7 @@ en:
reset: Reset
reset_password: Reset password
resubscribe: Resubscribe
role: Permissions
roles:
admin: Administrator
moderator: Moderator
staff: Staff
user: User
role: Role
search: Search
search_same_email_domain: Other users with the same e-mail domain
search_same_ip: Other users with the same IP
@ -649,6 +649,67 @@ en:
unresolved: Unresolved
updated_at: Updated
view_profile: View profile
roles:
add_new: Add role
assigned_users:
one: "%{count} user"
other: "%{count} users"
categories:
administration: Administration
devops: Devops
invites: Invites
moderation: Moderation
special: Special
delete: Delete
description_html: With <strong>user roles</strong>, you can customize which functions and areas of Mastodon your users can access.
edit: Edit '%{name}' role
everyone: Default permissions
everyone_full_description_html: This is the <strong>base role</strong> affecting <strong>all users</strong>, even those without an assigned role. All other roles inherit permissions from it.
permissions_count:
one: "%{count} permission"
other: "%{count} permissions"
privileges:
administrator: Administrator
administrator_description: Users with this permission will bypass every permission
delete_user_data: Delete User Data
delete_user_data_description: Allows users to delete other users' data without delay
invite_users: Invite Users
invite_users_description: Allows users to invite new people to the server
manage_announcements: Manage Announcements
manage_announcements_description: Allows users to manage announcements on the server
manage_appeals: Manage Appeals
manage_appeals_description: Allows users to review appeals against moderation actions
manage_blocks: Manage Blocks
manage_blocks_description: Allows users to block e-mail providers and IP addresses
manage_custom_emojis: Manage Custom Emojis
manage_custom_emojis_description: Allows users to manage custom emojis on the server
manage_federation: Manage Federation
manage_federation_description: Allows users to block or allow federation with other domains, and control deliverability
manage_invites: Manage Invites
manage_invites_description: Allows users to browse and deactivate invite links
manage_reports: Manage Reports
manage_reports_description: Allows users to review reports and perform moderation actions against them
manage_roles: Manage Roles
manage_roles_description: Allows users to manage and assign roles below theirs
manage_rules: Manage Rules
manage_rules_description: Allows users to change server rules
manage_settings: Manage Settings
manage_settings_description: Allows users to change site settings
manage_taxonomies: Manage Taxonomies
manage_taxonomies_description: Allows users to review trending content and update hashtag settings
manage_user_access: Manage User Access
manage_user_access_description: Allows users to disable other users' two-factor authentication, change their e-mail address, and reset their password
manage_users: Manage Users
manage_users_description: Allows users to view other users' details and perform moderation actions against them
manage_webhooks: Manage Webhooks
manage_webhooks_description: Allows users to set up webhooks for administrative events
view_audit_log: View Audit Log
view_audit_log_description: Allows users to see a history of administrative actions on the server
view_dashboard: View Dashboard
view_dashboard_description: Allows users to access the dashboard and various metrics
view_devops: Devops
view_devops_description: Allows users to access Sidekiq and pgHero dashboards
title: Roles
rules:
add_new: Add rule
delete: Delete
@ -701,9 +762,6 @@ en:
deletion:
desc_html: Allow anyone to delete their account
title: Open account deletion
min_invite_role:
disabled: No one
title: Allow invitations by
require_invite_text:
desc_html: When registrations require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
title: Require new users to enter a reason to join
@ -716,9 +774,6 @@ en:
show_known_fediverse_at_about_page:
desc_html: When disabled, restricts the public timeline linked from the landing page to showing only local content
title: Include federated content on unauthenticated public timeline page
show_staff_badge:
desc_html: Show a staff badge on a user page
title: Show staff badge
site_description:
desc_html: Introductory paragraph on the API. Describe what makes this Mastodon server special and anything else important. You can use HTML tags, in particular <code>&lt;a&gt;</code> and <code>&lt;em&gt;</code>.
title: Server description

View File

@ -96,6 +96,13 @@ en:
name: You can only change the casing of the letters, for example, to make it more readable
user:
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
role: The role controls which permissions the user has
user_role:
color: Color to be used for the role throughout the UI, as RGB in hex format
highlighted: This makes the role publicly visible
name: Public name of the role, if role is set to be displayed as a badge
permissions_as_keys: Users with this role will have access to...
position: Higher role decides conflict resolution in certain situations
webhook:
events: Select events to send
url: Where events will be sent to
@ -232,6 +239,14 @@ en:
name: Hashtag
trendable: Allow this hashtag to appear under trends
usable: Allow posts to use this hashtag
user:
role: Role
user_role:
color: Badge color
highlighted: Display role as badge on user profiles
name: Name
permissions_as_keys: Permissions
position: Priority
webhook:
events: Enabled events
url: Endpoint URL

View File

@ -2,66 +2,67 @@
SimpleNavigation::Configuration.run do |navigation|
navigation.items do |n|
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url
n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s|
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url
s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? } do |s|
s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_path
s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_path
end
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_url, if: -> { current_user.functional? } do |s|
s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_url
s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_url
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url
n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s|
s.item :appearance, safe_join([fa_icon('desktop fw'), t('settings.appearance')]), settings_preferences_appearance_path
s.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_preferences_notifications_path
s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_path
end
n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, if: -> { current_user.functional? }
n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? }
n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }
n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional? }
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s|
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_url, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url
n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_path do |s|
s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes}
s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_methods_path, highlights_on: %r{/settings/two_factor_authentication|/settings/otp_authentication|/settings/security_keys}
s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_path
end
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url do |s|
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url, if: -> { current_user.functional? }
s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url
n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_path do |s|
s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_path, if: -> { current_user.functional? }
s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_path
end
n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' && current_user.functional? }
n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url, if: -> { current_user.functional? }
n.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: -> { current_user.can?(:invite_users) && current_user.functional? }
n.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_path, if: -> { current_user.functional? }
n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_tags_path, if: proc { current_user.staff? } do |s|
n.item :trends, safe_join([fa_icon('fire fw'), t('admin.trends.title')]), admin_trends_statuses_path, if: -> { current_user.can?(:manage_taxonomies) } do |s|
s.item :statuses, safe_join([fa_icon('comments-o fw'), t('admin.trends.statuses.title')]), admin_trends_statuses_path, highlights_on: %r{/admin/trends/statuses}
s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.trends.tags.title')]), admin_trends_tags_path, highlights_on: %r{/admin/tags|/admin/trends/tags}
s.item :links, safe_join([fa_icon('newspaper-o fw'), t('admin.trends.links.title')]), admin_trends_links_path, highlights_on: %r{/admin/trends/links}
end
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), admin_reports_url, if: proc { current_user.staff? } do |s|
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports}
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes}
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_url, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.admin? }
n.item :moderation, safe_join([fa_icon('gavel fw'), t('moderation.title')]), nil, if: -> { current_user.can?(:manage_reports, :view_audit_log, :manage_users, :manage_invites, :manage_taxonomies, :manage_federation, :manage_blocks) } do |s|
s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_path, highlights_on: %r{/admin/reports}, if: -> { current_user.can?(:manage_reports) }
s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) }
s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) }
s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}, if: -> { current_user.can?(:manage_taxonomies) }
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_path(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) }
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }
end
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |s|
s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_url
s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_url, if: -> { current_user.admin? }, highlights_on: %r{/admin/settings}
s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}
s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}
s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_url, highlights_on: %r{/admin/custom_emojis}
s.item :webhooks, safe_join([fa_icon('inbox fw'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}
s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_url, if: -> { current_user.admin? && !whitelist_mode? }, highlights_on: %r{/admin/relays}
s.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }, if: -> { current_user.admin? }
s.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }, if: -> { current_user.admin? }
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), nil, if: -> { current_user.can?(:view_dashboard, :manage_settings, :manage_rules, :manage_announcements, :manage_custom_emojis, :manage_webhooks, :manage_federation) } do |s|
s.item :dashboard, safe_join([fa_icon('tachometer fw'), t('admin.dashboard.title')]), admin_dashboard_path, if: -> { current_user.can?(:view_dashboard) }
s.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), edit_admin_settings_path, if: -> { current_user.can?(:manage_settings) }, highlights_on: %r{/admin/settings}
s.item :rules, safe_join([fa_icon('gavel fw'), t('admin.rules.title')]), admin_rules_path, highlights_on: %r{/admin/rules}, if: -> { current_user.can?(:manage_rules) }
s.item :roles, safe_join([fa_icon('vcard fw'), t('admin.roles.title')]), admin_roles_path, highlights_on: %r{/admin/roles}, if: -> { current_user.can?(:manage_roles) }
s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) }
s.item :webhooks, safe_join([fa_icon('inbox fw'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) }
s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !whitelist_mode? && current_user.can?(:manage_federation) }
end
n.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_url, link_html: { 'data-method' => 'delete' }
n.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_path, link_html: { target: 'sidekiq' }, if: -> { current_user.can?(:view_devops) }
n.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_path, link_html: { target: 'pghero' }, if: -> { current_user.can?(:view_devops) }
n.item :logout, safe_join([fa_icon('sign-out fw'), t('auth.logout')]), destroy_user_session_path, link_html: { 'data-method' => 'delete' }
end
end

35
config/roles.yml Normal file
View File

@ -0,0 +1,35 @@
moderator:
name: Moderator
position: 10
permissions:
- view_dashboard
- view_audit_log
- manage_users
- manage_reports
- manage_taxonomies
admin:
name: Admin
position: 100
permissions:
- view_dashboard
- view_audit_log
- manage_users
- manage_user_access
- delete_user_data
- manage_reports
- manage_taxonomies
- manage_federation
- manage_settings
- manage_blocks
- manage_appeals
- manage_rules
- manage_invites
- manage_announcements
- manage_custom_emojis
- manage_webhooks
- manage_roles
owner:
name: Owner
position: 1000
permissions:
- administrator

View File

@ -10,7 +10,7 @@ Rails.application.routes.draw do
get 'health', to: 'health#show'
authenticate :user, lambda { |u| u.admin? } do
authenticate :user, lambda { |u| u.role&.can?(:view_devops) } do
mount Sidekiq::Web, at: 'sidekiq', as: :sidekiq
mount PgHero::Engine, at: 'pghero', as: :pghero
end
@ -295,17 +295,11 @@ Rails.application.routes.draw do
post :resend
end
end
resource :role, only: [] do
member do
post :promote
post :demote
end
end
end
resources :users, only: [] do
resource :two_factor_authentication, only: [:destroy]
resource :two_factor_authentication, only: [:destroy], controller: 'users/two_factor_authentications'
resource :role, only: [:show, :update], controller: 'users/roles'
end
resources :custom_emojis, only: [:index, :new, :create] do
@ -320,6 +314,7 @@ Rails.application.routes.draw do
end
end
resources :roles, except: [:show]
resources :account_moderation_notes, only: [:create, :destroy]
resource :follow_recommendations, only: [:show, :update]
resources :tags, only: [:show, :update]

View File

@ -0,0 +1,13 @@
class CreateUserRoles < ActiveRecord::Migration[6.1]
def change
create_table :user_roles do |t|
t.string :name, null: false, default: ''
t.string :color, null: false, default: ''
t.integer :position, null: false, default: 0
t.bigint :permissions, null: false, default: 0
t.boolean :highlighted, null: false, default: false
t.timestamps
end
end
end

View File

@ -0,0 +1,8 @@
class AddRoleIdToUsers < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
safety_assured { add_reference :users, :role, foreign_key: { to_table: 'user_roles', on_delete: :nullify }, index: false }
add_index :users, :role_id, algorithm: :concurrently, where: 'role_id IS NOT NULL'
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class MigrateRoles < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
class UserRole < ApplicationRecord; end
class User < ApplicationRecord; end
def up
load Rails.root.join('db', 'seeds', '03_roles.rb')
admin_role = UserRole.find_by(name: 'Admin')
moderator_role = UserRole.find_by(name: 'Moderator')
User.where(admin: true).in_batches.update_all(role_id: admin_role.id)
User.where(moderator: true).in_batches.update_all(role_id: moderator_role.id)
end
def down
admin_role = UserRole.find_by(name: 'Admin')
moderator_role = UserRole.find_by(name: 'Moderator')
User.where(role_id: admin_role.id).in_batches.update_all(admin: true) if admin_role
User.where(role_id: moderator_role.id).in_batches.update_all(moderator: true) if moderator_role
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
class MigrateSettingsToUserRoles < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
class UserRole < ApplicationRecord; end
def up
owner_role = UserRole.find_by(name: 'Owner')
admin_role = UserRole.find_by(name: 'Admin')
moderator_role = UserRole.find_by(name: 'Moderator')
everyone_role = UserRole.find_by(id: -99)
min_invite_role = Setting.min_invite_role
show_staff_badge = Setting.show_staff_badge
if everyone_role
everyone_role.permissions &= ~::UserRole::FLAGS[:invite_users] unless min_invite_role == 'user'
everyone_role.save
end
if owner_role
owner_role.highlighted = show_staff_badge
owner_role.save
end
if admin_role
admin_role.permissions |= ::UserRole::FLAGS[:invite_users] if %w(admin moderator).include?(min_invite_role)
admin_role.highlighted = show_staff_badge
admin_role.save
end
if moderator_role
moderator_role.permissions |= ::UserRole::FLAGS[:invite_users] if %w(moderator).include?(min_invite_role)
moderator_role.highlighted = show_staff_badge
moderator_role.save
end
end
def down; end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_06_13_110903) do
ActiveRecord::Schema.define(version: 2022_07_04_024901) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -968,6 +968,16 @@ ActiveRecord::Schema.define(version: 2022_06_13_110903) do
t.index ["user_id"], name: "index_user_invite_requests_on_user_id"
end
create_table "user_roles", force: :cascade do |t|
t.string "name", default: "", null: false
t.string "color", default: "", null: false
t.integer "position", default: 0, null: false
t.bigint "permissions", default: 0, null: false
t.boolean "highlighted", default: false, null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.datetime "created_at", null: false
@ -1003,11 +1013,13 @@ ActiveRecord::Schema.define(version: 2022_06_13_110903) do
t.string "webauthn_id"
t.inet "sign_up_ip"
t.boolean "skip_sign_in_token"
t.bigint "role_id"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, opclass: :text_pattern_ops, where: "(reset_password_token IS NOT NULL)"
t.index ["role_id"], name: "index_users_on_role_id", where: "(role_id IS NOT NULL)"
end
create_table "web_push_subscriptions", force: :cascade do |t|
@ -1159,6 +1171,7 @@ ActiveRecord::Schema.define(version: 2022_06_13_110903) do
add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
add_foreign_key "users", "invites", on_delete: :nullify
add_foreign_key "users", "oauth_applications", column: "created_by_application_id", on_delete: :nullify
add_foreign_key "users", "user_roles", column: "role_id", on_delete: :nullify
add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade
add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade

View File

@ -1,11 +1,5 @@
Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow push')
# frozen_string_literal: true
domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
account = Account.find_or_initialize_by(id: -99, actor_type: 'Application', locked: true, username: domain)
account.save!
if Rails.env.development?
admin = Account.where(username: 'admin').first_or_initialize(username: 'admin')
admin.save(validate: false)
User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, admin: true, account: admin, agreement: true, approved: true).save!
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |seed|
load seed
end

1
db/seeds/01_web_app.rb Normal file
View File

@ -0,0 +1 @@
Doorkeeper::Application.create_with(name: 'Web', redirect_uri: Doorkeeper.configuration.native_redirect_uri, scopes: 'read write follow push').find_or_create_by(superapp: true)

View File

@ -0,0 +1 @@
Account.create_with(actor_type: 'Application', locked: true, username: ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain).find_or_create_by(id: -99)

9
db/seeds/03_roles.rb Normal file
View File

@ -0,0 +1,9 @@
# Pre-create base role
UserRole.everyone
# Create default roles defined in config file
default_roles = YAML.load_file(Rails.root.join('config', 'roles.yml'))
default_roles.each do |_, config|
UserRole.create_with(position: config['position'], permissions_as_keys: config['permissions'], highlighted: true).find_or_create_by(name: config['name'])
end

8
db/seeds/04_admin.rb Normal file
View File

@ -0,0 +1,8 @@
if Rails.env.development?
domain = ENV['LOCAL_DOMAIN'] || Rails.configuration.x.local_domain
admin = Account.where(username: 'admin').first_or_initialize(username: 'admin')
admin.save(validate: false)
User.where(email: "admin@#{domain}").first_or_initialize(email: "admin@#{domain}", password: 'mastodonadmin', password_confirmation: 'mastodonadmin', confirmed_at: Time.now.utc, role: UserRole.find_by(name: 'Owner'), account: admin, agreement: true, approved: true).save!
end

View File

@ -54,7 +54,7 @@ module Mastodon
option :email, required: true
option :confirmed, type: :boolean
option :role, default: 'user', enum: %w(user moderator admin)
option :role
option :reattach, type: :boolean
option :force, type: :boolean
desc 'create USERNAME', 'Create a new user'
@ -65,8 +65,7 @@ module Mastodon
With the --confirmed option, the confirmation e-mail will
be skipped and the account will be active straight away.
With the --role option one of "user", "admin" or "moderator"
can be supplied. Defaults to "user"
With the --role option, the role can be supplied.
With the --reattach option, the new user will be reattached
to a given existing username of an old account. If the old
@ -75,9 +74,22 @@ module Mastodon
username to the new account anyway.
LONG_DESC
def create(username)
role_id = nil
if options[:role]
role = UserRole.find_by(name: options[:role])
if role.nil?
say('Cannot find user role with that name', :red)
exit(1)
end
role_id = role.id
end
account = Account.new(username: username)
password = SecureRandom.hex
user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
user = User.new(email: options[:email], password: password, agreement: true, approved: true, role_id: role_id, confirmed_at: options[:confirmed] ? Time.now.utc : nil, bypass_invite_request_check: true)
if options[:reattach]
account = Account.find_local(username) || Account.new(username: username)
@ -106,14 +118,14 @@ module Mastodon
user.errors.to_h.each do |key, error|
say('Failure/Error: ', :red)
say(key)
say(' ' + error, :red)
say(" #{error}", :red)
end
exit(1)
end
end
option :role, enum: %w(user moderator admin)
option :role
option :email
option :confirm, type: :boolean
option :enable, type: :boolean
@ -125,8 +137,7 @@ module Mastodon
long_desc <<-LONG_DESC
Modify a user account.
With the --role option, update the user's role to one of "user",
"moderator" or "admin".
With the --role option, update the user's role.
With the --email option, update the user's e-mail address. With
the --confirm option, mark the user's e-mail as confirmed.
@ -152,8 +163,14 @@ module Mastodon
end
if options[:role]
user.admin = options[:role] == 'admin'
user.moderator = options[:role] == 'moderator'
role = UserRole.find_by(name: options[:role])
if role.nil?
say('Cannot find user role with that name', :red)
exit(1)
end
user.role_id = role.id
end
password = SecureRandom.hex if options[:reset_password]
@ -172,7 +189,7 @@ module Mastodon
user.errors.to_h.each do |key, error|
say('Failure/Error: ', :red)
say(key)
say(' ' + error, :red)
say(" #{error}", :red)
end
exit(1)
@ -319,7 +336,7 @@ module Mastodon
unless skip_domains.empty?
say('The following domains were not available during the check:', :yellow)
skip_domains.each { |domain| say(' ' + domain) }
skip_domains.each { |domain| say(" #{domain}") }
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module SimpleNavigation
module ItemExtensions
def url
if @url.nil? && @sub_navigation
@sub_navigation.items.first.url
else
@url
end
end
end
end
SimpleNavigation::Item.prepend(SimpleNavigation::ItemExtensions)

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Admin::AccountModerationNotesController, type: :controller do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:target_account) { Fabricate(:account) }
before do

View File

@ -6,7 +6,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
before { sign_in current_user, scope: :user }
describe 'GET #index' do
let(:current_user) { Fabricate(:user, admin: true) }
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
around do |example|
default_per_page = Account.default_per_page
@ -60,7 +60,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
describe 'GET #show' do
let(:current_user) { Fabricate(:user, admin: true) }
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:account) { Fabricate(:account) }
it 'returns http success' do
@ -72,15 +72,15 @@ RSpec.describe Admin::AccountsController, type: :controller do
describe 'POST #memorialize' do
subject { post :memorialize, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: current_user_admin) }
let(:current_user) { Fabricate(:user, role: current_role) }
let(:account) { user.account }
let(:user) { Fabricate(:user, admin: target_user_admin) }
let(:user) { Fabricate(:user, role: target_role) }
context 'when user is admin' do
let(:current_user_admin) { true }
let(:current_role) { UserRole.find_by(name: 'Admin') }
context 'when target user is admin' do
let(:target_user_admin) { true }
let(:target_role) { UserRole.find_by(name: 'Admin') }
it 'fails to memorialize account' do
is_expected.to have_http_status :forbidden
@ -89,7 +89,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when target user is not admin' do
let(:target_user_admin) { false }
let(:target_role) { UserRole.find_by(name: 'Moderator') }
it 'succeeds in memorializing account' do
is_expected.to redirect_to admin_account_path(account.id)
@ -99,10 +99,10 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when user is not admin' do
let(:current_user_admin) { false }
let(:current_role) { UserRole.find_by(name: 'Moderator') }
context 'when target user is admin' do
let(:target_user_admin) { true }
let(:target_role) { UserRole.find_by(name: 'Admin') }
it 'fails to memorialize account' do
is_expected.to have_http_status :forbidden
@ -111,7 +111,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when target user is not admin' do
let(:target_user_admin) { false }
let(:target_role) { UserRole.find_by(name: 'Moderator') }
it 'fails to memorialize account' do
is_expected.to have_http_status :forbidden
@ -124,12 +124,12 @@ RSpec.describe Admin::AccountsController, type: :controller do
describe 'POST #enable' do
subject { post :enable, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { user.account }
let(:user) { Fabricate(:user, disabled: true) }
context 'when user is admin' do
let(:admin) { true }
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in enabling account' do
is_expected.to redirect_to admin_account_path(account.id)
@ -138,7 +138,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when user is not admin' do
let(:admin) { false }
let(:role) { UserRole.everyone }
it 'fails to enable account' do
is_expected.to have_http_status :forbidden
@ -150,19 +150,23 @@ RSpec.describe Admin::AccountsController, type: :controller do
describe 'POST #redownload' do
subject { post :redownload, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:account) { Fabricate(:account) }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account, domain: 'example.com') }
before do
allow_any_instance_of(ResolveAccountService).to receive(:call)
end
context 'when user is admin' do
let(:admin) { true }
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in redownloadin' do
it 'succeeds in redownloading' do
is_expected.to redirect_to admin_account_path(account.id)
end
end
context 'when user is not admin' do
let(:admin) { false }
let(:role) { UserRole.everyone }
it 'fails to redownload' do
is_expected.to have_http_status :forbidden
@ -173,11 +177,11 @@ RSpec.describe Admin::AccountsController, type: :controller do
describe 'POST #remove_avatar' do
subject { post :remove_avatar, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account) }
context 'when user is admin' do
let(:admin) { true }
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in removing avatar' do
is_expected.to redirect_to admin_account_path(account.id)
@ -185,7 +189,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when user is not admin' do
let(:admin) { false }
let(:role) { UserRole.everyone }
it 'fails to remove avatar' do
is_expected.to have_http_status :forbidden
@ -196,12 +200,12 @@ RSpec.describe Admin::AccountsController, type: :controller do
describe 'POST #unblock_email' do
subject { post :unblock_email, params: { id: account.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account, suspended: true) }
let!(:email_block) { Fabricate(:canonical_email_block, reference_account: account) }
context 'when user is admin' do
let(:admin) { true }
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in removing email blocks' do
expect { subject }.to change { CanonicalEmailBlock.where(reference_account: account).count }.from(1).to(0)
@ -214,7 +218,7 @@ RSpec.describe Admin::AccountsController, type: :controller do
end
context 'when user is not admin' do
let(:admin) { false }
let(:role) { UserRole.everyone }
it 'fails to remove avatar' do
subject

View File

@ -5,7 +5,7 @@ require 'rails_helper'
describe Admin::ActionLogsController, type: :controller do
describe 'GET #index' do
it 'returns 200' do
sign_in Fabricate(:user, admin: true)
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
get :index, params: { page: 1 }
expect(response).to have_http_status(200)

View File

@ -5,13 +5,14 @@ require 'rails_helper'
describe Admin::BaseController, type: :controller do
controller do
def success
authorize :dashboard, :index?
render 'admin/reports/show'
end
end
it 'requires administrator or moderator' do
routes.draw { get 'success' => 'admin/base#success' }
sign_in(Fabricate(:user, admin: false, moderator: false))
sign_in(Fabricate(:user))
get :success
expect(response).to have_http_status(:forbidden)
@ -19,14 +20,14 @@ describe Admin::BaseController, type: :controller do
it 'renders admin layout as a moderator' do
routes.draw { get 'success' => 'admin/base#success' }
sign_in(Fabricate(:user, moderator: true))
sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Moderator')))
get :success
expect(response).to render_template layout: 'admin'
end
it 'renders admin layout as an admin' do
routes.draw { get 'success' => 'admin/base#success' }
sign_in(Fabricate(:user, admin: true))
sign_in(Fabricate(:user, role: UserRole.find_by(name: 'Admin')))
get :success
expect(response).to render_template layout: 'admin'
end

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Admin::ChangeEmailsController, type: :controller do
render_views
let(:admin) { Fabricate(:user, admin: true) }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in admin

View File

@ -4,7 +4,7 @@ RSpec.describe Admin::ConfirmationsController, type: :controller do
render_views
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'POST #create' do

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe Admin::CustomEmojisController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user

View File

@ -12,7 +12,7 @@ describe Admin::DashboardController, type: :controller do
Admin::SystemCheck::Message.new(:rules_check, nil, admin_rules_path),
Admin::SystemCheck::Message.new(:sidekiq_process_check, 'foo, bar'),
])
sign_in Fabricate(:user, admin: true)
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
end
it 'returns 200' do

View File

@ -14,7 +14,7 @@ RSpec.describe Admin::Disputes::AppealsController, type: :controller do
end
describe 'POST #approve' do
let(:current_user) { Fabricate(:user, admin: true) }
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
allow(UserMailer).to receive(:appeal_approved).and_return(double('email', deliver_later: nil))
@ -35,7 +35,7 @@ RSpec.describe Admin::Disputes::AppealsController, type: :controller do
end
describe 'POST #reject' do
let(:current_user) { Fabricate(:user, admin: true) }
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
allow(UserMailer).to receive(:appeal_rejected).and_return(double('email', deliver_later: nil))

View File

@ -4,7 +4,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
render_views
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #new' do

View File

@ -6,7 +6,7 @@ RSpec.describe Admin::EmailDomainBlocksController, type: :controller do
render_views
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #index' do

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Admin::InstancesController, type: :controller do
render_views
let(:current_user) { Fabricate(:user, admin: true) }
let(:current_user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let!(:account) { Fabricate(:account, domain: 'popular') }
let!(:account2) { Fabricate(:account, domain: 'popular') }
@ -35,11 +35,11 @@ RSpec.describe Admin::InstancesController, type: :controller do
describe 'DELETE #destroy' do
subject { delete :destroy, params: { id: Instance.first.id } }
let(:current_user) { Fabricate(:user, admin: admin) }
let(:current_user) { Fabricate(:user, role: role) }
let(:account) { Fabricate(:account) }
context 'when user is admin' do
let(:admin) { true }
let(:role) { UserRole.find_by(name: 'Admin') }
it 'succeeds in purging instance' do
is_expected.to redirect_to admin_instances_path
@ -47,7 +47,7 @@ RSpec.describe Admin::InstancesController, type: :controller do
end
context 'when user is not admin' do
let(:admin) { false }
let(:role) { nil }
it 'fails to purge instance' do
is_expected.to have_http_status :forbidden

View File

@ -5,7 +5,7 @@ require 'rails_helper'
describe Admin::InvitesController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe Admin::ReportNotesController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe Admin::ReportsController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
before do
sign_in user, scope: :user
end

View File

@ -5,7 +5,7 @@ describe Admin::ResetsController do
let(:account) { Fabricate(:account) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'POST #create' do

View File

@ -3,31 +3,247 @@ require 'rails_helper'
describe Admin::RolesController do
render_views
let(:admin) { Fabricate(:user, admin: true) }
let(:permissions) { UserRole::Flags::NONE }
let(:current_role) { UserRole.create(name: 'Foo', permissions: permissions, position: 10) }
let(:current_user) { Fabricate(:user, role: current_role) }
before do
sign_in admin, scope: :user
sign_in current_user, scope: :user
end
describe 'POST #promote' do
subject { post :promote, params: { account_id: user.account_id } }
describe 'GET #index' do
before do
get :index
end
let(:user) { Fabricate(:user, moderator: false, admin: false) }
context 'when user does not have permission to manage roles' do
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
it 'promotes user' do
expect(subject).to redirect_to admin_account_path(user.account_id)
expect(user.reload).to be_moderator
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
end
describe 'POST #demote' do
subject { post :demote, params: { account_id: user.account_id } }
describe 'GET #new' do
before do
get :new
end
let(:user) { Fabricate(:user, moderator: true, admin: false) }
context 'when user does not have permission to manage roles' do
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
it 'demotes user' do
expect(subject).to redirect_to admin_account_path(user.account_id)
expect(user.reload).not_to be_moderator
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
end
describe 'POST #create' do
let(:selected_position) { 1 }
let(:selected_permissions_as_keys) { %w(manage_roles) }
before do
post :create, params: { user_role: { name: 'Bar', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
end
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
context 'when new role\'s does not elevate above the user\'s role' do
let(:selected_position) { 1 }
let(:selected_permissions_as_keys) { %w(manage_roles) }
it 'redirects to roles page' do
expect(response).to redirect_to(admin_roles_path)
end
it 'creates new role' do
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
end
end
context 'when new role\'s position is higher than user\'s role' do
let(:selected_position) { 100 }
let(:selected_permissions_as_keys) { %w(manage_roles) }
it 'renders new template' do
expect(response).to render_template(:new)
end
it 'does not create new role' do
expect(UserRole.find_by(name: 'Bar')).to be_nil
end
end
context 'when new role has permissions the user does not have' do
let(:selected_position) { 1 }
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
it 'renders new template' do
expect(response).to render_template(:new)
end
it 'does not create new role' do
expect(UserRole.find_by(name: 'Bar')).to be_nil
end
end
context 'when user has administrator permission' do
let(:permissions) { UserRole::FLAGS[:administrator] }
let(:selected_position) { 1 }
let(:selected_permissions_as_keys) { %w(manage_roles manage_users manage_reports) }
it 'redirects to roles page' do
expect(response).to redirect_to(admin_roles_path)
end
it 'creates new role' do
expect(UserRole.find_by(name: 'Bar')).to_not be_nil
end
end
end
end
describe 'GET #edit' do
let(:role_position) { 8 }
let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
before do
get :edit, params: { id: role.id }
end
context 'when user does not have permission to manage roles' do
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
context 'when user outranks the role' do
it 'returns http success' do
expect(response).to have_http_status(:success)
end
end
context 'when role outranks user' do
let(:role_position) { current_role.position + 1 }
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
end
end
describe 'PUT #update' do
let(:role_position) { 8 }
let(:role_permissions) { UserRole::FLAGS[:manage_users] }
let(:role) { UserRole.create(name: 'Bar', permissions: role_permissions, position: role_position) }
let(:selected_position) { 8 }
let(:selected_permissions_as_keys) { %w(manage_users) }
before do
put :update, params: { id: role.id, user_role: { name: 'Baz', position: selected_position, permissions_as_keys: selected_permissions_as_keys } }
end
context 'when user does not have permission to manage roles' do
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
it 'does not update the role' do
expect(role.reload.name).to eq 'Bar'
end
end
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
context 'when role has permissions the user doesn\'t' do
it 'renders edit template' do
expect(response).to render_template(:edit)
end
it 'does not update the role' do
expect(role.reload.name).to eq 'Bar'
end
end
context 'when user has all permissions of the role' do
let(:permissions) { UserRole::FLAGS[:manage_roles] | UserRole::FLAGS[:manage_users] }
context 'when user outranks the role' do
it 'redirects to roles page' do
expect(response).to redirect_to(admin_roles_path)
end
it 'updates the role' do
expect(role.reload.name).to eq 'Baz'
end
end
context 'when role outranks user' do
let(:role_position) { current_role.position + 1 }
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
it 'does not update the role' do
expect(role.reload.name).to eq 'Bar'
end
end
end
end
end
describe 'DELETE #destroy' do
let(:role_position) { 8 }
let(:role) { UserRole.create(name: 'Bar', permissions: UserRole::FLAGS[:manage_users], position: role_position) }
before do
delete :destroy, params: { id: role.id }
end
context 'when user does not have permission to manage roles' do
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
context 'when user has permission to manage roles' do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
context 'when user outranks the role' do
it 'redirects to roles page' do
expect(response).to redirect_to(admin_roles_path)
end
end
context 'when role outranks user' do
let(:role_position) { current_role.position + 1 }
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
end
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe Admin::SettingsController, type: :controller do
describe 'When signed in as an admin' do
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'GET #edit' do

View File

@ -3,7 +3,7 @@ require 'rails_helper'
describe Admin::StatusesController do
render_views
let(:user) { Fabricate(:user, admin: true) }
let(:user) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:account) { Fabricate(:account) }
let!(:status) { Fabricate(:status, account: account) }
let(:media_attached_status) { Fabricate(:status, account: account, sensitive: !sensitive) }

View File

@ -6,7 +6,7 @@ RSpec.describe Admin::TagsController, type: :controller do
render_views
before do
sign_in Fabricate(:user, admin: true)
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin'))
end
describe 'GET #show' do

View File

@ -0,0 +1,81 @@
require 'rails_helper'
describe Admin::Users::RolesController do
render_views
let(:current_role) { UserRole.create(name: 'Foo', permissions: UserRole::FLAGS[:manage_roles], position: 10) }
let(:current_user) { Fabricate(:user, role: current_role) }
let(:previous_role) { nil }
let(:user) { Fabricate(:user, role: previous_role) }
before do
sign_in current_user, scope: :user
end
describe 'GET #show' do
before do
get :show, params: { user_id: user.id }
end
it 'returns http success' do
expect(response).to have_http_status(:success)
end
context 'when target user is higher ranked than current user' do
let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
end
describe 'PUT #update' do
let(:selected_role) { UserRole.create(name: 'Bar', permissions: permissions, position: position) }
before do
put :update, params: { user_id: user.id, user: { role_id: selected_role.id } }
end
context do
let(:permissions) { UserRole::FLAGS[:manage_roles] }
let(:position) { 1 }
it 'updates user role' do
expect(user.reload.role_id).to eq selected_role&.id
end
it 'redirects back to account page' do
expect(response).to redirect_to(admin_account_path(user.account_id))
end
end
context 'when selected role has higher position than current user\'s role' do
let(:permissions) { UserRole::FLAGS[:administrator] }
let(:position) { 100 }
it 'does not update user role' do
expect(user.reload.role_id).to eq previous_role&.id
end
it 'renders edit form' do
expect(response).to render_template(:show)
end
end
context 'when target user is higher ranked than current user' do
let(:previous_role) { UserRole.create(name: 'Baz', permissions: UserRole::FLAGS[:administrator], position: 100) }
let(:permissions) { UserRole::FLAGS[:manage_roles] }
let(:position) { 1 }
it 'does not update user role' do
expect(user.reload.role_id).to eq previous_role&.id
end
it 'returns http forbidden' do
expect(response).to have_http_status(:forbidden)
end
end
end
end

View File

@ -1,12 +1,13 @@
require 'rails_helper'
require 'webauthn/fake_client'
describe Admin::TwoFactorAuthenticationsController do
describe Admin::Users::TwoFactorAuthenticationsController do
render_views
let(:user) { Fabricate(:user) }
before do
sign_in Fabricate(:user, admin: true), scope: :user
sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user
end
describe 'DELETE #destroy' do

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
render_views
let(:role) { 'moderator' }
let(:role) { UserRole.find_by(name: 'Moderator') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -22,7 +22,7 @@ RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -35,7 +35,7 @@ RSpec.describe Api::V1::Admin::AccountActionsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
render_views
let(:role) { 'moderator' }
let(:role) { UserRole.find_by(name: 'Moderator') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -22,7 +22,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -46,7 +46,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
[
[{ active: 'true', local: 'true', staff: 'true' }, [:admin_account]],
@ -77,7 +77,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -91,7 +91,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -109,7 +109,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -127,7 +127,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -145,7 +145,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -163,7 +163,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -181,7 +181,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
render_views
let(:role) { 'admin' }
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -21,7 +21,7 @@ RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -36,8 +36,8 @@ RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -58,8 +58,8 @@ RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -79,8 +79,8 @@ RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -99,8 +99,8 @@ RSpec.describe Api::V1::Admin::DomainAllowsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
render_views
let(:role) { 'admin' }
let(:role) { UserRole.find_by(name: 'Admin') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -21,7 +21,7 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -36,8 +36,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -58,8 +58,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -79,8 +79,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)
@ -100,8 +100,8 @@ RSpec.describe Api::V1::Admin::DomainBlocksController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', 'moderator'
it_behaves_like 'forbidden for wrong role', ''
it_behaves_like 'forbidden for wrong role', 'Moderator'
it 'returns http success' do
expect(response).to have_http_status(200)

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
render_views
let(:role) { 'moderator' }
let(:role) { UserRole.find_by(name: 'Moderator') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -22,7 +22,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -35,7 +35,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -48,7 +48,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -61,7 +61,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -74,7 +74,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -87,7 +87,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)
@ -100,7 +100,7 @@ RSpec.describe Api::V1::Admin::ReportsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
it 'returns http success' do
expect(response).to have_http_status(200)

View File

@ -13,7 +13,7 @@ RSpec.describe Api::V1::ReportsController, type: :controller do
end
describe 'POST #create' do
let!(:admin) { Fabricate(:user, admin: true) }
let!(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:scopes) { 'write:reports' }
let(:status) { Fabricate(:status) }

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
render_views
let(:role) { 'moderator' }
let(:role) { UserRole.find_by(name: 'Moderator') }
let(:user) { Fabricate(:user, role: role) }
let(:scopes) { 'admin:read admin:write' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
@ -22,7 +22,7 @@ RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
end
shared_examples 'forbidden for wrong role' do |wrong_role|
let(:role) { wrong_role }
let(:role) { UserRole.find_by(name: wrong_role) }
it 'returns http forbidden' do
expect(response).to have_http_status(403)
@ -46,7 +46,7 @@ RSpec.describe Api::V2::Admin::AccountsController, type: :controller do
end
it_behaves_like 'forbidden for wrong scope', 'write:statuses'
it_behaves_like 'forbidden for wrong role', 'user'
it_behaves_like 'forbidden for wrong role', ''
[
[{ status: 'active', origin: 'local', permissions: 'staff' }, [:admin_account]],

View File

@ -183,70 +183,6 @@ describe ApplicationController, type: :controller do
end
end
describe 'require_admin!' do
controller do
before_action :require_admin!
def success
head 200
end
end
before do
routes.draw { get 'success' => 'anonymous#success' }
end
it 'returns a 403 if current user is not admin' do
sign_in(Fabricate(:user, admin: false))
get 'success'
expect(response).to have_http_status(403)
end
it 'returns a 403 if current user is only a moderator' do
sign_in(Fabricate(:user, moderator: true))
get 'success'
expect(response).to have_http_status(403)
end
it 'does nothing if current user is admin' do
sign_in(Fabricate(:user, admin: true))
get 'success'
expect(response).to have_http_status(200)
end
end
describe 'require_staff!' do
controller do
before_action :require_staff!
def success
head 200
end
end
before do
routes.draw { get 'success' => 'anonymous#success' }
end
it 'returns a 403 if current user is not admin or moderator' do
sign_in(Fabricate(:user, admin: false, moderator: false))
get 'success'
expect(response).to have_http_status(403)
end
it 'does nothing if current user is moderator' do
sign_in(Fabricate(:user, moderator: true))
get 'success'
expect(response).to have_http_status(200)
end
it 'does nothing if current user is admin' do
sign_in(Fabricate(:user, admin: true))
get 'success'
expect(response).to have_http_status(200)
end
end
describe 'forbidden' do
controller do
def route_forbidden

View File

@ -5,7 +5,7 @@ RSpec.describe Disputes::AppealsController, type: :controller do
before { sign_in current_user, scope: :user }
let!(:admin) { Fabricate(:user, admin: true) }
let!(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
describe '#create' do
let(:current_user) { Fabricate(:user) }

View File

@ -7,30 +7,30 @@ describe InvitesController do
sign_in user
end
around do |example|
min_invite_role = Setting.min_invite_role
example.run
Setting.min_invite_role = min_invite_role
end
describe 'GET #index' do
subject { get :index }
let(:user) { Fabricate(:user, moderator: false, admin: false) }
let(:user) { Fabricate(:user) }
let!(:invite) { Fabricate(:invite, user: user) }
context 'when user is a staff' do
context 'when everyone can invite' do
before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
end
it 'renders index page' do
Setting.min_invite_role = 'user'
expect(subject).to render_template :index
expect(assigns(:invites)).to include invite
expect(assigns(:invites).count).to eq 1
end
end
context 'when user is not a staff' do
context 'when not everyone can invite' do
before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
end
it 'returns 403' do
Setting.min_invite_role = 'modelator'
expect(subject).to have_http_status 403
end
end
@ -39,8 +39,12 @@ describe InvitesController do
describe 'POST #create' do
subject { post :create, params: { invite: { max_uses: '10', expires_in: 1800 } } }
context 'when user is an admin' do
let(:user) { Fabricate(:user, moderator: false, admin: true) }
context 'when everyone can invite' do
let(:user) { Fabricate(:user) }
before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions | UserRole::FLAGS[:invite_users])
end
it 'succeeds to create a invite' do
expect { subject }.to change { Invite.count }.by(1)
@ -49,8 +53,12 @@ describe InvitesController do
end
end
context 'when user is not an admin' do
let(:user) { Fabricate(:user, moderator: true, admin: false) }
context 'when not everyone can invite' do
let(:user) { Fabricate(:user) }
before do
UserRole.everyone.update(permissions: UserRole.everyone.permissions & ~UserRole::FLAGS[:invite_users])
end
it 'returns 403' do
expect(subject).to have_http_status 403
@ -61,8 +69,8 @@ describe InvitesController do
describe 'DELETE #create' do
subject { delete :destroy, params: { id: invite.id } }
let(:user) { Fabricate(:user) }
let!(:invite) { Fabricate(:invite, user: user, expires_at: nil) }
let(:user) { Fabricate(:user, moderator: false, admin: true) }
it 'expires invite' do
expect(subject).to redirect_to invites_path

View File

@ -0,0 +1,5 @@
Fabricator(:user_role) do
name "MyString"
color "MyString"
permissions ""
end

View File

@ -445,7 +445,7 @@ RSpec.describe Account, type: :model do
it 'accepts arbitrary limits' do
2.times.each { Fabricate(:account, display_name: "Display Name") }
results = Account.search_for("display", 1)
results = Account.search_for("display", limit: 1)
expect(results.size).to eq 1
end
@ -473,7 +473,7 @@ RSpec.describe Account, type: :model do
)
account.follow!(match)
results = Account.advanced_search_for('A?l\i:c e', account, 10, true)
results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true)
expect(results).to eq [match]
end
@ -485,7 +485,7 @@ RSpec.describe Account, type: :model do
domain: 'example.com'
)
results = Account.advanced_search_for('A?l\i:c e', account, 10, true)
results = Account.advanced_search_for('A?l\i:c e', account, limit: 10, following: true)
expect(results).to eq []
end
@ -498,7 +498,7 @@ RSpec.describe Account, type: :model do
suspended: true
)
results = Account.advanced_search_for('username', account, 10, true)
results = Account.advanced_search_for('username', account, limit: 10, following: true)
expect(results).to eq []
end
@ -511,7 +511,7 @@ RSpec.describe Account, type: :model do
match.user.update(approved: false)
results = Account.advanced_search_for('username', account, 10, true)
results = Account.advanced_search_for('username', account, limit: 10, following: true)
expect(results).to eq []
end
@ -524,7 +524,7 @@ RSpec.describe Account, type: :model do
match.user.update(confirmed_at: nil)
results = Account.advanced_search_for('username', account, 10, true)
results = Account.advanced_search_for('username', account, limit: 10, following: true)
expect(results).to eq []
end
end
@ -588,7 +588,7 @@ RSpec.describe Account, type: :model do
it 'accepts arbitrary limits' do
2.times { Fabricate(:account, display_name: "Display Name") }
results = Account.advanced_search_for("display", account, 1)
results = Account.advanced_search_for("display", account, limit: 1)
expect(results.size).to eq 1
end

View File

@ -5,7 +5,7 @@ RSpec.describe Admin::AccountAction, type: :model do
describe '#save!' do
subject { account_action.save! }
let(:account) { Fabricate(:user, admin: true).account }
let(:account) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:target_account) { Fabricate(:account) }
let(:type) { 'disable' }

View File

@ -0,0 +1,189 @@
require 'rails_helper'
RSpec.describe UserRole, type: :model do
subject { described_class.create(name: 'Foo', position: 1) }
describe '#can?' do
context 'with a single flag' do
it 'returns true if any of them are present' do
subject.permissions = UserRole::FLAGS[:manage_reports]
expect(subject.can?(:manage_reports)).to be true
end
it 'returns false if it is not set' do
expect(subject.can?(:manage_reports)).to be false
end
end
context 'with multiple flags' do
it 'returns true if any of them are present' do
subject.permissions = UserRole::FLAGS[:manage_users]
expect(subject.can?(:manage_reports, :manage_users)).to be true
end
it 'returns false if none of them are present' do
expect(subject.can?(:manage_reports, :manage_users)).to be false
end
end
context 'with an unknown flag' do
it 'raises an error' do
expect { subject.can?(:foo) }.to raise_error ArgumentError
end
end
end
describe '#overrides?' do
it 'returns true if other role has lower position' do
expect(subject.overrides?(described_class.new(position: subject.position - 1))).to be true
end
it 'returns true if other role is nil' do
expect(subject.overrides?(nil)).to be true
end
it 'returns false if other role has higher position' do
expect(subject.overrides?(described_class.new(position: subject.position + 1))).to be false
end
end
describe '#permissions_as_keys' do
before do
subject.permissions = UserRole::FLAGS[:invite_users] | UserRole::FLAGS[:view_dashboard] | UserRole::FLAGS[:manage_reports]
end
it 'returns an array' do
expect(subject.permissions_as_keys).to match_array %w(invite_users view_dashboard manage_reports)
end
end
describe '#permissions_as_keys=' do
let(:input) { }
before do
subject.permissions_as_keys = input
end
context 'with a single value' do
let(:input) { %w(manage_users) }
it 'sets permission flags' do
expect(subject.permissions).to eq UserRole::FLAGS[:manage_users]
end
end
context 'with multiple values' do
let(:input) { %w(manage_users manage_reports) }
it 'sets permission flags' do
expect(subject.permissions).to eq UserRole::FLAGS[:manage_users] | UserRole::FLAGS[:manage_reports]
end
end
context 'with an unknown value' do
let(:input) { %w(foo) }
it 'does not set permission flags' do
expect(subject.permissions).to eq UserRole::Flags::NONE
end
end
end
describe '#computed_permissions' do
context 'when the role is nobody' do
let(:subject) { described_class.nobody }
it 'returns none' do
expect(subject.computed_permissions).to eq UserRole::Flags::NONE
end
end
context 'when the role is everyone' do
let(:subject) { described_class.everyone }
it 'returns permissions' do
expect(subject.computed_permissions).to eq subject.permissions
end
end
context 'when role has the administrator flag' do
before do
subject.permissions = UserRole::FLAGS[:administrator]
end
it 'returns all permissions' do
expect(subject.computed_permissions).to eq UserRole::Flags::ALL
end
end
context do
it 'returns permissions combined with the everyone role' do
expect(subject.computed_permissions).to eq described_class.everyone.permissions
end
end
end
describe '.everyone' do
subject { described_class.everyone }
it 'returns a role' do
expect(subject).to be_kind_of(described_class)
end
it 'is identified as the everyone role' do
expect(subject.everyone?).to be true
end
it 'has default permissions' do
expect(subject.permissions).to eq UserRole::FLAGS[:invite_users]
end
it 'has negative position' do
expect(subject.position).to eq -1
end
end
describe '.nobody' do
subject { described_class.nobody }
it 'returns a role' do
expect(subject).to be_kind_of(described_class)
end
it 'is identified as the nobody role' do
expect(subject.nobody?).to be true
end
it 'has no permissions' do
expect(subject.permissions).to eq UserRole::Flags::NONE
end
it 'has negative position' do
expect(subject.position).to eq -1
end
end
describe '#everyone?' do
it 'returns true when id is -99' do
subject.id = -99
expect(subject.everyone?).to be true
end
it 'returns false when id is not -99' do
subject.id = 123
expect(subject.everyone?).to be false
end
end
describe '#nobody?' do
it 'returns true when id is nil' do
subject.id = nil
expect(subject.nobody?).to be true
end
it 'returns false when id is not nil' do
subject.id = 123
expect(subject.nobody?).to be false
end
end
end

View File

@ -56,14 +56,6 @@ RSpec.describe User, type: :model do
end
end
describe 'admins' do
it 'returns an array of users who are admin' do
user_1 = Fabricate(:user, admin: false)
user_2 = Fabricate(:user, admin: true)
expect(User.admins).to match_array([user_2])
end
end
describe 'confirmed' do
it 'returns an array of users who are confirmed' do
user_1 = Fabricate(:user, confirmed_at: nil)
@ -289,49 +281,6 @@ RSpec.describe User, type: :model do
end
end
describe '#role' do
it 'returns admin for admin' do
user = User.new(admin: true)
expect(user.role).to eq 'admin'
end
it 'returns moderator for moderator' do
user = User.new(moderator: true)
expect(user.role).to eq 'moderator'
end
it 'returns user otherwise' do
user = User.new
expect(user.role).to eq 'user'
end
end
describe '#role?' do
it 'returns false when invalid role requested' do
user = User.new(admin: true)
expect(user.role?('disabled')).to be false
end
it 'returns true when exact role match' do
user = User.new
mod = User.new(moderator: true)
admin = User.new(admin: true)
expect(user.role?('user')).to be true
expect(mod.role?('moderator')).to be true
expect(admin.role?('admin')).to be true
end
it 'returns true when role higher than needed' do
mod = User.new(moderator: true)
admin = User.new(admin: true)
expect(mod.role?('user')).to be true
expect(admin.role?('user')).to be true
expect(admin.role?('moderator')).to be true
end
end
describe '#disable!' do
subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) }
let(:current_sign_in_at) { Time.zone.now }
@ -420,110 +369,6 @@ RSpec.describe User, type: :model do
end
end
describe '#promote!' do
subject(:user) { Fabricate(:user, admin: is_admin, moderator: is_moderator) }
before do
user.promote!
end
context 'when user is an admin' do
let(:is_admin) { true }
context 'when user is a moderator' do
let(:is_moderator) { true }
it 'changes moderator filed false' do
expect(user).to be_admin
expect(user).not_to be_moderator
end
end
context 'when user is not a moderator' do
let(:is_moderator) { false }
it 'does not change status' do
expect(user).to be_admin
expect(user).not_to be_moderator
end
end
end
context 'when user is not admin' do
let(:is_admin) { false }
context 'when user is a moderator' do
let(:is_moderator) { true }
it 'changes user into an admin' do
expect(user).to be_admin
expect(user).not_to be_moderator
end
end
context 'when user is not a moderator' do
let(:is_moderator) { false }
it 'changes user into a moderator' do
expect(user).not_to be_admin
expect(user).to be_moderator
end
end
end
end
describe '#demote!' do
subject(:user) { Fabricate(:user, admin: admin, moderator: moderator) }
before do
user.demote!
end
context 'when user is an admin' do
let(:admin) { true }
context 'when user is a moderator' do
let(:moderator) { true }
it 'changes user into a moderator' do
expect(user).not_to be_admin
expect(user).to be_moderator
end
end
context 'when user is not a moderator' do
let(:moderator) { false }
it 'changes user into a moderator' do
expect(user).not_to be_admin
expect(user).to be_moderator
end
end
end
context 'when user is not an admin' do
let(:admin) { false }
context 'when user is a moderator' do
let(:moderator) { true }
it 'changes user into a plain user' do
expect(user).not_to be_admin
expect(user).not_to be_moderator
end
end
context 'when user is not a moderator' do
let(:moderator) { false }
it 'does not change any fields' do
expect(user).not_to be_admin
expect(user).not_to be_moderator
end
end
end
end
describe '#active_for_authentication?' do
subject { user.active_for_authentication? }
let(:user) { Fabricate(:user, disabled: disabled, confirmed_at: confirmed_at) }
@ -560,4 +405,8 @@ RSpec.describe User, type: :model do
end
end
end
describe '.those_who_can' do
pending
end
end

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe AccountModerationNotePolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :create? do
@ -31,7 +31,7 @@ RSpec.describe AccountModerationNotePolicy do
context 'admin' do
it 'grants to destroy' do
expect(subject).to permit(admin, AccountModerationNotePolicy)
expect(subject).to permit(admin, account_moderation_note)
end
end

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe AccountPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
let(:alice) { Fabricate(:account) }
@ -55,7 +55,7 @@ RSpec.describe AccountPolicy do
end
end
permissions :redownload?, :subscribe?, :unsubscribe? do
permissions :redownload? do
context 'admin' do
it 'permits' do
expect(subject).to permit(admin)
@ -70,7 +70,7 @@ RSpec.describe AccountPolicy do
end
permissions :suspend?, :silence? do
let(:staff) { Fabricate(:user, admin: true).account }
let(:staff) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
context 'staff' do
context 'record is staff' do
@ -94,7 +94,7 @@ RSpec.describe AccountPolicy do
end
permissions :memorialize? do
let(:other_admin) { Fabricate(:user, admin: true).account }
let(:other_admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
context 'admin' do
context 'record is admin' do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe CustomEmojiPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :enable?, :disable? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe DomainBlockPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :show?, :create?, :destroy? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe EmailDomainBlockPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :create?, :destroy? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe InstancePolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :show?, :destroy? do

View File

@ -5,8 +5,8 @@ require 'pundit/rspec'
RSpec.describe InvitePolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:john) { Fabricate(:account) }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:user).account }
permissions :index? do
context 'staff?' do
@ -17,16 +17,22 @@ RSpec.describe InvitePolicy do
end
permissions :create? do
context 'min_required_role?' do
context 'has privilege' do
before do
UserRole.everyone.update(permissions: UserRole::FLAGS[:invite_users])
end
it 'permits' do
allow_any_instance_of(described_class).to receive(:min_required_role?) { true }
expect(subject).to permit(john, Invite)
end
end
context 'not min_required_role?' do
context 'does not have privilege' do
before do
UserRole.everyone.update(permissions: UserRole::Flags::NONE)
end
it 'denies' do
allow_any_instance_of(described_class).to receive(:min_required_role?) { false }
expect(subject).to_not permit(john, Invite)
end
end
@ -54,39 +60,15 @@ RSpec.describe InvitePolicy do
end
context 'not owner?' do
context 'Setting.min_invite_role == "admin"' do
before do
Setting.min_invite_role = 'admin'
end
context 'admin?' do
it 'permits' do
expect(subject).to permit(admin, Fabricate(:invite))
end
end
context 'not admin?' do
it 'denies' do
expect(subject).to_not permit(john, Fabricate(:invite))
end
context 'admin?' do
it 'permits' do
expect(subject).to permit(admin, Fabricate(:invite))
end
end
context 'Setting.min_invite_role != "admin"' do
before do
Setting.min_invite_role = 'else'
end
context 'staff?' do
it 'permits' do
expect(subject).to permit(admin, Fabricate(:invite))
end
end
context 'not staff?' do
it 'denies' do
expect(subject).to_not permit(john, Fabricate(:invite))
end
context 'not admin?' do
it 'denies' do
expect(subject).to_not permit(john, Fabricate(:invite))
end
end
end

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe RelayPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :update? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe ReportNotePolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :create? do
@ -25,7 +25,8 @@ RSpec.describe ReportNotePolicy do
permissions :destroy? do
context 'admin?' do
it 'permit' do
expect(subject).to permit(admin, ReportNote)
report_note = Fabricate(:report_note, account: john)
expect(subject).to permit(admin, report_note)
end
end

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe ReportPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :update?, :index?, :show? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe SettingsPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :update?, :show? do

View File

@ -6,7 +6,7 @@ require 'pundit/rspec'
RSpec.describe StatusPolicy, type: :model do
subject { described_class }
let(:admin) { Fabricate(:user, admin: true) }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')) }
let(:alice) { Fabricate(:account, username: 'alice') }
let(:bob) { Fabricate(:account, username: 'bob') }
let(:status) { Fabricate(:status, account: alice) }

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe TagPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :index?, :show?, :update? do

View File

@ -5,7 +5,7 @@ require 'pundit/rspec'
RSpec.describe UserPolicy do
let(:subject) { described_class }
let(:admin) { Fabricate(:user, admin: true).account }
let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Admin')).account }
let(:john) { Fabricate(:account) }
permissions :reset_password?, :change_email? do
@ -111,57 +111,4 @@ RSpec.describe UserPolicy do
end
end
end
permissions :promote? do
context 'admin?' do
context 'promotable?' do
it 'permits' do
expect(subject).to permit(admin, john.user)
end
end
context '!promotable?' do
it 'denies' do
expect(subject).to_not permit(admin, admin.user)
end
end
end
context '!admin?' do
it 'denies' do
expect(subject).to_not permit(john, User)
end
end
end
permissions :demote? do
context 'admin?' do
context '!record.admin?' do
context 'demoteable?' do
it 'permits' do
john.user.update(moderator: true)
expect(subject).to permit(admin, john.user)
end
end
context '!demoteable?' do
it 'denies' do
expect(subject).to_not permit(admin, john.user)
end
end
end
context 'record.admin?' do
it 'denies' do
expect(subject).to_not permit(admin, admin.user)
end
end
end
context '!admin?' do
it 'denies' do
expect(subject).to_not permit(john, User)
end
end
end
end