Merge branch 'main' into glitch-soc/merge-upstream
Conflicts: - `app/controllers/concerns/sign_in_token_authentication_concern.rb`: Upstream removed this file, while glitch-soc had changes to deal with its theming system. Removed the file like upstream did.
This commit is contained in:
@ -225,22 +225,6 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do
|
||||
let!(:other_user) do
|
||||
Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
|
||||
end
|
||||
|
||||
before do
|
||||
post :create, params: { user: { email: other_user.email, password: other_user.password } }
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders two factor authentication page' do
|
||||
expect(controller).to render_template("two_factor")
|
||||
expect(controller).to render_template(partial: "_otp_authentication_form")
|
||||
end
|
||||
end
|
||||
|
||||
context 'using upcase email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email.upcase, password: user.password } }
|
||||
@ -266,21 +250,6 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid OTP, attempting to leverage previous half-login to bypass password auth' do
|
||||
let!(:other_user) do
|
||||
Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
|
||||
end
|
||||
|
||||
before do
|
||||
post :create, params: { user: { email: other_user.email, password: other_user.password } }
|
||||
post :create, params: { user: { email: user.email, otp_attempt: user.current_otp } }, session: { attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the server has an decryption error' do
|
||||
before do
|
||||
allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError)
|
||||
@ -401,126 +370,6 @@ RSpec.describe Auth::SessionsController, type: :controller do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when 2FA is disabled and IP is unfamiliar' do
|
||||
let!(:user) { Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', current_sign_in_at: 3.weeks.ago) }
|
||||
|
||||
before do
|
||||
request.remote_ip = '10.10.10.10'
|
||||
request.user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0'
|
||||
|
||||
allow(UserMailer).to receive(:sign_in_token).and_return(double('email', deliver_later!: nil))
|
||||
end
|
||||
|
||||
context 'using email and password' do
|
||||
before do
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders sign in token authentication page' do
|
||||
expect(controller).to render_template("sign_in_token")
|
||||
end
|
||||
|
||||
it 'generates sign in token' do
|
||||
expect(user.reload.sign_in_token).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends sign in token e-mail' do
|
||||
expect(UserMailer).to have_received(:sign_in_token)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do
|
||||
let!(:other_user) do
|
||||
Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32))
|
||||
end
|
||||
|
||||
before do
|
||||
post :create, params: { user: { email: other_user.email, password: other_user.password } }
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders sign in token authentication page' do
|
||||
expect(controller).to render_template("sign_in_token")
|
||||
end
|
||||
|
||||
it 'generates sign in token' do
|
||||
expect(user.reload.sign_in_token).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends sign in token e-mail' do
|
||||
expect(UserMailer).to have_received(:sign_in_token)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do
|
||||
let!(:other_user) do
|
||||
Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
|
||||
end
|
||||
|
||||
before do
|
||||
post :create, params: { user: { email: other_user.email, password: other_user.password } }
|
||||
post :create, params: { user: { email: user.email, password: user.password } }
|
||||
end
|
||||
|
||||
it 'renders sign in token authentication page' do
|
||||
expect(controller).to render_template("sign_in_token")
|
||||
end
|
||||
|
||||
it 'generates sign in token' do
|
||||
expect(user.reload.sign_in_token).to_not be_nil
|
||||
end
|
||||
|
||||
it 'sends sign in token e-mail' do
|
||||
expect(UserMailer).to have_received(:sign_in_token).with(user, any_args)
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid sign in token' do
|
||||
before do
|
||||
user.generate_sign_in_token && user.save
|
||||
post :create, params: { user: { sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'redirects to home' do
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
|
||||
it 'logs the user in' do
|
||||
expect(controller.current_user).to eq user
|
||||
end
|
||||
end
|
||||
|
||||
context 'using a valid sign in token, attempting to leverage previous half-login to bypass password auth' do
|
||||
let!(:other_user) do
|
||||
Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago)
|
||||
end
|
||||
|
||||
before do
|
||||
user.generate_sign_in_token && user.save
|
||||
post :create, params: { user: { email: other_user.email, password: other_user.password } }
|
||||
post :create, params: { user: { email: user.email, sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'using an invalid sign in token' do
|
||||
before do
|
||||
post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s }
|
||||
end
|
||||
|
||||
it 'shows a login error' do
|
||||
expect(flash[:alert]).to match I18n.t('users.invalid_sign_in_token')
|
||||
end
|
||||
|
||||
it "doesn't log the user in" do
|
||||
expect(controller.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #webauthn_options' do
|
||||
|
57
spec/lib/suspicious_sign_in_detector_spec.rb
Normal file
57
spec/lib/suspicious_sign_in_detector_spec.rb
Normal file
@ -0,0 +1,57 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SuspiciousSignInDetector do
|
||||
describe '#suspicious?' do
|
||||
let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) }
|
||||
let(:request) { double(remote_ip: remote_ip) }
|
||||
let(:remote_ip) { nil }
|
||||
|
||||
subject { described_class.new(user).suspicious?(request) }
|
||||
|
||||
context 'when user has 2FA enabled' do
|
||||
before do
|
||||
user.update!(otp_required_for_login: true)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when exact IP has been used before' do
|
||||
let(:remote_ip) { '1.1.1.1' }
|
||||
|
||||
before do
|
||||
user.update!(sign_up_ip: remote_ip)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when similar IP has been used before' do
|
||||
let(:remote_ip) { '1.1.2.2' }
|
||||
|
||||
before do
|
||||
user.update!(sign_up_ip: '1.1.1.1')
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(subject).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when IP is completely unfamiliar' do
|
||||
let(:remote_ip) { '2.2.2.2' }
|
||||
|
||||
before do
|
||||
user.update!(sign_up_ip: '1.1.1.1')
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(subject).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -87,8 +87,8 @@ class UserMailerPreview < ActionMailer::Preview
|
||||
UserMailer.appeal_approved(User.first, Appeal.last)
|
||||
end
|
||||
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token
|
||||
def sign_in_token
|
||||
UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc)
|
||||
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/suspicious_sign_in
|
||||
def suspicious_sign_in
|
||||
UserMailer.suspicious_sign_in(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc)
|
||||
end
|
||||
end
|
||||
|
@ -195,7 +195,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
|
||||
let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id]) }
|
||||
|
||||
context 'with a Note object' do
|
||||
let(:object) { note }
|
||||
let(:object) { note.merge(updated: '2021-09-08T22:39:25Z') }
|
||||
|
||||
it 'updates status' do
|
||||
existing_status.reload
|
||||
@ -211,7 +211,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
|
||||
id: "https://#{valid_domain}/@foo/1234/create",
|
||||
type: 'Create',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: note,
|
||||
object: note.merge(updated: '2021-09-08T22:39:25Z'),
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
require 'rails_helper'
|
||||
|
||||
def poll_option_json(name, votes)
|
||||
{ type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
|
||||
end
|
||||
|
||||
RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
|
||||
let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
|
||||
|
||||
@ -46,6 +50,180 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do
|
||||
expect(status.reload.spoiler_text).to eq 'Show more'
|
||||
end
|
||||
|
||||
context 'when the changes are only in sanitized-out HTML' do
|
||||
let!(:status) { Fabricate(:status, text: '<p>Hello world <a href="https://joinmastodon.org" rel="nofollow">joinmastodon.org</a></p>', account: Fabricate(:account, domain: 'example.com')) }
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
content: '<p>Hello world <a href="https://joinmastodon.org" rel="noreferrer">joinmastodon.org</a></p>',
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
subject.call(status, json)
|
||||
end
|
||||
|
||||
it 'does not create any edits' do
|
||||
expect(status.reload.edits).to be_empty
|
||||
end
|
||||
|
||||
it 'does not mark status as edited' do
|
||||
expect(status.edited?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has not been explicitly edited' do
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
content: 'Updated text',
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
subject.call(status, json)
|
||||
end
|
||||
|
||||
it 'does not create any edits' do
|
||||
expect(status.reload.edits).to be_empty
|
||||
end
|
||||
|
||||
it 'does not mark status as edited' do
|
||||
expect(status.reload.edited?).to be false
|
||||
end
|
||||
|
||||
it 'does not update the text' do
|
||||
expect(status.reload.text).to eq 'Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has not been explicitly edited and features a poll' do
|
||||
let(:account) { Fabricate(:account, domain: 'example.com') }
|
||||
let!(:expiration) { 10.days.from_now.utc }
|
||||
let!(:status) do
|
||||
Fabricate(:status,
|
||||
text: 'Hello world',
|
||||
account: account,
|
||||
poll_attributes: {
|
||||
options: %w(Foo Bar),
|
||||
account: account,
|
||||
multiple: false,
|
||||
hide_totals: false,
|
||||
expires_at: expiration
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/foo',
|
||||
type: 'Question',
|
||||
content: 'Hello world',
|
||||
endTime: expiration.iso8601,
|
||||
oneOf: [
|
||||
poll_option_json('Foo', 4),
|
||||
poll_option_json('Bar', 3),
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
subject.call(status, json)
|
||||
end
|
||||
|
||||
it 'does not create any edits' do
|
||||
expect(status.reload.edits).to be_empty
|
||||
end
|
||||
|
||||
it 'does not mark status as edited' do
|
||||
expect(status.reload.edited?).to be false
|
||||
end
|
||||
|
||||
it 'does not update the text' do
|
||||
expect(status.reload.text).to eq 'Hello world'
|
||||
end
|
||||
|
||||
it 'updates tallies' do
|
||||
expect(status.poll.reload.cached_tallies).to eq [4, 3]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status changes a poll despite being not explicitly marked as updated' do
|
||||
let(:account) { Fabricate(:account, domain: 'example.com') }
|
||||
let!(:expiration) { 10.days.from_now.utc }
|
||||
let!(:status) do
|
||||
Fabricate(:status,
|
||||
text: 'Hello world',
|
||||
account: account,
|
||||
poll_attributes: {
|
||||
options: %w(Foo Bar),
|
||||
account: account,
|
||||
multiple: false,
|
||||
hide_totals: false,
|
||||
expires_at: expiration
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'https://example.com/foo',
|
||||
type: 'Question',
|
||||
content: 'Hello world',
|
||||
endTime: expiration.iso8601,
|
||||
oneOf: [
|
||||
poll_option_json('Foo', 4),
|
||||
poll_option_json('Bar', 3),
|
||||
poll_option_json('Baz', 3),
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
subject.call(status, json)
|
||||
end
|
||||
|
||||
it 'does not create any edits' do
|
||||
expect(status.reload.edits).to be_empty
|
||||
end
|
||||
|
||||
it 'does not mark status as edited' do
|
||||
expect(status.reload.edited?).to be false
|
||||
end
|
||||
|
||||
it 'does not update the text' do
|
||||
expect(status.reload.text).to eq 'Hello world'
|
||||
end
|
||||
|
||||
it 'does not update tallies' do
|
||||
expect(status.poll.reload.cached_tallies).to eq [0, 0]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when receiving an edit older than the latest processed' do
|
||||
before do
|
||||
status.snapshot!(at_time: status.created_at, rate_limit: false)
|
||||
status.update!(text: 'Hello newer world', edited_at: Time.now.utc)
|
||||
status.snapshot!(rate_limit: false)
|
||||
end
|
||||
|
||||
it 'does not create any edits' do
|
||||
expect { subject.call(status, json) }.not_to change { status.reload.edits.pluck(&:id) }
|
||||
end
|
||||
|
||||
it 'does not update the text, spoiler_text or edited_at' do
|
||||
expect { subject.call(status, json) }.not_to change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no changes at all' do
|
||||
let(:payload) do
|
||||
{
|
||||
|
Reference in New Issue
Block a user