mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-05 09:21:11 +00:00
Compare commits
38 Commits
adeae58b61
...
d4d5ae215b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d4d5ae215b | ||
![]() |
14cb5ff881 | ||
![]() |
bc952ebde9 | ||
![]() |
c1542643f5 | ||
![]() |
733587c4bf | ||
![]() |
e259a67151 | ||
![]() |
a5b2b564ab | ||
![]() |
6d82a3d2a5 | ||
![]() |
879fd3406e | ||
![]() |
50298a1ce1 | ||
![]() |
d114645e60 | ||
![]() |
8bd9d60cdf | ||
![]() |
cf1db1a025 | ||
![]() |
4ce99fcf55 | ||
![]() |
1f6f3bf01a | ||
![]() |
7af438159a | ||
![]() |
7e1eeff268 | ||
![]() |
2b98d29942 | ||
![]() |
6a40447759 | ||
![]() |
63d0efa0c2 | ||
![]() |
bdf102df4e | ||
![]() |
e5f4362b8f | ||
![]() |
26081d66fd | ||
![]() |
a8f5e3fa62 | ||
![]() |
f5c14b753a | ||
![]() |
1073956fbc | ||
![]() |
673d875a95 | ||
![]() |
7a957b1f49 | ||
![]() |
6fda7a9f56 | ||
![]() |
1c52aa76eb | ||
![]() |
df97c75d34 | ||
![]() |
659c695a5a | ||
![]() |
86e38dc4f6 | ||
![]() |
7b1f26eea2 | ||
![]() |
6cc6c36c44 | ||
![]() |
16b19f48cc | ||
![]() |
4e1d61ecd0 | ||
![]() |
218a9e70c5 |
|
@ -109,3 +109,6 @@ FETCH_REPLIES_MAX_SINGLE=500
|
|||
|
||||
# Max number of replies Collection pages to fetch - total
|
||||
FETCH_REPLIES_MAX_PAGES=500
|
||||
|
||||
# MFA Required for Users
|
||||
REQUIRE_MULTI_FACTOR_AUTH=false
|
|
@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
|
|||
include DatabaseHelper
|
||||
include AuthorizedFetchHelper
|
||||
include SelfDestructHelper
|
||||
include MfaForceConcern
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
|
|
|
@ -18,6 +18,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
skip_before_action :check_self_destruct!, only: [:edit, :update]
|
||||
skip_before_action :require_functional!, only: [:edit, :update]
|
||||
skip_before_action :check_mfa_requirement, only: [:edit, :update]
|
||||
|
||||
def new
|
||||
super(&:build_invite_request)
|
||||
|
@ -100,12 +101,13 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_invite
|
||||
@invite = begin
|
||||
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
||||
invite if invite&.valid_for_use?
|
||||
if invite_code.present?
|
||||
Invite.find_by(code: invite_code)
|
||||
elsif params[:invite_code].present?
|
||||
Invite.find_by(code: params[:invite_code])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -132,15 +134,13 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
def require_rules_acceptance!
|
||||
return if @rules.empty? || (session[:accept_token].present? && params[:accept] == session[:accept_token])
|
||||
|
||||
@accept_token = session[:accept_token] = SecureRandom.hex
|
||||
@invite_code = invite_code
|
||||
|
||||
set_locale { render :rules }
|
||||
session[:accept_token] = SecureRandom.hex(16)
|
||||
redirect_to new_user_registration_path(accept: session[:accept_token])
|
||||
end
|
||||
|
||||
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
|
||||
if params[:action] == 'create'
|
||||
false # Disable flash messages for sign-up
|
||||
false
|
||||
else
|
||||
super
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
skip_before_action :require_no_authentication, only: [:create]
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :update_user_sign_in
|
||||
skip_before_action :check_mfa_requirement, only: [:destroy]
|
||||
|
||||
around_action :preserve_stored_location, only: :destroy, if: :continue_after?
|
||||
|
||||
|
@ -199,12 +200,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
def respond_to_on_destroy
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
redirect_to: after_sign_out_path_for(resource_name),
|
||||
}, status: 200
|
||||
end
|
||||
format.all { super }
|
||||
format.any(*navigational_formats) { redirect_to after_sign_out_path_for(:user) }
|
||||
format.all { head 204 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ class Auth::SetupController < ApplicationController
|
|||
before_action :set_user
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :check_mfa_requirement
|
||||
|
||||
def show; end
|
||||
|
||||
|
|
27
app/controllers/concerns/mfa_force_concern.rb
Normal file
27
app/controllers/concerns/mfa_force_concern.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MfaForceConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :check_mfa_requirement, if: :user_signed_in?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_mfa_requirement
|
||||
return unless mfa_force_enabled?
|
||||
return if current_user.otp_enabled?
|
||||
|
||||
flash[:alert] = I18n.t('require_multi_factor_auth.required_message')
|
||||
redirect_to settings_otp_authentication_path
|
||||
end
|
||||
|
||||
def mfa_force_enabled?
|
||||
mfa_config[:force_enabled]
|
||||
end
|
||||
|
||||
def mfa_config
|
||||
@mfa_config ||= Rails.application.config_for(:mfa)
|
||||
end
|
||||
end
|
|
@ -6,6 +6,7 @@ module Settings
|
|||
include ChallengableConcern
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :check_mfa_requirement
|
||||
|
||||
before_action :require_challenge!
|
||||
before_action :ensure_otp_secret
|
||||
|
|
|
@ -6,6 +6,7 @@ module Settings
|
|||
include ChallengableConcern
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :check_mfa_requirement
|
||||
|
||||
before_action :verify_otp_not_enabled, only: [:show]
|
||||
before_action :require_challenge!, only: [:create]
|
||||
|
|
|
@ -6,6 +6,7 @@ module Settings
|
|||
|
||||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :check_mfa_requirement
|
||||
|
||||
before_action :require_challenge!, only: :disable
|
||||
before_action :require_otp_enabled
|
||||
|
|
|
@ -8,6 +8,7 @@ const meta = {
|
|||
component: Button,
|
||||
args: {
|
||||
secondary: false,
|
||||
plain: false,
|
||||
compact: false,
|
||||
dangerous: false,
|
||||
disabled: false,
|
||||
|
@ -57,6 +58,14 @@ export const Secondary: Story = {
|
|||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Plain: Story = {
|
||||
args: {
|
||||
plain: true,
|
||||
children: 'Plain button',
|
||||
},
|
||||
play: buttonTest,
|
||||
};
|
||||
|
||||
export const Compact: Story = {
|
||||
args: {
|
||||
compact: true,
|
||||
|
@ -101,6 +110,14 @@ export const SecondaryDisabled: Story = {
|
|||
play: disabledButtonTest,
|
||||
};
|
||||
|
||||
export const PlainDisabled: Story = {
|
||||
args: {
|
||||
...Plain.args,
|
||||
disabled: true,
|
||||
},
|
||||
play: disabledButtonTest,
|
||||
};
|
||||
|
||||
const loadingButtonTest: Story['play'] = async ({
|
||||
args,
|
||||
canvas,
|
||||
|
|
|
@ -9,6 +9,7 @@ interface BaseProps
|
|||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
plain?: boolean;
|
||||
compact?: boolean;
|
||||
dangerous?: boolean;
|
||||
loading?: boolean;
|
||||
|
@ -35,6 +36,7 @@ export const Button: React.FC<Props> = ({
|
|||
disabled,
|
||||
block,
|
||||
secondary,
|
||||
plain,
|
||||
compact,
|
||||
dangerous,
|
||||
loading,
|
||||
|
@ -62,6 +64,7 @@ export const Button: React.FC<Props> = ({
|
|||
<button
|
||||
className={classNames('button', className, {
|
||||
'button-secondary': secondary,
|
||||
'button--plain': plain,
|
||||
'button--compact': compact,
|
||||
'button--block': block,
|
||||
'button--dangerous': dangerous,
|
||||
|
|
|
@ -201,6 +201,41 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.button--plain {
|
||||
color: $highlight-text-color;
|
||||
background: transparent;
|
||||
padding: 6px;
|
||||
|
||||
// The button has no outline, so we use negative margin to
|
||||
// visually align its label with its surroundings while maintaining
|
||||
// a generous click target
|
||||
margin-inline: -6px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
color: lighten($highlight-text-color, 4%);
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
opacity: 0.7;
|
||||
border-color: transparent;
|
||||
color: $ui-button-disabled-color;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
color: $ui-button-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.button-tertiary {
|
||||
background: transparent;
|
||||
padding: 6px 17px;
|
||||
|
|
|
@ -232,6 +232,15 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||
canQuote: {
|
||||
automaticApproval: approved_uris,
|
||||
},
|
||||
canReply: {
|
||||
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
},
|
||||
canLike: {
|
||||
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
},
|
||||
canAnnounce: {
|
||||
always: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -2151,3 +2151,7 @@ en:
|
|||
not_supported: This browser doesn't support security keys
|
||||
otp_required: To use security keys please enable two-factor authentication first.
|
||||
registered_on: Registered on %{date}
|
||||
|
||||
require_multi_factor_auth:
|
||||
required_message: The administrator of this site has configured as mandatory that users enable two-factor authentication due to security policies. Please configure your two-factor authentication to continue using the platform.
|
||||
security_policy: Security Policy Requirement
|
||||
|
|
|
@ -2149,4 +2149,4 @@ es:
|
|||
not_enabled: Aún no has activado WebAuthn
|
||||
not_supported: Este navegador no soporta claves de seguridad
|
||||
otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor.
|
||||
registered_on: Registrado el %{date}
|
||||
registered_on: Registrado el %{date}
|
15
config/mfa.yml
Normal file
15
config/mfa.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Multi-Factor Authentication configuration
|
||||
default: &default
|
||||
force_enabled: false
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
force_enabled: <%= ENV.fetch('REQUIRE_MULTI_FACTOR_AUTH', 'false') == 'true' %>
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
force_enabled: false
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
force_enabled: <%= ENV.fetch('REQUIRE_MULTI_FACTOR_AUTH', 'false') == 'true' %>
|
111
spec/controllers/concerns/mfa_force_concern_spec.rb
Normal file
111
spec/controllers/concerns/mfa_force_concern_spec.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MfaForceConcern do
|
||||
controller(ApplicationController) do
|
||||
def index
|
||||
render plain: 'OK'
|
||||
end
|
||||
end
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
routes.draw { get 'index' => 'anonymous#index' }
|
||||
end
|
||||
|
||||
describe 'MFA force functionality' do
|
||||
context 'when REQUIRE_MULTI_FACTOR_AUTH is enabled' do
|
||||
before do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
sign_in user, scope: :user
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has MFA enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'allows access to normal pages' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have MFA enabled' do
|
||||
before do
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
|
||||
it 'redirects to MFA setup page' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
get :index
|
||||
expect(response).to redirect_to(settings_otp_authentication_path)
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows the required message' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
get :index
|
||||
expect(flash[:alert]).to eq(I18n.t('require_multi_factor_auth.required_message'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessing MFA setup pages' do
|
||||
it 'allows access to OTP authentication page' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
allow(controller.request).to receive(:path).and_return('/settings/otp_authentication')
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows access to MFA confirmation page' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
allow(controller.request).to receive(:path).and_return('/settings/two_factor_authentication/confirmation')
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows access to logout' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
allow(controller.request).to receive(:path).and_return('/auth/sign_out')
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when REQUIRE_MULTI_FACTOR_AUTH is disabled' do
|
||||
before do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'false') do
|
||||
sign_in user, scope: :user
|
||||
user.update(otp_required_for_login: false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows access to normal pages' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'false') do
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not signed in' do
|
||||
it 'allows access to normal pages' do
|
||||
ClimateControl.modify(REQUIRE_MULTI_FACTOR_AUTH: 'true') do
|
||||
get :index
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11957,8 +11957,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"sass@npm:^1.62.1":
|
||||
version: 1.91.0
|
||||
resolution: "sass@npm:1.91.0"
|
||||
version: 1.92.0
|
||||
resolution: "sass@npm:1.92.0"
|
||||
dependencies:
|
||||
"@parcel/watcher": "npm:^2.4.1"
|
||||
chokidar: "npm:^4.0.0"
|
||||
|
@ -11969,7 +11969,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
sass: sass.js
|
||||
checksum: 10c0/5be1c98f7a618cb5f90b62f63d2aa0f78f9bf369c93ec7cd9880752a26b0ead19aa63cc341e8a26ce6c74d080baa5705f1685dff52fe6a3f28a7828ae50182b6
|
||||
checksum: 10c0/bdff9fa6988620e2a81962efdd016e3894d19934cfadc105cf41db767f59dd47afd8ff32840e612ef700cb67e19d9e83c108f1724eb8f0bef56c4877dbe6f14d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user