From cef18abb6462d137e57fc8920d29e5cce646e4c6 Mon Sep 17 00:00:00 2001 From: Peter Frank Date: Mon, 14 Sep 2026 09:00:52 -0400 Subject: [PATCH 1/3] Fix responsive YouTube embed height (#23834) * Fix responsive YouTube embed height The iframe carries width/height attributes as fallback dimensions, but with only `width: 100%` in the inline style the fixed `height` attribute still resolves, so `aspect-ratio` never recalculates the height. In wider containers (observed on organization pages after #23827) this rendered a stretched 2.05:1 player. Adding `height: auto` lets the declared aspect ratio control the height while keeping the attributes as a fallback. Co-Authored-By: Claude Fable 5.1 * Fix failing specs in liquid_embed_extractor_spec and descript_tag_spec --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: Ben Halpern --- app/views/liquids/_youtube.html.erb | 4 ++-- spec/liquid_tags/descript_tag_spec.rb | 8 ++++++++ spec/liquid_tags/youtube_tag_spec.rb | 4 ++-- spec/services/liquid_embed_extractor_spec.rb | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/views/liquids/_youtube.html.erb b/app/views/liquids/_youtube.html.erb index d1a37c670e5c0..4fff4f5ff396c 100644 --- a/app/views/liquids/_youtube.html.erb +++ b/app/views/liquids/_youtube.html.erb @@ -4,7 +4,7 @@ src="https://www.youtube.com/embed/<%= id %>" width="<%= local_assigns[:width] || 315 %>" height="<%= local_assigns[:height] || 560 %>" - style="width: 100%; aspect-ratio: 9 / 16;" + style="width: 100%; height: auto; aspect-ratio: 9 / 16;" allowfullscreen loading="lazy"> @@ -14,7 +14,7 @@ src="https://www.youtube.com/embed/<%= id %>" width="<%= local_assigns[:width] || 710 %>" height="<%= local_assigns[:height] || 399 %>" - style="width: 100%; aspect-ratio: 16 / 9;" + style="width: 100%; height: auto; aspect-ratio: 16 / 9;" allowfullscreen loading="lazy"> diff --git a/spec/liquid_tags/descript_tag_spec.rb b/spec/liquid_tags/descript_tag_spec.rb index a2cdccb553e23..435b872c70266 100644 --- a/spec/liquid_tags/descript_tag_spec.rb +++ b/spec/liquid_tags/descript_tag_spec.rb @@ -26,6 +26,14 @@ def generate_embed(url) end describe "rendering" do + before do + allow(Addrinfo).to receive(:getaddrinfo).and_call_original + %w[www.share.descript.com share.descript.com descript.com].each do |host| + allow(Addrinfo).to receive(:getaddrinfo).with(host, nil, nil, :STREAM) + .and_return([instance_double(Addrinfo, ip_address: "34.95.113.47")]) + end + end + it "returns StandardError for invalid Descript URL", :aggregate_failures do invalid_descript_urls.each do |invalid_url| stub_network_request(url: invalid_url, status_code: 404) diff --git a/spec/liquid_tags/youtube_tag_spec.rb b/spec/liquid_tags/youtube_tag_spec.rb index 9dca95c43596c..6279a005f96c1 100644 --- a/spec/liquid_tags/youtube_tag_spec.rb +++ b/spec/liquid_tags/youtube_tag_spec.rb @@ -50,14 +50,14 @@ def generate_tag(input) it "uses a vertical aspect ratio for YouTube Shorts" do result = generate_tag("https://www.youtube.com/shorts/#{valid_id}") - expect(result).to include("aspect-ratio: 9 / 16") + expect(result).to include("width: 100%; height: auto; aspect-ratio: 9 / 16") expect(result).to include('width="315"') expect(result).to include('height="560"') end it "uses a horizontal aspect ratio for regular videos" do result = generate_tag("https://www.youtube.com/watch?v=#{valid_id}") - expect(result).to include("aspect-ratio: 16 / 9") + expect(result).to include("width: 100%; height: auto; aspect-ratio: 16 / 9") expect(result).to include('width="710"') expect(result).to include('height="399"') end diff --git a/spec/services/liquid_embed_extractor_spec.rb b/spec/services/liquid_embed_extractor_spec.rb index 719a55eb6ab38..40abbc07178a4 100644 --- a/spec/services/liquid_embed_extractor_spec.rb +++ b/spec/services/liquid_embed_extractor_spec.rb @@ -74,8 +74,8 @@ end it "correctly resolves internal DEV Article links wrapped in general UnifiedEmbeds into native polymorphic relationships" do - dev_article = create(:article, title: "Test Article") - dev_article.user.update!(username: "testuser") + user = create(:user, username: "testuser") + dev_article = create(:article, user: user, title: "Test Article") domain = Settings::General.app_domain || "localhost:3000" article_url = "http://#{domain}/testuser/#{dev_article.slug}" From 7515c954e52a38e6b502a9fdd6663c44d7e12bdf Mon Sep 17 00:00:00 2001 From: jcsawyer123 Date: Mon, 14 Sep 2026 14:50:36 +0100 Subject: [PATCH 2/3] [DEV-3974] Adopt omniauth-mlh 4.2 (#23784) * chore: wire omniauth-mlh 4.2.0 and refresh caches * fix: narrow omniauth-mlh dependency update * chore: vendor omniauth-mlh 4.2.0 gem --------- Co-authored-by: Ben Halpern --- Gemfile | 2 +- Gemfile.lock | 4 ++-- vendor/cache/omniauth-mlh-4.1.0.gem | Bin 11776 -> 0 bytes vendor/cache/omniauth-mlh-4.2.0.gem | Bin 0 -> 12800 bytes 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 vendor/cache/omniauth-mlh-4.1.0.gem create mode 100644 vendor/cache/omniauth-mlh-4.2.0.gem diff --git a/Gemfile b/Gemfile index f7aef2fe02bd7..e9a36324d6e83 100644 --- a/Gemfile +++ b/Gemfile @@ -73,7 +73,7 @@ gem "omniauth-apple", "~> 1.0" # OmniAuth strategy for Sign In with Apple gem "omniauth-facebook", "~> 9.0" # OmniAuth strategy for Facebook gem "omniauth-github", "~> 2.0" # OmniAuth strategy for GitHub gem "omniauth-google-oauth2", "~> 1.0" # OmniAuth strategy for Google OAuth2 -gem "omniauth-mlh", "~> 4.1" +gem "omniauth-mlh", "~> 4.2" gem "omniauth-rails_csrf_protection", "~> 2.0" # Provides CSRF protection on OmniAuth request endpoint on Rails application. gem "omniauth-twitter", "~> 1.4" # OmniAuth strategy for Twitter gem "parallel", "~> 1.22" # Run any kind of code in parallel processes diff --git a/Gemfile.lock b/Gemfile.lock index f542d0235f4a3..38c9333d65c39 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -675,7 +675,7 @@ GEM oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-mlh (4.1.0) + omniauth-mlh (4.2.0) oauth2 (~> 2.0.9) omniauth (~> 2.1.1) omniauth-oauth2 (~> 1.8.0) @@ -1207,7 +1207,7 @@ DEPENDENCIES omniauth-facebook (~> 9.0) omniauth-github (~> 2.0) omniauth-google-oauth2 (~> 1.0) - omniauth-mlh (~> 4.1) + omniauth-mlh (~> 4.2) omniauth-rails_csrf_protection (~> 2.0) omniauth-twitter (~> 1.4) parallel (~> 1.22) diff --git a/vendor/cache/omniauth-mlh-4.1.0.gem b/vendor/cache/omniauth-mlh-4.1.0.gem deleted file mode 100644 index 49a5ada30444ec77acf222199b524d474d0c752c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11776 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 diff --git a/vendor/cache/omniauth-mlh-4.2.0.gem b/vendor/cache/omniauth-mlh-4.2.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..32afc61464c538d0ab44b0e3dbe7c7a402e91ace GIT binary patch literal 12800 zcmeHtWl&u~mo4t@dhy`y?hxGF3C_hmm*5gyLU4B{xVyW%L$KiP+{wK6W2U~TdhgH7 zym?jM{&D)8-nIAXUfsQ_yK6aEx|>;;xtpo zg#SgpV(h8X)F8Rh|4s9-fLAzES62?P)S5&3& zS!(z#xakh&sR8cmz@VbG#*4eJ>m8G~Ei#$CEd@ndrK>G=q%~#FizbetIoQ9OmjAKT zg^NHq4i0|S`7?ZQ;d5Q@Uao%v35q7ari29NmC?Nu9>A?5@M0)<4iit16p5kxVrV*L z@p!{+xHpKVZ+c3U)hk_s&M(3kw8177bOtf$=cuY-embTAYr}@el+;6-PTS%Bi#q3l z8Yp*DoYa1`CUQhUEW$MOHpVw&dMVuJx~bQ$Ve7Q?vBM<9sK*ZMYNsx=d&LaF{{N@$1PJjz^Yl z-sF-Q`f9Mn=E#S@LINbQvAPe<4!b3ICux%c+=Y06`Jlg|%%Bl8lM5pG`A(86?X_O}0)ak?Jf(21zD4S;m=U;E&l1@w*9P)>a?~2- zSQE1BEYI^rItWM9$_ML8w|m z*M}7mVgN?ChNSkqJKSwZ%i`ov(W!__qx!}a?;)`$?7XbFC;-NESY)CR zEt|RX-}RriuNOQ6HY6+#Y|@)<(0tMK7l_5GHPiSY*6Z|oK9Epw;cj#XA)a{{v&4ubyM;hboi z2RgiNc5Q1N71aG^XEB=Kg1&!jyM#5<}nrAa8*iThFh&ELt|-g**D76;D3)?m5)5VwHuLE!#yQU>4^zH(?5X5h02 z9^y;Dt~ws8C1ibrHFrz_1jfu`+IWc&1lpOm&=Q_OJVVgw*SgRoZT%`bHHe}>0o$H7 z0r-+BOYDybL8dxZ{`u67EvzPb_r4S17%IfZ!wP@ukIR@KwmlQ4XwZ}Buh(-d@b=c$ zhS4o}_=$^2d;qn;1@?_LnrPL_-b&W-m;hDPEj)XgS=5~+M#cmOIsh9gqh1z)5ve$Z z>It9!Gu7Tao4ol`x?(Y^Bwd6`mCa6NTECYx7!$C(Ou8@fvc2J_oew^o@^n)I$6c2# zhnvA5AweT4(>g;os5x=`@u0oyw^6D9f84QZHnD>CK`Za*tRR}_?)FFEzee;5X5t4X zI^}k;CgcG|4aAH&4lvmxdv?QWFJIyFTHJ&&@k@*EqkY&FhBC8AvH)2ff|v(4fPp>t z$QKB&ywO(D81JCPZYwK38z+ao!MgX+YmF4f`rHM%Y!L^c&_oSCrG1X&sPn+-ywZ$V z8D}q`zr1(AH}Qj+hcBrXylH_%w<6IS?{sx1BG2i=%DxXnf|Z*lgS)@%b0EHQKHSL};B3x<{=IP=F-pMq!bvCX2a zv9Q0V&oJCxB~J%4XM7Hidn!4pO$j(WoIXb+0eVm74%^RM>z=K%ZycY5ox(Eh6)ZGU zwOlAs0p_hFL3XY1$g!$inH{h ze5zwtrm#2CEf;##0PVpR3=z-ht6U=q#_4wP+JyY*sh_tn%z64UqX;%Z5@d`xV`TkE zKl6uuuK|MBw`{8~{o`(~&rOs1NR?AtqL1z7D7&^B(w7eP-(Q=-B$-*+Ua`iVf}#TN zgXt~XQsDKVabrF3`l|6f_=|7$FKCSF1m4fs@u_Di3vR8KutGf1x3#2(b$Y4!5&;~1 zVQmO`B%O#OM?vJ=(X{#{GdB=QS6$(NFo8b;DTSXrpSUb=d6cnKkBLxs!|F1f=K_?G z@>|&lc(iPMF|xk-v}53@GFPZ%BTDPh z8!0$OwGJwgh#|xmN^>@0mWRic$$z)_Bw%0D(tw$2Uv-NqIxGu+KJU0LFG8~&`H_(^ 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 I|KAb#7Xp_wM*si- literal 0 HcmV?d00001 From 5780d841f67535e28e089053a675052237a21db8 Mon Sep 17 00:00:00 2001 From: jcsawyer123 Date: Mon, 14 Sep 2026 17:02:58 +0100 Subject: [PATCH 3/3] [DEV-3974] Guard cross-account identity switching (#23783) * feat: require confirmation before cross-account identity attach - differing resolved target raises AccountSwitchConfirmation - ineligible resolution fails closed, attaching to nobody - suspended/spam targets excluded from silent attach paths * chore(auth): trim account switch comments and specs * fix(auth): add missing account_not_eligible translations for ineligibility errors --------- Co-authored-by: jcs Co-authored-by: Ben Halpern --- app/errors/authentication/errors.rb | 15 ++++ app/services/authentication/authenticator.rb | 18 +++++ config/locales/services/en.yml | 1 + config/locales/services/fr.yml | 1 + config/locales/services/pt.yml | 1 + .../authenticator_account_switch_spec.rb | 79 +++++++++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 spec/services/authentication/authenticator_account_switch_spec.rb diff --git a/app/errors/authentication/errors.rb b/app/errors/authentication/errors.rb index c1860e6897669..a65deb6ae4cf7 100644 --- a/app/errors/authentication/errors.rb +++ b/app/errors/authentication/errors.rb @@ -20,5 +20,20 @@ def message # Raised when we find an email that's from a spammy domain. class SpammyEmailDomain < Error end + + class Ineligible < Error + def message + I18n.t("services.authentication.authenticator.account_not_eligible") + end + end + + class AccountSwitchConfirmation < Error + attr_reader :target_user + + def initialize(target_user) + @target_user = target_user + super("account_switch_confirmation") + end + end end end diff --git a/app/services/authentication/authenticator.rb b/app/services/authentication/authenticator.rb index a45e7c7e405a2..364abaca43597 100644 --- a/app/services/authentication/authenticator.rb +++ b/app/services/authentication/authenticator.rb @@ -41,6 +41,7 @@ def call refresh_identity(identity, linked_identity) return current_user end + guard_account_switch!(identity) if current_user # These variables need to be set outside of the scope of the # transaction in order to be used after the transaction is completed. @@ -169,6 +170,23 @@ def repoint_identity(incoming_identity, linked_identity) nil end + def guard_account_switch!(identity) + candidate = identity.user || verified_email_user + return if candidate.nil? || candidate == current_user + + raise ::Authentication::Errors::Ineligible if candidate.spam_or_suspended? + + raise ::Authentication::Errors::AccountSwitchConfirmation.new(candidate) + end + + def verified_email_user + email = provider.user_email + return nil if email.blank? + + user = User.find_by(email: email) + user&.confirmed? ? user : nil + end + def proper_user(identity) if current_user Rails.logger.debug { "Current user exists: #{current_user.id}" } diff --git a/config/locales/services/en.yml b/config/locales/services/en.yml index f22f89fc8a2f6..2a69d5b90f8da 100644 --- a/config/locales/services/en.yml +++ b/config/locales/services/en.yml @@ -8,6 +8,7 @@ en: authentication: authenticator: not_allowed: Sorry, but the domain for your email address is not allowed. This Forem may have limited signup to only specified email domains or blocked this specific domain from joining. + account_not_eligible: Sorry, this account is not eligible to sign in. Please contact the community staff if you believe this is a mistake. providers: apple: name: "%{first} %{last}" diff --git a/config/locales/services/fr.yml b/config/locales/services/fr.yml index 7c90d12cec841..657f984a60733 100644 --- a/config/locales/services/fr.yml +++ b/config/locales/services/fr.yml @@ -7,6 +7,7 @@ fr: unpublished_video: Vidéo non publiée ~ %{rand} authentication: authenticator: + account_not_eligible: "Désolé, ce compte ne peut pas se connecter. Veuillez contacter l’équipe de la communauté si vous pensez qu’il s’agit d’une erreur." not_allowed: Désolé, mais le domaine de votre adresse e-mail n'est pas autorisé. Ce Forem a peut-être limité l'inscription à certains domaines de messagerie ou bloqué l'accès à ce domaine spécifique. providers: apple: diff --git a/config/locales/services/pt.yml b/config/locales/services/pt.yml index 583cd77fc9076..da62b3c49219f 100644 --- a/config/locales/services/pt.yml +++ b/config/locales/services/pt.yml @@ -7,6 +7,7 @@ pt: unpublished_video: Vídeo Não Publicado ~ %{rand} authentication: authenticator: + account_not_eligible: "Desculpe, esta conta não está autorizada a entrar. Entre em contato com a equipe da comunidade se você acredita que isso é um engano." not_allowed: Desculpe, mas o domínio do seu endereço de e-mail não é permitido. Este Forem pode ter limitado o cadastro apenas a domínios de e-mail específicos ou bloqueado este domínio específico de participar. providers: apple: diff --git a/spec/services/authentication/authenticator_account_switch_spec.rb b/spec/services/authentication/authenticator_account_switch_spec.rb new file mode 100644 index 0000000000000..d41ca8dabd2db --- /dev/null +++ b/spec/services/authentication/authenticator_account_switch_spec.rb @@ -0,0 +1,79 @@ +require "rails_helper" + +RSpec.describe Authentication::Authenticator, type: :service do + let(:current_user) { create(:user) } + + def mlh_payload(uid:, email:) + OmniAuth::AuthHash.new( + provider: "mlh", + uid: uid, + info: OmniAuth::AuthHash::InfoHash.new(email: email, name: "MLH User"), + credentials: OmniAuth::AuthHash.new(token: "tok_#{uid}", secret: "sec"), + extra: { raw_info: { created_at: 2.years.ago.iso8601 } }, + ) + end + + before do + omniauth_mock_mlh_payload + allow(ForemStatsClient).to receive(:increment) + allow(Settings::Authentication).to receive(:providers).and_return(Authentication::Providers.available) + end + + context "when a different user is signed in and the incoming identity resolves elsewhere" do + it "raises AccountSwitchConfirmation carrying the resolved target (exact verified email)" do + target = create(:user) + payload = mlh_payload(uid: "core-switch-1", email: target.email) + + expect do + expect do + described_class.call(payload, current_user: current_user) + end.to raise_error( + Authentication::Errors::AccountSwitchConfirmation, + ) { |error| expect(error.target_user).to eq(target) } + end.not_to change(Identity, :count) + end + + it "raises AccountSwitchConfirmation carrying the resolved target (uid ownership)" do + target = create(:user) + create(:identity, user: target, provider: "mlh", uid: "core-switch-2") + payload = mlh_payload(uid: "core-switch-2", email: "nobody-else@example.com") + + expect do + described_class.call(payload, current_user: current_user) + end.to raise_error( + Authentication::Errors::AccountSwitchConfirmation, + ) { |error| expect(error.target_user).to eq(target) } + end + + it "fails closed without attaching when the resolved account is suspended" do + target = create(:user) + target.add_role(:suspended) + payload = mlh_payload(uid: "core-switch-3", email: target.email) + + expect do + expect do + described_class.call(payload, current_user: current_user) + end.to raise_error(Authentication::Errors::Ineligible) + end.not_to change(Identity, :count) + end + end + + context "when the signed-in user resolves as the identity's owner themselves" do + it "attaches normally without requiring confirmation" do + payload = mlh_payload(uid: "core-self-1", email: current_user.email) + + expect(described_class.call(payload, current_user: current_user)).to eq(current_user) + expect(current_user.identities.find_by(provider: "mlh").uid).to eq("core-self-1") + end + end + + context "when the incoming unclaimed identity resolves to nobody" do + it "keeps the normal attach-to-current-user flow" do + payload = mlh_payload(uid: "core-nobody-1", email: "unclaimed@example.com") + + expect do + expect(described_class.call(payload, current_user: current_user)).to eq(current_user) + end.to change(Identity, :count).by(1) + end + end +end