From b2d719975673bd45084978c390b2ff4cb3d6d676 Mon Sep 17 00:00:00 2001 From: shipeasy-ai Date: Fri, 19 Jun 2026 20:37:33 -0400 Subject: [PATCH] EmbedScoped: allow /embed/builder re-entry with a pinned scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EmbedBuilderController is the JWT re-entry point but runs behind EmbedScoped's enforce_embed_scope! before_action. Once a prior open pins an embed_scope into the session cookie, re-opening the builder (the same template again, or a different one) hit `head :forbidden` because /embed/builder was not in UNSCOPED_ALLOW — the concern locked out its own re-entry endpoint. First open worked; every reopen 403'd. The token, not the session, authenticates /embed/builder, so always let it through; EmbedBuilderController then overwrites the scope from the fresh token. Add a request spec covering the re-entry/re-scope case. --- app/controllers/concerns/embed_scoped.rb | 7 +++++++ spec/requests/embed_builder_spec.rb | 25 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/controllers/concerns/embed_scoped.rb b/app/controllers/concerns/embed_scoped.rb index cc60829e..af71d78e 100644 --- a/app/controllers/concerns/embed_scoped.rb +++ b/app/controllers/concerns/embed_scoped.rb @@ -26,6 +26,13 @@ module EmbedScoped # create/upload flow, the signing/asset surfaces, and the few endpoints the # builder's own browser code calls. None expose another account's templates. UNSCOPED_ALLOW = [ + # The JWT re-entry point itself. It runs BEHIND this before_action, so once + # a prior scope is pinned in the session cookie, re-opening the builder + # (the same template again, or a different one) would 403 here before + # EmbedBuilderController gets to re-establish the scope from the new token. + # The token — not the session — authenticates this endpoint, so always let + # it through; the controller then overwrites the scope. + %r{\A/embed/builder(?:[/?]|\z)}, %r{\A/new(?:[/?]|\z)}, %r{\A/templates/new(?:[/?]|\z)}, %r{\A/templates_uploads?(?:[/?]|\z)}, diff --git a/spec/requests/embed_builder_spec.rb b/spec/requests/embed_builder_spec.rb index 5f08e073..5705553b 100644 --- a/spec/requests/embed_builder_spec.rb +++ b/spec/requests/embed_builder_spec.rb @@ -113,5 +113,30 @@ describe 'Embed builder' do expect(response).to have_http_status(:forbidden) end + + it 're-enters /embed/builder with a pinned scope and re-scopes to the new template' do + first = create(:template, account:, author: user, external_id: 'first') + second = create(:template, account:, author: user, external_id: 'second') + + # First open pins the embed scope to `first` in the session cookie. + get embed_builder_path, params: { token: token(template_id: first.id) } + expect(response).to redirect_to(edit_template_path(first)) + + # Re-opening the builder for a DIFFERENT template (the cookie still carries + # the `first` scope) must NOT 403 — /embed/builder is the re-entry point + # that re-establishes scope from the new token. Regression: it used to + # 403 here because the stale scope blocked its own re-entry endpoint. + get embed_builder_path, params: { token: token(template_id: second.id) } + expect(response).not_to have_http_status(:forbidden) + expect(response).to redirect_to(edit_template_path(second)) + + # The new scope now allows the second template's editor. + begin + get edit_template_path(second) + expect(response).not_to have_http_status(:forbidden) + rescue ActionView::Template::Error, Shakapacker::Manifest::MissingEntryError + # Reached view rendering => the re-scoped template was allowed through. + end + end end end