mirror of
https://github.com/discourse/discourse.git
synced 2025-08-17 18:04:11 +08:00
FIX: improve "read only" modes (#33521)
The reasons for these changes is https://meta.discourse.org/t/-/89605 broke and admins were not able to log back in if they had previously enabled the "read only" mode. Thus ensued a deep dive into how all the "read only" modes worked, which was made difficult due to the lack of tests. The "cornerstone" of this PR is the `read_only_mixin.rb` file which was improved to be able to differentiate between the "readonly" mode and the "staff writes only" mode. I then made use of the `allow_in_readonly_mode` and `allow_in_staff_writes_only_mode` method to **explicitely** list all the actions that should work in those modes. I also added the "readonly" mixin to the `WebhooksController` since it doesn't inherit from the `ApplicationController`. I improved the security of the `/u/admin-login` endpoint by always sending the same message no matter if we found or not an admin account with the provided email address. I added two system specs: 1. for ensuring that admins can log in via /u/admin-lgoin and then clicking the link in the email they received while the site is in readonly mode. 2. for ensuring the "staff writes only mode" is _actually_ tested by ensuring a moderator can log in and create a topic while the site is in that mode. Plenty of specs were updated to ensure 100% converage of the various "read only" modes.
This commit is contained in:
parent
0dcbbe0de4
commit
bccf4e0b53
22 changed files with 521 additions and 126 deletions
|
@ -8,7 +8,20 @@ class Admin::BackupsController < Admin::AdminController
|
|||
|
||||
before_action :ensure_backups_enabled
|
||||
skip_before_action :check_xhr, only: %i[index show logs check_backup_chunk upload_backup_chunk]
|
||||
skip_before_action :ensure_backups_enabled, only: %w[show status index email]
|
||||
skip_before_action :ensure_backups_enabled, only: %i[show status index email]
|
||||
|
||||
allow_in_readonly_mode :create,
|
||||
:cancel,
|
||||
:email,
|
||||
:destroy,
|
||||
:restore,
|
||||
:rollback,
|
||||
:readonly,
|
||||
:upload_backup_chunk,
|
||||
:create_multipart,
|
||||
:complete_multipart,
|
||||
:abort_multipart,
|
||||
:batch_presign_multipart_parts
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
|
|
@ -145,7 +145,7 @@ class ApplicationController < ActionController::Base
|
|||
rescue_from PG::ReadOnlySqlTransaction do |e|
|
||||
Discourse.received_postgres_readonly!
|
||||
Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}")
|
||||
rescue_with_handler(Discourse::ReadOnly.new) || raise
|
||||
rescue_with_handler(Discourse::ReadOnly) || raise
|
||||
end
|
||||
|
||||
rescue_from ActionController::ParameterMissing do |e|
|
||||
|
@ -580,9 +580,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def rate_limit_second_factor!(user)
|
||||
return if params[:second_factor_token].blank?
|
||||
|
||||
RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 6, 1.minute).performed!
|
||||
|
||||
RateLimiter.new(nil, "second-factor-min-#{user.username}", 6, 1.minute).performed! if user
|
||||
end
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@ class CategoriesController < ApplicationController
|
|||
]
|
||||
skip_before_action :verify_authenticity_token, only: %i[search]
|
||||
|
||||
# The front-end is POSTing data to this endpoint, but we're not modifying anything
|
||||
allow_in_readonly_mode :search
|
||||
|
||||
SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR = 1.5
|
||||
MIN_CATEGORIES_TOPICS = 5
|
||||
MAX_CATEGORIES_LIMIT = 25
|
||||
|
|
|
@ -42,8 +42,6 @@ class PresenceController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
raise Discourse::ReadOnly if @readonly_mode
|
||||
|
||||
client_id = params[:client_id]
|
||||
if !client_id.is_a?(String) || client_id.blank?
|
||||
raise Discourse::InvalidParameters.new(:client_id)
|
||||
|
|
|
@ -12,7 +12,8 @@ class SessionController < ApplicationController
|
|||
|
||||
skip_before_action :check_xhr, only: %i[second_factor_auth_show]
|
||||
|
||||
allow_in_staff_writes_only_mode :create, :email_login, :forgot_password
|
||||
allow_in_readonly_mode :email_login
|
||||
allow_in_staff_writes_only_mode :create, :forgot_password
|
||||
|
||||
ACTIVATE_USER_KEY = "activate_user"
|
||||
FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY = 6
|
||||
|
@ -107,7 +108,6 @@ class SessionController < ApplicationController
|
|||
|
||||
def become
|
||||
raise Discourse::InvalidAccess if Rails.env.production?
|
||||
raise Discourse::ReadOnly if @readonly_mode
|
||||
|
||||
if ENV["DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE"] != "1"
|
||||
return render plain: <<~TEXT, status: 403
|
||||
|
@ -166,7 +166,7 @@ class SessionController < ApplicationController
|
|||
|
||||
def sso_login
|
||||
raise Discourse::NotFound unless SiteSetting.enable_discourse_connect
|
||||
raise Discourse::ReadOnly if @readonly_mode && !staff_writes_only_mode?
|
||||
raise Discourse::ReadOnly if @readonly_mode && !@staff_writes_only_mode
|
||||
|
||||
params.require(:sso)
|
||||
params.require(:sig)
|
||||
|
@ -207,7 +207,7 @@ class SessionController < ApplicationController
|
|||
invite = validate_invitiation!(sso)
|
||||
|
||||
if user = sso.lookup_or_create_user(request.remote_ip)
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !user&.staff?
|
||||
|
||||
if user.suspended?
|
||||
render_sso_error(text: failed_to_login(user)[:error], status: 403)
|
||||
|
@ -332,7 +332,7 @@ class SessionController < ApplicationController
|
|||
|
||||
user = User.find_by_username_or_email(normalized_login_param)
|
||||
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !user&.staff?
|
||||
|
||||
rate_limit_second_factor!(user)
|
||||
|
||||
|
@ -446,11 +446,9 @@ class SessionController < ApplicationController
|
|||
|
||||
def email_login
|
||||
token = params[:token]
|
||||
matched_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])
|
||||
user = matched_token&.user
|
||||
user = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])&.user
|
||||
|
||||
check_local_login_allowed(user: user, check_login_via_email: true)
|
||||
|
||||
rate_limit_second_factor!(user)
|
||||
|
||||
if user.present? && !authenticate_second_factor(user).ok
|
||||
|
@ -458,12 +456,17 @@ class SessionController < ApplicationController
|
|||
end
|
||||
|
||||
if user = EmailToken.confirm(token, scope: EmailToken.scopes[:email_login])
|
||||
if @staff_writes_only_mode
|
||||
raise Discourse::ReadOnly unless user.staff?
|
||||
elsif @readonly_mode
|
||||
raise Discourse::ReadOnly unless user.admin?
|
||||
end
|
||||
|
||||
if login_not_approved_for?(user)
|
||||
return render json: login_not_approved
|
||||
elsif payload = login_error_check(user)
|
||||
return render json: payload
|
||||
else
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
|
||||
user.update_timezone_if_missing(params[:timezone])
|
||||
log_on_user(user)
|
||||
return render json: success_json
|
||||
|
@ -636,7 +639,7 @@ class SessionController < ApplicationController
|
|||
end
|
||||
|
||||
if user
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user.staff?
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !user.staff?
|
||||
enqueue_password_reset_for_user(user)
|
||||
else
|
||||
RateLimiter.new(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- encoding : utf-8 -*-
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Users::OmniauthCallbacksController < ApplicationController
|
||||
|
@ -13,6 +12,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
# will not have a CSRF token, however the payload is all validated so its safe
|
||||
skip_before_action :verify_authenticity_token, only: :complete
|
||||
|
||||
# These are usually GET requests but some providers use POST requests
|
||||
allow_in_staff_writes_only_mode :complete
|
||||
|
||||
def confirm_request
|
||||
|
@ -21,17 +21,15 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
end
|
||||
|
||||
def complete
|
||||
auth = request.env["omniauth.auth"]
|
||||
raise Discourse::NotFound unless request.env["omniauth.auth"]
|
||||
raise Discourse::ReadOnly if @readonly_mode && !staff_writes_only_mode?
|
||||
raise Discourse::ReadOnly if @readonly_mode && !@staff_writes_only_mode
|
||||
raise Discourse::NotFound unless auth = request.env["omniauth.auth"]
|
||||
|
||||
auth[:session] = session
|
||||
|
||||
authenticator = self.class.find_authenticator(params[:provider])
|
||||
|
||||
if session.delete(:auth_reconnect) && authenticator.can_connect_existing_user? && current_user
|
||||
path = persist_auth_token(auth)
|
||||
return redirect_to path
|
||||
return redirect_to persist_auth_token(auth)
|
||||
else
|
||||
DiscourseEvent.trigger(:before_auth, authenticator, auth, session, cookies, request)
|
||||
@auth_result = authenticator.after_authenticate(auth)
|
||||
|
@ -71,7 +69,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
|
||||
return render_auth_result_failure if @auth_result.failed?
|
||||
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !@auth_result.user&.staff?
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !@auth_result.user&.staff?
|
||||
|
||||
complete_response_data
|
||||
|
||||
|
|
|
@ -107,7 +107,8 @@ class UsersController < ApplicationController
|
|||
|
||||
before_action :add_noindex_header, only: %i[show my_redirect]
|
||||
|
||||
allow_in_staff_writes_only_mode :admin_login, :email_login, :password_reset_update
|
||||
allow_in_readonly_mode :admin_login
|
||||
allow_in_staff_writes_only_mode :email_login, :password_reset_update
|
||||
|
||||
MAX_RECENT_SEARCHES = 5
|
||||
|
||||
|
@ -862,7 +863,6 @@ class UsersController < ApplicationController
|
|||
RateLimiter.new(nil, "remove-password-hr-#{user.username}", 6, 1.hour).performed!
|
||||
|
||||
raise Discourse::NotFound if !user || !user.user_password
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !user.staff?
|
||||
raise Discourse::InvalidAccess if !session_is_trusted?
|
||||
|
||||
user.remove_password
|
||||
|
@ -872,16 +872,15 @@ class UsersController < ApplicationController
|
|||
|
||||
def password_reset_update
|
||||
expires_now
|
||||
token = params[:token]
|
||||
password_reset_find_user(token, committing_change: true)
|
||||
|
||||
token = params[:token]
|
||||
|
||||
password_reset_find_user(token, committing_change: true)
|
||||
rate_limit_second_factor!(@user)
|
||||
|
||||
# no point doing anything else if we can't even find
|
||||
# a user from the token
|
||||
if @user
|
||||
raise Discourse::ReadOnly if staff_writes_only_mode? && !@user.staff?
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !@user&.staff?
|
||||
|
||||
if @user
|
||||
if !secure_session["second-factor-#{token}"]
|
||||
second_factor_authentication_result =
|
||||
@user.authenticate_second_factor(params, secure_session)
|
||||
|
@ -1008,21 +1007,17 @@ class UsersController < ApplicationController
|
|||
RateLimiter.new(nil, "admin-login-hr-#{request.remote_ip}", 6, 1.hour).performed!
|
||||
RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed!
|
||||
|
||||
if user = User.with_email(params[:email]).admins.human_users.first
|
||||
if user = User.real.admins.with_email(params[:email]).first
|
||||
email_token =
|
||||
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login])
|
||||
token_string = email_token.token
|
||||
token_string += "?safe_mode=no_plugins,no_themes" if params["use_safe_mode"]
|
||||
Jobs.enqueue(
|
||||
:critical_user_email,
|
||||
type: "admin_login",
|
||||
user_id: user.id,
|
||||
email_token: token_string,
|
||||
)
|
||||
@message = I18n.t("admin_login.success")
|
||||
else
|
||||
@message = I18n.t("admin_login.errors.unknown_email_address")
|
||||
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]).token
|
||||
email_token += "?safe_mode=no_plugins,no_themes" if params["use_safe_mode"]
|
||||
Jobs.enqueue(:critical_user_email, type: "admin_login", user_id: user.id, email_token:)
|
||||
end
|
||||
|
||||
# NOTE: we don't check for `readonly` mode here because it might leak information about whether
|
||||
# an email address is a registered admin account.
|
||||
|
||||
@message = I18n.t("admin_login.acknowledgement", email: params[:email])
|
||||
end
|
||||
|
||||
render layout: "no_ember"
|
||||
|
@ -1040,13 +1035,15 @@ class UsersController < ApplicationController
|
|||
|
||||
RateLimiter.new(nil, "email-login-hour-#{request.remote_ip}", 6, 1.hour).performed!
|
||||
RateLimiter.new(nil, "email-login-min-#{request.remote_ip}", 3, 1.minute).performed!
|
||||
user = User.human_users.find_by_username_or_email(params[:login])
|
||||
user = User.real.find_by_username_or_email(params[:login])
|
||||
user_presence = user.present? && !user.staged
|
||||
|
||||
if user
|
||||
RateLimiter.new(nil, "email-login-hour-#{user.id}", 6, 1.hour).performed!
|
||||
RateLimiter.new(nil, "email-login-min-#{user.id}", 3, 1.minute).performed!
|
||||
|
||||
raise Discourse::ReadOnly if @staff_writes_only_mode && !user.staff?
|
||||
|
||||
if user_presence
|
||||
DiscourseEvent.trigger(:before_email_login, user)
|
||||
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "openssl"
|
||||
|
||||
class WebhooksController < ActionController::Base
|
||||
include ReadOnlyMixin
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
before_action :check_readonly_mode
|
||||
before_action :block_if_readonly_mode
|
||||
|
||||
rescue_from Discourse::ReadOnly do
|
||||
head :service_unavailable
|
||||
end
|
||||
|
||||
def mailgun
|
||||
return signature_failure if SiteSetting.mailgun_api_key.blank?
|
||||
|
||||
|
@ -279,7 +286,7 @@ class WebhooksController < ActionController::Base
|
|||
|
||||
begin
|
||||
public_key = OpenSSL::PKey::EC.new(Base64.decode64(SiteSetting.sendgrid_verification_key))
|
||||
rescue StandardError => err
|
||||
rescue StandardError
|
||||
Rails.logger.error("Invalid Sendgrid verification key")
|
||||
return false
|
||||
end
|
||||
|
|
|
@ -5307,10 +5307,7 @@ en:
|
|||
badge_title_metadata: "%{display_name} badge on %{site_title}"
|
||||
|
||||
admin_login:
|
||||
success: "Email Sent"
|
||||
errors:
|
||||
unknown_email_address: "Unknown email address."
|
||||
invalid_token: "Invalid token."
|
||||
acknowledgement: "If an admin account with the email address %{email} exists, you will receive an email with a link to log in."
|
||||
email_input: "Admin Email"
|
||||
submit_button: "Send Email"
|
||||
safe_mode: "Safe Mode: disable all themes/plugins when logging in"
|
||||
|
|
|
@ -109,13 +109,14 @@ class ApplicationLayoutPreloader
|
|||
end
|
||||
|
||||
def preload_anonymous_data
|
||||
check_readonly_mode if @readonly_mode.nil?
|
||||
@preloaded["site"] = Site.json_for(@guardian)
|
||||
@preloaded["siteSettings"] = SiteSetting.client_settings_json
|
||||
@preloaded["customHTML"] = custom_html_json
|
||||
@preloaded["banner"] = banner_json
|
||||
@preloaded["customEmoji"] = custom_emoji
|
||||
@preloaded["isReadOnly"] = get_or_check_readonly_mode.to_json
|
||||
@preloaded["isStaffWritesOnly"] = get_or_check_staff_writes_only_mode.to_json
|
||||
@preloaded["isReadOnly"] = @readonly_mode.to_json
|
||||
@preloaded["isStaffWritesOnly"] = @staff_writes_only_mode.to_json
|
||||
@preloaded["activatedThemes"] = activated_themes_json
|
||||
end
|
||||
|
||||
|
|
|
@ -2,21 +2,29 @@
|
|||
|
||||
module ReadOnlyMixin
|
||||
module ClassMethods
|
||||
def actions_allowed_in_readonly_mode
|
||||
@actions_allowed_in_readonly_mode ||= []
|
||||
end
|
||||
|
||||
def actions_allowed_in_staff_writes_only_mode
|
||||
@actions_allowed_in_staff_writes_only_mode ||= []
|
||||
end
|
||||
|
||||
def allow_in_readonly_mode(*actions)
|
||||
actions_allowed_in_readonly_mode.concat(actions.map(&:to_sym))
|
||||
end
|
||||
|
||||
def allow_in_staff_writes_only_mode(*actions)
|
||||
actions_allowed_in_staff_writes_only_mode.concat(actions.map(&:to_sym))
|
||||
end
|
||||
|
||||
def allowed_in_staff_writes_only_mode?(action_name)
|
||||
actions_allowed_in_staff_writes_only_mode.include?(action_name.to_sym)
|
||||
def allowed_in_readonly_mode?(action)
|
||||
actions_allowed_in_readonly_mode.include?(action.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
def staff_writes_only_mode?
|
||||
@staff_writes_only_mode
|
||||
def allowed_in_staff_writes_only_mode?(action)
|
||||
actions_allowed_in_staff_writes_only_mode.include?(action.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
def check_readonly_mode
|
||||
|
@ -32,34 +40,25 @@ module ReadOnlyMixin
|
|||
end
|
||||
end
|
||||
|
||||
def get_or_check_readonly_mode
|
||||
check_readonly_mode if @readonly_mode.nil?
|
||||
@readonly_mode
|
||||
end
|
||||
|
||||
def get_or_check_staff_writes_only_mode
|
||||
check_readonly_mode if @staff_writes_only_mode.nil?
|
||||
@staff_writes_only_mode
|
||||
end
|
||||
|
||||
def add_readonly_header
|
||||
response.headers["Discourse-Readonly"] = "true" if @readonly_mode
|
||||
end
|
||||
|
||||
def allowed_in_readonly_mode?
|
||||
self.class.allowed_in_readonly_mode?(action_name)
|
||||
end
|
||||
|
||||
def allowed_in_staff_writes_only_mode?
|
||||
self.class.allowed_in_staff_writes_only_mode?(action_name)
|
||||
end
|
||||
|
||||
def block_if_readonly_mode
|
||||
return if request.fullpath.start_with?(path "/admin/backups")
|
||||
return if request.fullpath.start_with?(path "/categories/search")
|
||||
return if request.get? || request.head?
|
||||
|
||||
if @staff_writes_only_mode
|
||||
raise Discourse::ReadOnly.new if !current_user&.staff? && !allowed_in_staff_writes_only_mode?
|
||||
elsif @readonly_mode
|
||||
raise Discourse::ReadOnly.new
|
||||
if @staff_writes_only_mode && (allowed_in_staff_writes_only_mode? || current_user&.staff?)
|
||||
return
|
||||
end
|
||||
return if !@readonly_mode || allowed_in_readonly_mode?
|
||||
raise Discourse::ReadOnly
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
|
|
|
@ -143,7 +143,7 @@ RSpec.describe Middleware::RequestTracker do
|
|||
# /srv/status is never a tracked view because content-type is text/plain
|
||||
data =
|
||||
Middleware::RequestTracker.get_data(
|
||||
env("HTTP_USER_AGENT" => "kube-probe/1.18", "REQUEST_URI" => "/srv/status?shutdown_ok=1"),
|
||||
env("HTTP_USER_AGENT" => "kube-probe/1.18", "REQUEST_URI" => "/srv/status"),
|
||||
["200", { "Content-Type" => "text/plain" }],
|
||||
0.1,
|
||||
)
|
||||
|
|
59
spec/lib/read_only_mixin_spec.rb
Normal file
59
spec/lib/read_only_mixin_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe ReadOnlyMixin do
|
||||
before { Rails.application.eager_load! }
|
||||
|
||||
it "allows only these actions in readonly mode" do
|
||||
controllers_with_readonly_actions = []
|
||||
|
||||
ApplicationController.descendants.each do |controller_class|
|
||||
controller_class.actions_allowed_in_readonly_mode&.each do |action|
|
||||
controllers_with_readonly_actions << [controller_class, action]
|
||||
end
|
||||
end
|
||||
|
||||
expect(controllers_with_readonly_actions).to contain_exactly(
|
||||
# All the /admin/backups actions that modify data
|
||||
[Admin::BackupsController, :readonly],
|
||||
[Admin::BackupsController, :create],
|
||||
[Admin::BackupsController, :cancel],
|
||||
[Admin::BackupsController, :restore],
|
||||
[Admin::BackupsController, :rollback],
|
||||
[Admin::BackupsController, :destroy],
|
||||
[Admin::BackupsController, :email],
|
||||
[Admin::BackupsController, :upload_backup_chunk],
|
||||
[Admin::BackupsController, :create_multipart],
|
||||
[Admin::BackupsController, :abort_multipart],
|
||||
[Admin::BackupsController, :complete_multipart],
|
||||
[Admin::BackupsController, :batch_presign_multipart_parts],
|
||||
# Search uses a POST request but doesn't modify any data
|
||||
[CategoriesController, :search],
|
||||
# Allows admins to log in (via email) when the site is in readonly mode (cf. https://meta.discourse.org/t/-/89605)
|
||||
[SessionController, :email_login],
|
||||
[UsersController, :admin_login],
|
||||
)
|
||||
end
|
||||
|
||||
it "allows only these actions in staff writes only mode" do
|
||||
controllers_with_staff_writes_only_actions = []
|
||||
|
||||
ApplicationController.descendants.each do |controller_class|
|
||||
controller_class.actions_allowed_in_staff_writes_only_mode&.each do |action|
|
||||
controllers_with_staff_writes_only_actions << [controller_class, action]
|
||||
end
|
||||
end
|
||||
|
||||
expect(controllers_with_staff_writes_only_actions).to contain_exactly(
|
||||
# Allows staff to log in using email/username and password
|
||||
[SessionController, :create],
|
||||
# Allows staff to reset their password (part 1/2)
|
||||
[SessionController, :forgot_password],
|
||||
# Allows staff to log in via OAuth
|
||||
[Users::OmniauthCallbacksController, :complete],
|
||||
# Allows staff to log in via email link
|
||||
[UsersController, :email_login],
|
||||
# Allows staff to reset their password (part 2/2)
|
||||
[UsersController, :password_reset_update],
|
||||
)
|
||||
end
|
||||
end
|
|
@ -155,6 +155,16 @@ RSpec.describe Admin::BackupsController do
|
|||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "starts a backup" do
|
||||
BackupRestore.expects(:backup!)
|
||||
post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "with rate limiting enabled" do
|
||||
before { RateLimiter.enable }
|
||||
|
||||
|
@ -284,6 +294,18 @@ RSpec.describe Admin::BackupsController do
|
|||
delete "/admin/backups/#{backup_filename}.json"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "removes the backup if found" do
|
||||
create_backup_files(backup_filename)
|
||||
|
||||
expect { delete "/admin/backups/#{backup_filename}.json" }.to change {
|
||||
UserHistory.where(action: UserHistory.actions[:backup_destroy]).count
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "backup deletion not allowed" do
|
||||
|
@ -367,6 +389,16 @@ RSpec.describe Admin::BackupsController do
|
|||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "starts a restore" do
|
||||
BackupRestore.expects(:restore!)
|
||||
post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" }
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "backup restoration not allowed" do
|
||||
|
@ -527,6 +559,29 @@ RSpec.describe Admin::BackupsController do
|
|||
expect(response.status).to eq(200)
|
||||
expect(response.body).to eq("")
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "uploads the file successfully" do
|
||||
described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true)
|
||||
|
||||
post "/admin/backups/upload.json",
|
||||
params: {
|
||||
resumableFilename: "foo-1234.tar.gz",
|
||||
resumableTotalSize: 1,
|
||||
resumableIdentifier: "test",
|
||||
resumableChunkNumber: "1",
|
||||
resumableChunkSize: "1",
|
||||
resumableCurrentChunkSize: "1",
|
||||
file: fixture_file_upload(Tempfile.new),
|
||||
}
|
||||
|
||||
expect_job_enqueued(job: :backup_chunks_merger)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "completing an upload by enqueuing backup_chunks_merger" do
|
||||
|
@ -748,6 +803,16 @@ RSpec.describe Admin::BackupsController do
|
|||
get "/admin/backups/rollback.json"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "should rollback the restore" do
|
||||
BackupRestore.expects(:rollback!)
|
||||
post "/admin/backups/rollback.json"
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "backup rollback not allowed" do
|
||||
|
@ -788,6 +853,18 @@ RSpec.describe Admin::BackupsController do
|
|||
get "/admin/backups/cancel.json"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "should cancel an backup" do
|
||||
BackupRestore.expects(:cancel!)
|
||||
|
||||
delete "/admin/backups/cancel.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "backup cancellation not allowed" do
|
||||
|
@ -845,6 +922,18 @@ RSpec.describe Admin::BackupsController do
|
|||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "enqueues email job" do
|
||||
create_backup_files(backup_filename)
|
||||
|
||||
expect { put "/admin/backups/#{backup_filename}.json" }.to change {
|
||||
Jobs::DownloadBackupEmail.jobs.size
|
||||
}.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "returns 404 when the backup does not exist" do
|
||||
put "/admin/backups/#{backup_filename}.json"
|
||||
|
||||
|
@ -973,6 +1062,23 @@ RSpec.describe Admin::BackupsController do
|
|||
expect(external_upload_stub.exists?).to eq(true)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "creates the multipart upload" do
|
||||
stub_create_multipart_backup_request
|
||||
|
||||
post "/admin/backups/create-multipart.json",
|
||||
params: {
|
||||
file_name: "test.tar.gz",
|
||||
upload_type: upload_type,
|
||||
file_size: 4098,
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when backup of same filename already exists" do
|
||||
let(:backup_file_exists_response) { { status: 200, body: "" } }
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ RSpec.describe CategoriesController do
|
|||
end
|
||||
|
||||
it "does not return subcategories without query param" do
|
||||
subcategory = Fabricate(:category, user: admin, parent_category: category)
|
||||
Fabricate(:category, user: admin, parent_category: category)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
|
@ -343,8 +343,8 @@ RSpec.describe CategoriesController do
|
|||
category1 = Fabricate(:category)
|
||||
category2 = Fabricate(:category)
|
||||
upload = Fabricate(:upload)
|
||||
topic1 = Fabricate(:topic, category: category1)
|
||||
topic2 = Fabricate(:topic, category: category1, image_upload: upload)
|
||||
Fabricate(:topic, category: category1)
|
||||
Fabricate(:topic, category: category1, image_upload: upload)
|
||||
|
||||
CategoryFeaturedTopic.feature_topics
|
||||
SiteSetting.desktop_category_page_style = "categories_with_featured_topics"
|
||||
|
@ -363,8 +363,7 @@ RSpec.describe CategoriesController do
|
|||
response.parsed_body["category_list"]["categories"].find { |c| c["id"] == category1.id }
|
||||
expect(category_response["topics"].count).to eq(2)
|
||||
|
||||
upload = Fabricate(:upload)
|
||||
topic3 = Fabricate(:topic, category: category2, image_upload: upload)
|
||||
Fabricate(:topic, category: category2, image_upload: Fabricate(:upload))
|
||||
CategoryFeaturedTopic.feature_topics
|
||||
|
||||
second_request_queries =
|
||||
|
@ -689,14 +688,13 @@ RSpec.describe CategoriesController do
|
|||
|
||||
it "returns errors when there is a name conflict while moving a category into another" do
|
||||
parent_category = Fabricate(:category, name: "Parent", user: admin)
|
||||
other_category =
|
||||
Fabricate(
|
||||
:category,
|
||||
name: category.name,
|
||||
user: admin,
|
||||
parent_category: parent_category,
|
||||
slug: "a-different-slug",
|
||||
)
|
||||
Fabricate(
|
||||
:category,
|
||||
name: category.name,
|
||||
user: admin,
|
||||
parent_category:,
|
||||
slug: "a-different-slug",
|
||||
)
|
||||
|
||||
put "/categories/#{category.id}.json", params: { parent_category_id: parent_category.id }
|
||||
|
||||
|
@ -704,7 +702,7 @@ RSpec.describe CategoriesController do
|
|||
end
|
||||
|
||||
it "returns 422 if email_in address is already in use for other category" do
|
||||
_other_category = Fabricate(:category, name: "Other", email_in: "mail@example.com")
|
||||
Fabricate(:category, name: "Other", email_in: "mail@example.com")
|
||||
|
||||
put "/categories/#{category.id}.json",
|
||||
params: {
|
||||
|
@ -1569,6 +1567,22 @@ RSpec.describe CategoriesController do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when in readonly mode" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "works" do
|
||||
post "/categories/search.json", params: { term: "" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["categories"].map { |c| c["id"] }).to contain_exactly(
|
||||
SiteSetting.uncategorized_category_id,
|
||||
category.id,
|
||||
subcategory.id,
|
||||
category2.id,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#hierachical_search" do
|
||||
|
|
|
@ -16,7 +16,7 @@ RSpec.describe ForumsController do
|
|||
end
|
||||
|
||||
it "returns a readonly header if the site is in staff-writes-only mode" do
|
||||
Discourse.stubs(:staff_writes_only_mode?).returns(true)
|
||||
Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY)
|
||||
get "/srv/status"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.headers["Discourse-Readonly"]).to eq("true")
|
||||
|
@ -43,10 +43,5 @@ RSpec.describe ForumsController do
|
|||
expect(response.status).to eq(200)
|
||||
expect(response.body).not_to include("not match")
|
||||
end
|
||||
|
||||
it "returns a 200 response when given shutdown_ok" do
|
||||
get "/srv/status?shutdown_ok=1"
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -224,34 +224,50 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
end
|
||||
|
||||
context "when in readonly mode" do
|
||||
it "should return a 503" do
|
||||
Discourse.enable_readonly_mode
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "returns a 503 (GET)" do
|
||||
get "/auth/google_oauth2/callback"
|
||||
expect(response.code).to eq("503")
|
||||
expect(response.status).to eq(503)
|
||||
end
|
||||
|
||||
it "returns a 503 (POST)" do
|
||||
post "/auth/google_oauth2/callback"
|
||||
expect(response.status).to eq(503)
|
||||
end
|
||||
end
|
||||
|
||||
context "when in staff writes only mode" do
|
||||
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
|
||||
|
||||
it "returns a 503 for non-staff" do
|
||||
it "returns a 503 for non-staff (GET)" do
|
||||
mock_auth(user.email, user.username, user.name)
|
||||
get "/auth/google_oauth2/callback.json"
|
||||
expect(response.status).to eq(503)
|
||||
logged_on_user = Discourse.current_user_provider.new(request.env).current_user
|
||||
|
||||
expect(logged_on_user).to eq(nil)
|
||||
expect(Discourse.current_user_provider.new(request.env).current_user).to eq(nil)
|
||||
end
|
||||
|
||||
it "completes for staff" do
|
||||
it "returns a 503 for non-staff (POST)" do
|
||||
mock_auth(user.email, user.username, user.name)
|
||||
post "/auth/google_oauth2/callback.json"
|
||||
expect(response.status).to eq(503)
|
||||
expect(Discourse.current_user_provider.new(request.env).current_user).to eq(nil)
|
||||
end
|
||||
|
||||
it "completes for admins (GET)" do
|
||||
user.update!(admin: true)
|
||||
mock_auth(user.email, user.username, user.name)
|
||||
get "/auth/google_oauth2/callback.json"
|
||||
expect(response.status).to eq(302)
|
||||
logged_on_user = Discourse.current_user_provider.new(request.env).current_user
|
||||
expect(Discourse.current_user_provider.new(request.env).current_user).to eq(user)
|
||||
end
|
||||
|
||||
expect(logged_on_user).not_to eq(nil)
|
||||
it "completes for moderators (POST)" do
|
||||
user.update!(moderator: true)
|
||||
mock_auth(user.email, user.username, user.name)
|
||||
post "/auth/google_oauth2/callback.json"
|
||||
expect(response.status).to eq(302)
|
||||
expect(Discourse.current_user_provider.new(request.env).current_user).to eq(user)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -635,7 +651,6 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
provider_uid: "123545",
|
||||
)
|
||||
|
||||
old_email = user.email
|
||||
user.update!(email: "email@example.com")
|
||||
|
||||
get "/auth/google_oauth2/callback.json"
|
||||
|
@ -911,8 +926,6 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||
SiteSetting.google_oauth2_hd_groups = true
|
||||
|
||||
stub_request(:post, "https://oauth2.googleapis.com/token").to_return do |request|
|
||||
jwt = Rack::Utils.parse_query(request.body)["assertion"]
|
||||
decoded_token = JWT.decode(jwt, private_key.public_key, true, { algorithm: "RS256" })
|
||||
{
|
||||
status: 200,
|
||||
body: { "access_token" => token, "type" => "bearer" }.to_json,
|
||||
|
|
|
@ -131,12 +131,34 @@ RSpec.describe SessionController do
|
|||
end
|
||||
|
||||
describe "#email_login" do
|
||||
let(:email_token) do
|
||||
Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login])
|
||||
end
|
||||
let(:email_token) { Fabricate(:email_token, user:, scope: EmailToken.scopes[:email_login]) }
|
||||
|
||||
before { SiteSetting.enable_local_logins_via_email = true }
|
||||
|
||||
context "when in readonly mode" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "allows admins to login" do
|
||||
user.update!(admin: true)
|
||||
post "/session/email-login/#{email_token.token}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(session[:current_user_id]).to eq(user.id)
|
||||
end
|
||||
|
||||
it "does not allow moderators to login" do
|
||||
user.update!(moderator: true)
|
||||
post "/session/email-login/#{email_token.token}.json"
|
||||
expect(response.status).to eq(503)
|
||||
expect(session[:current_user_id]).to eq(nil)
|
||||
end
|
||||
|
||||
it "does not allow regular users to login" do
|
||||
post "/session/email-login/#{email_token.token}.json"
|
||||
expect(response.status).to eq(503)
|
||||
expect(session[:current_user_id]).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when in staff writes only mode" do
|
||||
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
|
||||
|
||||
|
@ -147,7 +169,14 @@ RSpec.describe SessionController do
|
|||
expect(session[:current_user_id]).to eq(user.id)
|
||||
end
|
||||
|
||||
it "does not allow other users to login" do
|
||||
it "allows moderators to login" do
|
||||
user.update!(moderator: true)
|
||||
post "/session/email-login/#{email_token.token}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(session[:current_user_id]).to eq(user.id)
|
||||
end
|
||||
|
||||
it "does not allow regular users to login" do
|
||||
post "/session/email-login/#{email_token.token}.json"
|
||||
expect(response.status).to eq(503)
|
||||
expect(session[:current_user_id]).to eq(nil)
|
||||
|
|
|
@ -208,7 +208,7 @@ RSpec.describe UsersController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#remove password" do
|
||||
describe "#remove_password" do
|
||||
it "responds forbidden when not logged in" do
|
||||
put "/u/#{user.username}/remove-password.json"
|
||||
expect(response.status).to eq(403)
|
||||
|
@ -750,7 +750,7 @@ RSpec.describe UsersController do
|
|||
end
|
||||
|
||||
describe "#admin_login" do
|
||||
it "enqueues mail with admin email and sso enabled" do
|
||||
it "enqueues mail with admin email" do
|
||||
put "/u/admin-login", params: { email: admin.email }
|
||||
expect(response.status).to eq(200)
|
||||
expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
|
||||
|
@ -767,15 +767,26 @@ RSpec.describe UsersController do
|
|||
end
|
||||
|
||||
context "when email is incorrect" do
|
||||
it "should return the right response" do
|
||||
put "/u/admin-login", params: { email: "random" }
|
||||
it "doesn't enqueue the mail and returns the same message" do
|
||||
expect { put "/u/admin-login", params: { email: "random" } }.to_not change {
|
||||
Jobs::CriticalUserEmail.jobs.size
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to match(I18n.t("admin_login.acknowledgement", email: "random"))
|
||||
end
|
||||
end
|
||||
|
||||
response_body = response.body
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
expect(response_body).to match(I18n.t("admin_login.errors.unknown_email_address"))
|
||||
expect(response_body).to_not match(I18n.t("login.second_factor_description"))
|
||||
it "enqueues mail with admin email" do
|
||||
expect { put "/u/admin-login", params: { email: admin.email } }.to change {
|
||||
Jobs::CriticalUserEmail.jobs.size
|
||||
}.by(1)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to match(I18n.t("admin_login.acknowledgement", email: admin.email))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4725,8 +4736,7 @@ RSpec.describe UsersController do
|
|||
RateLimiter.enable
|
||||
freeze_time
|
||||
|
||||
user = post_user
|
||||
token = user.email_tokens.first
|
||||
post_user
|
||||
|
||||
6.times do |n|
|
||||
put "/u/update-activation-email.json",
|
||||
|
@ -4826,8 +4836,7 @@ RSpec.describe UsersController do
|
|||
RateLimiter.enable
|
||||
freeze_time
|
||||
|
||||
user = inactive_user
|
||||
token = user.email_tokens.first
|
||||
inactive_user
|
||||
|
||||
6.times do |n|
|
||||
put "/u/update-activation-email.json",
|
||||
|
@ -5777,6 +5786,40 @@ RSpec.describe UsersController do
|
|||
)
|
||||
end
|
||||
|
||||
describe "when staff writes only mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
|
||||
|
||||
it "enqueues the right email for moderator" do
|
||||
user1.update!(moderator: true)
|
||||
|
||||
expect { post "/u/email-login.json", params: { login: user1.email } }.to change {
|
||||
Jobs::CriticalUserEmail.jobs.count
|
||||
}.by(1)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["user_found"]).to eq(true)
|
||||
end
|
||||
|
||||
it "enqueues the right email for admin" do
|
||||
user1.update!(admin: true)
|
||||
|
||||
expect { post "/u/email-login.json", params: { login: user1.email } }.to change {
|
||||
Jobs::CriticalUserEmail.jobs.count
|
||||
}.by(1)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["user_found"]).to eq(true)
|
||||
end
|
||||
|
||||
it "does not enqueue the email for a regular user" do
|
||||
expect { post "/u/email-login.json", params: { login: user1.email } }.not_to change {
|
||||
Jobs::CriticalUserEmail.jobs.count
|
||||
}
|
||||
|
||||
expect(response.status).to eq(503)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when enable_local_logins_via_email is disabled" do
|
||||
before { SiteSetting.enable_local_logins_via_email = false }
|
||||
|
||||
|
@ -6430,7 +6473,6 @@ RSpec.describe UsersController do
|
|||
it "renames the key" do
|
||||
sign_in(user1)
|
||||
put "/u/rename_passkey/#{passkey.id}.json", params: { name: "new name" }
|
||||
response_parsed = response.parsed_body
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(passkey.reload.name).to eq("new name")
|
||||
|
|
|
@ -79,6 +79,31 @@ RSpec.describe WebhooksController do
|
|||
expect(email_log.bounce_error_code).to eq("5.1.1")
|
||||
expect(email_log.user.user_stat.bounce_score).to eq(SiteSetting.soft_bounce_score)
|
||||
end
|
||||
|
||||
context "when readonly mode is enabled" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "returns 503" do
|
||||
user = Fabricate(:user, email:)
|
||||
email_log = Fabricate(:email_log, user:, message_id:, to_address: email)
|
||||
|
||||
post "/webhooks/mailgun.json",
|
||||
params: {
|
||||
"token" => token,
|
||||
"timestamp" => timestamp,
|
||||
"event" => "dropped",
|
||||
"recipient" => email,
|
||||
"Message-Id" => "<#{message_id}>",
|
||||
"signature" => signature,
|
||||
"error" =>
|
||||
"smtp; 550-5.1.1 The email account that you tried to reach does not exist.",
|
||||
"code" => "5.1.1",
|
||||
}
|
||||
|
||||
expect(response.status).to eq(503)
|
||||
expect(email_log.reload.bounced).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sendgrid" do
|
||||
|
|
39
spec/system/admin_email_login_readonly_spec.rb
Normal file
39
spec/system/admin_email_login_readonly_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Admin email login in readonly mode", type: :system do
|
||||
fab!(:admin)
|
||||
|
||||
context "when site is in readonly mode" do
|
||||
before { Discourse.enable_readonly_mode }
|
||||
|
||||
it "allows admin to request email login from /u/admin-login page" do
|
||||
Jobs.run_immediately!
|
||||
ActionMailer::Base.deliveries.clear
|
||||
|
||||
page.visit "/u/admin-login"
|
||||
|
||||
fill_in "email", with: admin.email
|
||||
click_button "Send Email"
|
||||
|
||||
expect(page).to have_content(I18n.t("admin_login.acknowledgement", email: admin.email))
|
||||
|
||||
expect(ActionMailer::Base.deliveries.count).to eq(1)
|
||||
|
||||
mail = ActionMailer::Base.deliveries.last
|
||||
expect(mail.to).to contain_exactly(admin.email)
|
||||
expect(mail.body.to_s).to include("/session/email-login/")
|
||||
end
|
||||
|
||||
it "allows admin to login via email token during readonly mode" do
|
||||
email_token =
|
||||
admin.email_tokens.create!(email: admin.email, scope: EmailToken.scopes[:email_login])
|
||||
|
||||
page.visit "/session/email-login/#{email_token.token}"
|
||||
|
||||
find(".email-login-form .btn-primary").click
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
expect(page).to have_content(admin.username)
|
||||
end
|
||||
end
|
||||
end
|
59
spec/system/staff_writes_only_mode_spec.rb
Normal file
59
spec/system/staff_writes_only_mode_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Staff writes only mode", type: :system do
|
||||
password = SecureRandom.alphanumeric(20)
|
||||
|
||||
fab!(:moderator) { Fabricate(:moderator, password:) }
|
||||
fab!(:user) { Fabricate(:user, password:) }
|
||||
fab!(:topic) { Fabricate(:topic, user:) }
|
||||
fab!(:post) { Fabricate(:post, topic:, user:) }
|
||||
|
||||
let(:login_form) { PageObjects::Pages::Login.new }
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
|
||||
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
|
||||
|
||||
context "when moderator" do
|
||||
before { EmailToken.confirm(Fabricate(:email_token, user: moderator).token) }
|
||||
|
||||
it "can login and post during staff writes only mode" do
|
||||
login_form.open.fill(username: moderator.username, password:).click_login
|
||||
|
||||
expect(page).to have_css(".header-dropdown-toggle.current-user")
|
||||
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
title = "Test topic from moderator"
|
||||
body = "This is a test post created by a moderator during staff writes only mode."
|
||||
|
||||
composer.fill_title(title)
|
||||
composer.fill_content(body)
|
||||
|
||||
composer.create
|
||||
|
||||
expect(page).to have_content(title)
|
||||
expect(page).to have_content(body)
|
||||
end
|
||||
end
|
||||
|
||||
context "when regular user" do
|
||||
before { EmailToken.confirm(Fabricate(:email_token, user:).token) }
|
||||
|
||||
it "cannot login during staff writes only mode" do
|
||||
login_form.open.fill(username: user.username, password:).click_login
|
||||
|
||||
expect(page).not_to have_css(".header-dropdown-toggle.current-user")
|
||||
expect(page).to have_css("input#login-account-name")
|
||||
end
|
||||
|
||||
it "can view topics but sees staff only mode message when not logged in" do
|
||||
page.visit topic.url
|
||||
|
||||
expect(page).to have_content(topic.title)
|
||||
expect(page).to have_content(post.raw)
|
||||
expect(page).to have_content(I18n.t("js.staff_writes_only_mode.enabled"))
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue