DEV: Change anonymous_posting_min_trust_level to a group-based setting (#24072)

No plugins or themes rely on anonymous_posting_min_trust_level so we
can just switch straight over to anonymous_posting_allowed_groups

This also adds an AUTO_GROUPS const which can be imported in JS
tests which is analogous to the one defined in group.rb. This can be used
to set the current user's groups where JS tests call for checking these groups
against site settings.

Finally a AtLeastOneGroupValidator validator is added for group_list site
settings which ensures that at least one group is always selected, since if
you want to allow all users to use a feature in this way you can just use
the everyone group.
This commit is contained in:
Martin Brennan 2023-10-25 11:45:10 +10:00 committed by GitHub
parent 5e395d4382
commit 9db4eaa870
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 223 additions and 72 deletions

View File

@ -15,10 +15,9 @@ export default class AdminRoute extends DiscourseRoute {
activate() {
if (
!this.currentUser.isInAnyGroups(
this.siteSettings.groupSettingArray(
"enable_experimental_admin_ui_groups"
)
!this.siteSettings.userInAnyGroups(
"enable_experimental_admin_ui_groups",
this.currentUser
)
) {
return DiscourseURL.redirectTo("/admin");

View File

@ -28,8 +28,10 @@ export default class UserMenuProfileTabContent extends Component {
get showToggleAnonymousButton() {
return (
(this.siteSettings.allow_anonymous_posting &&
this.currentUser.trust_level >=
this.siteSettings.anonymous_posting_min_trust_level) ||
this.siteSettings.userInAnyGroups(
"anonymous_posting_allowed_groups",
this.currentUser
)) ||
this.currentUser.is_anonymous
);
}

View File

@ -21,3 +21,50 @@ export const SIDEBAR_URL = {
export const SIDEBAR_SECTION = {
max_title_length: 30,
};
export const AUTO_GROUPS = {
everyone: {
id: 0,
automatic: true,
name: "everyone",
display_name: "everyone",
},
admins: { id: 1, automatic: true, name: "admins", display_name: "admins" },
moderators: {
id: 2,
automatic: true,
name: "moderators",
display_name: "moderators",
},
staff: { id: 3, automatic: true, name: "staff", display_name: "staff" },
trust_level_0: {
id: 10,
automatic: true,
name: "trust_level_0",
display_name: "trust_level_0",
},
trust_level_1: {
id: 11,
automatic: true,
name: "trust_level_1",
display_name: "trust_level_1",
},
trust_level_2: {
id: 12,
automatic: true,
name: "trust_level_2",
display_name: "trust_level_2",
},
trust_level_3: {
id: 13,
automatic: true,
name: "trust_level_3",
display_name: "trust_level_3",
},
trust_level_4: {
id: 14,
automatic: true,
name: "trust_level_4",
display_name: "trust_level_4",
},
};

View File

@ -31,10 +31,9 @@ export default class AdminRevampSectionLink extends BaseSectionLink {
return (
this.currentUser.staff &&
this.currentUser.isInAnyGroups(
this.siteSettings.groupSettingArray(
"enable_experimental_admin_ui_groups"
)
this.siteSettings.userInAnyGroups(
"enable_experimental_admin_ui_groups",
this.currentUser
)
);
}

View File

@ -2,26 +2,35 @@ import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import PreloadStore from "discourse/lib/preload-store";
export function createSiteSettingsFromPreloaded(data) {
const settings = new TrackedObject(data);
settings.groupSettingArray = (groupSetting) => {
const setting = settings[groupSetting];
if (!setting) {
return [];
}
return setting
.toString()
.split("|")
.filter(Boolean)
.map((groupId) => parseInt(groupId, 10));
};
settings.userInAnyGroups = (groupSetting, user) => {
const groupIds = settings.groupSettingArray(groupSetting);
return user.isInAnyGroups(groupIds);
};
return settings;
}
@disableImplicitInjections
export default class SiteSettingsService {
static isServiceFactory = true;
static create() {
const settings = new TrackedObject(PreloadStore.get("siteSettings"));
settings.groupSettingArray = (groupSetting) => {
const setting = settings[groupSetting];
if (!setting) {
return [];
}
return setting
.toString()
.split("|")
.filter(Boolean)
.map((groupId) => parseInt(groupId, 10));
};
return settings;
return createSiteSettingsFromPreloaded(PreloadStore.get("siteSettings"));
}
}

View File

@ -3,6 +3,7 @@ import { click, currentURL, triggerKeyEvent, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { Promise } from "rsvp";
import DButton from "discourse/components/d-button";
import { AUTO_GROUPS } from "discourse/lib/constants";
import { withPluginApi } from "discourse/lib/plugin-api";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import TopicFixtures from "discourse/tests/fixtures/topic";
@ -30,7 +31,7 @@ acceptance("User menu", function (needs) {
needs.settings({
allow_anonymous_posting: true,
anonymous_posting_min_trust_level: 3,
anonymous_posting_allowed_groups: "3",
});
let requestHeaders = {};
@ -564,6 +565,10 @@ acceptance("User menu", function (needs) {
"Do Not Disturb button has the right icon when Do Not Disturb is enabled"
);
assert.ok(
exists("#quick-access-profile ul li.enable-anonymous .btn"),
"toggle anon button is shown"
);
let toggleAnonButton = query(
"#quick-access-profile ul li.enable-anonymous .btn"
);
@ -602,7 +607,15 @@ acceptance("User menu", function (needs) {
);
await click("header.d-header"); // close the menu
updateCurrentUser({ is_anonymous: false, trust_level: 2 });
updateCurrentUser({
is_anonymous: false,
trust_level: 2,
groups: [
AUTO_GROUPS.trust_level_0,
AUTO_GROUPS.trust_level_1,
AUTO_GROUPS.trust_level_2,
],
});
await click(".d-header-icons .current-user");
await click("#user-menu-button-profile");
@ -616,9 +629,17 @@ acceptance("User menu", function (needs) {
);
await click("header.d-header"); // close the menu
updateCurrentUser({ is_anonymous: true, trust_level: 2 });
updateCurrentUser({
is_anonymous: true,
trust_level: 2,
groups: [
AUTO_GROUPS.trust_level_0,
AUTO_GROUPS.trust_level_1,
AUTO_GROUPS.trust_level_2,
],
});
this.siteSettings.allow_anonymous_posting = false;
this.siteSettings.anonymous_posting_min_trust_level = 3;
this.siteSettings.anonymous_posting_allowed_groups = "3";
await click(".d-header-icons .current-user");
await click("#user-menu-button-profile");
@ -628,9 +649,19 @@ acceptance("User menu", function (needs) {
);
await click("header.d-header"); // close the menu
updateCurrentUser({ is_anonymous: false, trust_level: 4 });
updateCurrentUser({
is_anonymous: true,
trust_level: 4,
groups: [
AUTO_GROUPS.trust_level_0,
AUTO_GROUPS.trust_level_1,
AUTO_GROUPS.trust_level_2,
AUTO_GROUPS.trust_level_3,
AUTO_GROUPS.trust_level_4,
],
});
this.siteSettings.allow_anonymous_posting = false;
this.siteSettings.anonymous_posting_min_trust_level = 3;
this.siteSettings.anonymous_posting_allowed_groups = "3";
await click(".d-header-icons .current-user");
await click("#user-menu-button-profile");
@ -640,9 +671,17 @@ acceptance("User menu", function (needs) {
);
await click("header.d-header"); // close the menu
updateCurrentUser({ is_anonymous: false, trust_level: 2 });
updateCurrentUser({
is_anonymous: false,
trust_level: 2,
groups: [
AUTO_GROUPS.trust_level_0,
AUTO_GROUPS.trust_level_1,
AUTO_GROUPS.trust_level_2,
],
});
this.siteSettings.allow_anonymous_posting = true;
this.siteSettings.anonymous_posting_min_trust_level = 3;
this.siteSettings.anonymous_posting_allowed_groups = "3";
await click(".d-header-icons .current-user");
await click("#user-menu-button-profile");

View File

@ -1,4 +1,7 @@
import { deepFreeze } from "discourse-common/lib/object";
import {
AUTO_GROUPS,
} from "discourse/lib/constants";
export default {
"/session/current.json": deepFreeze({
@ -30,18 +33,12 @@ export default {
can_review: true,
ignored_users: [],
groups: [
{
id: 10,
automatic: true,
name: "trust_level_0",
display_name: "trust_level_0",
},
{
id: 11,
automatic: true,
name: "trust_level_1",
display_name: "trust_level_1",
}
AUTO_GROUPS.admins,
AUTO_GROUPS.moderators,
AUTO_GROUPS.staff,
AUTO_GROUPS.trust_level_0,
AUTO_GROUPS.trust_level_1,
AUTO_GROUPS.trust_level_2,
],
user_option: {
external_links_in_new_tab: false,

View File

@ -3,6 +3,7 @@ import { setupRenderingTest as emberSetupRenderingTest } from "ember-qunit";
import $ from "jquery";
import QUnit, { test } from "qunit";
import { autoLoadModules } from "discourse/instance-initializers/auto-load-modules";
import { AUTO_GROUPS } from "discourse/lib/constants";
import Session from "discourse/models/session";
import Site from "discourse/models/site";
import TopicTrackingState from "discourse/models/topic-tracking-state";
@ -27,20 +28,7 @@ export function setupRenderingTest(hooks) {
name: "Robin Ward",
admin: false,
moderator: false,
groups: [
{
id: 10,
automatic: true,
name: "trust_level_0",
display_name: "trust_level_0",
},
{
id: 11,
automatic: true,
name: "trust_level_1",
display_name: "trust_level_1",
},
],
groups: [AUTO_GROUPS.trust_level_0, AUTO_GROUPS.trust_level_1],
user_option: {
timezone: "Australia/Brisbane",
},

View File

@ -1,4 +1,4 @@
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { createSiteSettingsFromPreloaded } from "discourse/services/site-settings";
const CLIENT_SETTING_TEST_OVERRIDES = {
title: "QUnit Discourse Tests",
@ -38,6 +38,6 @@ export function mergeSettings(other) {
}
export function resetSettings() {
siteSettings = new TrackedObject(ORIGINAL_CLIENT_SITE_SETTINGS);
siteSettings = createSiteSettingsFromPreloaded(ORIGINAL_CLIENT_SITE_SETTINGS);
return siteSettings;
}

View File

@ -25,7 +25,7 @@ class AnonymousShadowCreator
def get
return unless user
return unless SiteSetting.allow_anonymous_posting
return if user.trust_level < SiteSetting.anonymous_posting_min_trust_level
return if !user.in_any_groups?(SiteSetting.anonymous_posting_allowed_groups_map)
return if SiteSetting.must_approve_users? && !user.approved?
shadow = user.shadow_user

View File

@ -2177,6 +2177,7 @@ en:
allow_anonymous_posting: "Allow users to switch to anonymous mode"
allow_anonymous_likes: "Allow anonymous users to like posts"
anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting"
anonymous_posting_allowed_groups: "Groups that are allowed to enable anonymous posting"
anonymous_account_duration_minutes: "To protect anonymity create a new anonymous account every N minutes for each user. Example: if set to 600, as soon as 600 minutes elapse from last post AND user switches to anon, a new anonymous account is created."
hide_user_profiles_from_public: "Disable user cards, user profiles and user directory for anonymous users."
@ -2488,7 +2489,7 @@ en:
reply_by_email_address_is_empty: "You must set a 'reply by email address' before enabling reply by email."
email_polling_disabled: "You must enable either manual, POP3 polling or have a custom mail poller enabled before enabling reply by email."
user_locale_not_enabled: "You must first enable 'allow user locale' before enabling this setting."
personal_message_enabled_groups_invalid: "You must specify at least one group for this setting. If you do not want anyone except staff to send PMs, choose the staff group."
at_least_one_group_required: "You must specify at least one group for this setting."
invalid_regex: "Regex is invalid or not allowed."
invalid_regex_with_message: "The regex '%{regex}' has an error: %{message}"
email_editable_enabled: "You must disable 'email editable' before enabling this setting."

View File

@ -682,6 +682,13 @@ users:
default: 1
enum: "TrustLevelSetting"
client: true
anonymous_posting_allowed_groups:
default: "11" # auto group trust_level_1
type: group_list
client: true
allow_any: false
refresh: true
validator: "AtLeastOneGroupValidator"
anonymous_account_duration_minutes:
default: 10080
max: 99000
@ -860,7 +867,7 @@ posting:
client: true
allow_any: false
refresh: true
validator: "PersonalMessageEnabledGroupsValidator"
validator: "AtLeastOneGroupValidator"
editing_grace_period: 300
editing_grace_period_max_diff: 100
editing_grace_period_max_diff_high_trust: 400

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class MigrateTlToGroupSettingsAnonymousPostingMinTl < ActiveRecord::Migration[7.0]
def up
anonymous_posting_min_trust_level_raw =
DB.query_single(
"SELECT value FROM site_settings WHERE name = 'anonymous_posting_min_trust_level'",
).first
# Default for old setting is TL1, we only need to do anything if it's been changed in the DB.
if anonymous_posting_min_trust_level_raw.present?
# Matches Group::AUTO_GROUPS to the trust levels.
anonymous_posting_allowed_groups =
case anonymous_posting_min_trust_level_raw
when "0"
"10"
when "1"
"11"
when "2"
"12"
when "3"
"13"
when "4"
"14"
end
# Data_type 20 is group_list.
exec(<<~SQL, setting: anonymous_posting_allowed_groups)
INSERT INTO site_settings(name, value, data_type, created_at, updated_at)
VALUES('anonymous_posting_allowed_groups', :setting, '20', NOW(), NOW())
SQL
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -8,6 +8,7 @@ module SiteSettings::DeprecatedSettings
# [<old setting>, <new_setting>, <override>, <version to drop>]
["search_tokenize_chinese_japanese_korean", "search_tokenize_chinese", true, "2.9"],
["default_categories_regular", "default_categories_normal", true, "3.0"],
["anonymous_posting_min_trust_level", "anonymous_posting_allowed_groups", false, "3.3"],
]
def setup_deprecated_methods

View File

@ -133,6 +133,18 @@ end
task "javascript:update_constants" => :environment do
task_name = "update_constants"
auto_groups =
Group::AUTO_GROUPS.inject({}) do |result, (group_name, group_id)|
result.merge(
group_name => {
id: group_id,
automatic: true,
name: group_name,
display_name: group_name,
},
)
end
write_template("discourse/app/lib/constants.js", task_name, <<~JS)
export const SEARCH_PRIORITIES = #{Searchable::PRIORITIES.to_json};
@ -147,6 +159,8 @@ task "javascript:update_constants" => :environment do
export const SIDEBAR_SECTION = {
max_title_length: #{SidebarSection::MAX_TITLE_LENGTH},
}
export const AUTO_GROUPS = #{auto_groups.to_json};
JS
pretty_notifications = Notification.types.map { |n| " #{n[0]}: #{n[1]}," }.join("\n")

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class PersonalMessageEnabledGroupsValidator
class AtLeastOneGroupValidator
def initialize(opts = {})
@opts = opts
end
@ -10,6 +10,6 @@ class PersonalMessageEnabledGroupsValidator
end
def error_message
I18n.t("site_settings.errors.personal_message_enabled_groups_invalid")
I18n.t("site_settings.errors.at_least_one_group_required")
end
end

View File

@ -67,8 +67,9 @@ export default class Chat extends Service {
return (
this.currentUser.staff ||
this.currentUser.isInAnyGroups(
this.siteSettings.groupSettingArray("direct_message_enabled_groups")
this.siteSettings.userInAnyGroups(
"direct_message_enabled_groups",
this.currentUser
)
);
}

View File

@ -128,6 +128,7 @@ RSpec.describe User do
it "should initiate bot for real user only" do
user = Fabricate(:user, trust_level: 1)
Group.refresh_automatic_groups!
shadow = AnonymousShadowCreator.get(user)
expect(TopicAllowedUser.where(user_id: shadow.id).count).to eq(0)

View File

@ -153,6 +153,7 @@ RSpec.describe ApplicationController do
it "should not redirect anonymous users when enforce_second_factor is 'all'" do
SiteSetting.enforce_second_factor = "all"
SiteSetting.allow_anonymous_posting = true
Group.refresh_automatic_groups!
sign_in(user)
post "/u/toggle-anon.json"

View File

@ -607,6 +607,7 @@ RSpec.describe UsersController do
user = sign_in(Fabricate(:user))
user.trust_level = 1
user.save!
Group.refresh_automatic_groups!
post "/u/toggle-anon.json"
expect(response.status).to eq(200)

View File

@ -8,10 +8,16 @@ RSpec.describe AnonymousShadowCreator do
context "when anonymous posting is enabled" do
fab!(:user) { Fabricate(:user, trust_level: 3) }
before { SiteSetting.allow_anonymous_posting = true }
before do
SiteSetting.allow_anonymous_posting = true
SiteSetting.anonymous_posting_allowed_groups = "11"
Group.refresh_automatic_groups!
end
it "returns no shadow if trust level is not met" do
expect(AnonymousShadowCreator.get(Fabricate.build(:user, trust_level: 0))).to eq(nil)
it "returns no shadow if the user is not in a group that is allowed to anonymously post" do
user = Fabricate(:user, trust_level: 0)
Group.refresh_automatic_groups!
expect(AnonymousShadowCreator.get(user)).to eq(nil)
end
it "returns no shadow if must_approve_users is true and user is not approved" do