From 2455537bc1392a9919b520f1a2d0f4b5a600f194 Mon Sep 17 00:00:00 2001
From: adam-james <21064735+adam-james-v@users.noreply.github.com>
Date: Wed, 8 May 2024 10:54:17 -0700
Subject: [PATCH] Dashboard Param Values Common Impl for Embedding/Preview
 Embedding (#42052)

* Dashboard Param Values Common Impl for Embedding/Preview Embedding

Refactor the embedding api namespace to allow a bit of reuse between the embedding api and the embedding preview
api. In some cases, the implementations are not identical (in terms of the shape of data expected/endpoints
available), so this is a first step to making them the same.

Related:

https://www.notion.so/metabase/Make-embedding-preview-behave-consistently-with-actual-static-embeds-855353d8e5e8411d8164c7ac563c7d2f

* Add a test to verify that preview-embed param-values endpoint works

* Use preview embed endpoint and fix relevant tests

* Add tests

* Change name to reflect what actually happens in the function

---------

Co-authored-by: Mahatthana Nomsawadi <mahatthana.n@gmail.com>
---
 ...-embed-preview-parameter-values.cy.spec.js | 233 +++++++++
 ...ew-parameter-values-not-working.cy.spec.js | 149 ------
 ...-when-embedding-long-dashboards.cy.spec.js |   1 -
 .../dashboard/actions/data-fetching.js        |   3 +-
 frontend/src/metabase/services.js             |  10 +-
 src/metabase/api/embed.clj                    | 488 +-----------------
 src/metabase/api/embed/common.clj             | 478 +++++++++++++++++
 src/metabase/api/preview_embed.clj            |  33 +-
 test/metabase/api/embed_test.clj              |  28 +-
 test/metabase/api/preview_embed_test.clj      |  46 ++
 10 files changed, 821 insertions(+), 648 deletions(-)
 create mode 100644 e2e/test/scenarios/embedding/reproductions/37914-41635-embed-preview-parameter-values.cy.spec.js
 delete mode 100644 e2e/test/scenarios/embedding/reproductions/37914-embed-preview-parameter-values-not-working.cy.spec.js
 create mode 100644 src/metabase/api/embed/common.clj

diff --git a/e2e/test/scenarios/embedding/reproductions/37914-41635-embed-preview-parameter-values.cy.spec.js b/e2e/test/scenarios/embedding/reproductions/37914-41635-embed-preview-parameter-values.cy.spec.js
new file mode 100644
index 00000000000..567ad40eb39
--- /dev/null
+++ b/e2e/test/scenarios/embedding/reproductions/37914-41635-embed-preview-parameter-values.cy.spec.js
@@ -0,0 +1,233 @@
+import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
+import {
+  addOrUpdateDashboardCard,
+  getIframeBody,
+  modal,
+  openStaticEmbeddingModal,
+  popover,
+  restore,
+  visitDashboard,
+} from "e2e/support/helpers";
+
+const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
+
+const questionDetails = {
+  name: "Products",
+  query: { "source-table": PRODUCTS_ID },
+};
+
+const filter3 = {
+  name: "Text 2",
+  slug: "text_2",
+  id: "b0665b6a",
+  type: "string/=",
+  sectionId: "string",
+};
+
+const filter2 = {
+  name: "Text 1",
+  slug: "text_1",
+  id: "d4c9f2e5",
+  type: "string/=",
+  sectionId: "string",
+};
+
+const filter = {
+  filteringParameters: [filter2.id],
+  name: "Text",
+  slug: "text",
+  id: "d1b69627",
+  type: "string/=",
+  sectionId: "string",
+};
+
+describe("dashboard preview", () => {
+  beforeEach(() => {
+    cy.intercept("GET", "/api/preview_embed/dashboard/**").as(
+      "previewDashboard",
+    );
+    cy.intercept("GET", "/api/preview_embed/dashboard/**/params/**/values").as(
+      "previewValues",
+    );
+
+    restore();
+    cy.signInAsAdmin();
+  });
+
+  it("dashboard linked filters values don't work in static embed preview (metabase#37914)", () => {
+    const dashboardDetails = {
+      parameters: [filter, filter2, filter3],
+      enable_embedding: true,
+      embedding_params: {
+        [filter.slug]: "enabled",
+        [filter2.slug]: "enabled",
+        [filter3.slug]: "enabled",
+      },
+    };
+    cy.createQuestionAndDashboard({
+      questionDetails,
+      dashboardDetails,
+    }).then(({ body: { card_id, dashboard_id } }) => {
+      addOrUpdateDashboardCard({
+        dashboard_id,
+        card_id,
+        card: {
+          parameter_mappings: [
+            {
+              card_id,
+              parameter_id: filter.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+            {
+              card_id,
+              parameter_id: filter2.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+            {
+              card_id,
+              parameter_id: filter3.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+          ],
+        },
+      });
+
+      visitDashboard(dashboard_id);
+    });
+
+    openStaticEmbeddingModal({
+      activeTab: "parameters",
+      previewMode: "preview",
+    });
+
+    modal().within(() => {
+      // Makes it less likely to flake.
+      cy.wait("@previewDashboard");
+
+      getIframeBody().within(() => {
+        cy.log(
+          "Set filter 2 value, so filter 1 should be filtered by filter 2",
+        );
+        cy.button(filter2.name).click();
+        cy.wait("@previewValues");
+        popover().within(() => {
+          cy.findByText("Gadget").should("be.visible");
+          cy.findByText("Gizmo").should("be.visible");
+          cy.findByText("Widget").should("be.visible");
+          cy.findByText("Doohickey").click();
+          cy.button("Add filter").click();
+        });
+
+        cy.log("Assert filter 1");
+        cy.button(filter.name).click();
+        popover().within(() => {
+          cy.findByText("Gadget").should("not.exist");
+          cy.findByText("Gizmo").should("not.exist");
+          cy.findByText("Widget").should("not.exist");
+          cy.findByText("Doohickey").should("be.visible");
+        });
+      });
+    });
+  });
+
+  it("dashboard linked filters values in embed preview don't behave like embedding (metabase#41635)", () => {
+    const dashboardDetails = {
+      parameters: [filter, filter2, filter3],
+      enable_embedding: true,
+      embedding_params: {
+        [filter.slug]: "enabled",
+        [filter2.slug]: "locked",
+        [filter3.slug]: "locked",
+      },
+    };
+    cy.createQuestionAndDashboard({
+      questionDetails,
+      dashboardDetails,
+    }).then(({ body: { card_id, dashboard_id } }) => {
+      addOrUpdateDashboardCard({
+        dashboard_id,
+        card_id,
+        card: {
+          parameter_mappings: [
+            {
+              card_id,
+              parameter_id: filter.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+            {
+              card_id,
+              parameter_id: filter2.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+            {
+              card_id,
+              parameter_id: filter3.id,
+              target: [
+                "dimension",
+                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
+              ],
+            },
+          ],
+        },
+      });
+
+      visitDashboard(dashboard_id);
+    });
+
+    openStaticEmbeddingModal({
+      activeTab: "parameters",
+      previewMode: "preview",
+    });
+
+    // Makes it less likely to flake.
+    cy.wait("@previewDashboard");
+
+    cy.log("Set the first locked parameter values");
+    modal()
+      .findByRole("generic", { name: "Previewing locked parameters" })
+      .findByText("Text 1")
+      .click();
+    popover().within(() => {
+      cy.findByText("Doohickey").click();
+      cy.button("Add filter").click();
+    });
+
+    cy.log("Set the second locked parameter values");
+    modal()
+      .findByRole("generic", { name: "Previewing locked parameters" })
+      .findByText("Text 2")
+      .click();
+    popover().within(() => {
+      cy.findByText("Doohickey").click();
+      cy.findByText("Gizmo").click();
+      cy.findByText("Gadget").click();
+      cy.button("Add filter").click();
+    });
+
+    getIframeBody().within(() => {
+      cy.log("Assert filter 1");
+      cy.button(filter.name).click();
+      popover().within(() => {
+        cy.findByText("Gadget").should("not.exist");
+        cy.findByText("Gizmo").should("not.exist");
+        cy.findByText("Widget").should("not.exist");
+        cy.findByText("Doohickey").should("be.visible");
+      });
+    });
+  });
+});
diff --git a/e2e/test/scenarios/embedding/reproductions/37914-embed-preview-parameter-values-not-working.cy.spec.js b/e2e/test/scenarios/embedding/reproductions/37914-embed-preview-parameter-values-not-working.cy.spec.js
deleted file mode 100644
index a4210e07666..00000000000
--- a/e2e/test/scenarios/embedding/reproductions/37914-embed-preview-parameter-values-not-working.cy.spec.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database";
-import {
-  addOrUpdateDashboardCard,
-  dashboardHeader,
-  getIframeBody,
-  modal,
-  popover,
-  restore,
-  visitDashboard,
-} from "e2e/support/helpers";
-
-const { PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
-
-const questionDetails = {
-  name: "Products",
-  query: { "source-table": PRODUCTS_ID },
-};
-
-const filter3 = {
-  name: "Text 2",
-  slug: "text_2",
-  id: "b0665b6a",
-  type: "string/=",
-  sectionId: "string",
-};
-
-const filter2 = {
-  name: "Text 1",
-  slug: "text_1",
-  id: "d4c9f2e5",
-  type: "string/=",
-  sectionId: "string",
-};
-
-const filter = {
-  filteringParameters: [filter2.id],
-  name: "Text",
-  slug: "text",
-  id: "d1b69627",
-  type: "string/=",
-  sectionId: "string",
-};
-
-const dashboardDetails = {
-  parameters: [filter, filter2, filter3],
-  enable_embedding: true,
-  embedding_params: {
-    [filter.slug]: "enabled",
-    [filter2.slug]: "enabled",
-    [filter3.slug]: "enabled",
-  },
-};
-
-describe("issue 37914", () => {
-  beforeEach(() => {
-    cy.intercept("GET", "/api/preview_embed/dashboard/**").as(
-      "previewDashboard",
-    );
-    cy.intercept("GET", "/api/dashboard/**/params/**/values").as("values");
-
-    restore();
-    cy.signInAsAdmin();
-
-    cy.createQuestionAndDashboard({
-      questionDetails,
-      dashboardDetails,
-    }).then(({ body: { id, card_id, dashboard_id } }) => {
-      addOrUpdateDashboardCard({
-        dashboard_id,
-        card_id,
-        card: {
-          parameter_mappings: [
-            {
-              card_id,
-              parameter_id: filter.id,
-              target: [
-                "dimension",
-                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
-              ],
-            },
-            {
-              card_id,
-              parameter_id: filter2.id,
-              target: [
-                "dimension",
-                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
-              ],
-            },
-            {
-              card_id,
-              parameter_id: filter3.id,
-              target: [
-                "dimension",
-                ["field", PRODUCTS.CATEGORY, { "base-type": "type/Text" }],
-              ],
-            },
-          ],
-        },
-      });
-
-      visitDashboard(dashboard_id);
-    });
-  });
-
-  it("dashboard linked filters values doesn't work in static embed preview (metabase#37914)", () => {
-    dashboardHeader().within(() => {
-      cy.icon("share").click();
-    });
-
-    popover().findByText("Embed").click();
-
-    modal().within(() => {
-      cy.findByText("Static embed").click();
-
-      cy.button("Agree and continue").click();
-
-      cy.findByRole("tab", { name: "Parameters" }).click();
-
-      cy.findByText("Preview").click();
-
-      // Makes it less likely to flake.
-      cy.wait("@previewDashboard");
-
-      getIframeBody().within(() => {
-        cy.log(
-          "Set filter 2 value, so filter 1 should be filtered by filter 2",
-        );
-        cy.button(filter2.name).click();
-        cy.wait("@values");
-        popover().within(() => {
-          cy.findByText("Gadget").should("be.visible");
-          cy.findByText("Gizmo").should("be.visible");
-          cy.findByText("Widget").should("be.visible");
-          cy.findByText("Doohickey").click();
-          cy.button("Add filter").click();
-        });
-
-        cy.log("Assert filter 1");
-        cy.button(filter.name).click();
-        popover().within(() => {
-          cy.findByText("Gadget").should("not.exist");
-          cy.findByText("Gizmo").should("not.exist");
-          cy.findByText("Widget").should("not.exist");
-          cy.findByText("Doohickey").should("be.visible");
-        });
-      });
-    });
-  });
-});
diff --git a/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js b/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js
index d0b68551d2f..2a0c57d7623 100644
--- a/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js
+++ b/e2e/test/scenarios/embedding/reproductions/40660-overflow-when-embedding-long-dashboards.cy.spec.js
@@ -24,7 +24,6 @@ describe("issue 40660", () => {
     cy.intercept("GET", "/api/preview_embed/dashboard/**").as(
       "previewDashboard",
     );
-    cy.intercept("GET", "/api/dashboard/**/params/**/values").as("values");
 
     restore();
     cy.signInAsAdmin();
diff --git a/frontend/src/metabase/dashboard/actions/data-fetching.js b/frontend/src/metabase/dashboard/actions/data-fetching.js
index 7054366686c..04679e03f48 100644
--- a/frontend/src/metabase/dashboard/actions/data-fetching.js
+++ b/frontend/src/metabase/dashboard/actions/data-fetching.js
@@ -3,7 +3,6 @@ import { denormalize, normalize, schema } from "normalizr";
 import { t } from "ttag";
 
 import { showAutoApplyFiltersToast } from "metabase/dashboard/actions/parameters";
-import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
 import { defer } from "metabase/lib/promise";
 import {
   createAction,
@@ -192,7 +191,7 @@ export const fetchDashboard = createAsyncThunk(
         );
         result = {
           ...result,
-          id: IS_EMBED_PREVIEW ? result.id : dashId,
+          id: dashId,
           dashcards: result.dashcards.map(dc => ({
             ...dc,
             dashboard_id: dashId,
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 814093abbdf..f74660547d6 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -480,7 +480,9 @@ export function setEmbedQuestionEndpoints(token) {
 
 export function setEmbedDashboardEndpoints() {
   if (!IS_EMBED_PREVIEW) {
-    setDashboardEndpoints("/api/embed");
+    setDashboardEndpoints(embedBase);
+  } else {
+    setDashboardParameterValuesEndpoint(embedBase);
   }
 }
 
@@ -523,6 +525,12 @@ function setDashboardEndpoints(prefix) {
   );
 }
 
+function setDashboardParameterValuesEndpoint(prefix) {
+  DashboardApi.parameterValues = GET(
+    `${prefix}/dashboard/:dashId/params/:paramId/values`,
+  );
+}
+
 export const ActionsApi = {
   list: GET("/api/action"),
   get: GET("/api/action/:id"),
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index eb067236aa7..6159fbafe05 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -15,370 +15,25 @@
                   :dashboard <dashboard-id>}
        :params   <params>}"
   (:require
-   [clojure.set :as set]
-   [clojure.string :as str]
    [compojure.core :refer [GET]]
    [medley.core :as m]
-   [metabase.api.card :as api.card]
    [metabase.api.common :as api]
-   [metabase.api.common.validation :as validation]
-   [metabase.api.dashboard :as api.dashboard]
    [metabase.api.dataset :as api.dataset]
+   [metabase.api.embed.common :as api.embed.common]
    [metabase.api.public :as api.public]
-   [metabase.driver.common.parameters.operators :as params.ops]
    [metabase.events :as events]
    [metabase.models.card :as card :refer [Card]]
    [metabase.models.dashboard :refer [Dashboard]]
-   [metabase.models.params :as params]
-   [metabase.pulse.parameters :as pulse-params]
    [metabase.query-processor.card :as qp.card]
    [metabase.query-processor.middleware.constraints :as qp.constraints]
    [metabase.query-processor.pivot :as qp.pivot]
    [metabase.util :as u]
    [metabase.util.embed :as embed]
-   [metabase.util.i18n
-    :as i18n
-    :refer [tru]]
-   [metabase.util.log :as log]
-   [metabase.util.malli :as mu]
    [metabase.util.malli.schema :as ms]
    [toucan2.core :as t2]))
 
 (set! *warn-on-reflection* true)
 
-;;; ------------------------------------------------- Param Checking -------------------------------------------------
-
-(defn- check-params-are-allowed
-  "Check that the conditions specified by `object-embedding-params` are satisfied."
-  [object-embedding-params token-params user-params]
-  (let [all-params        (set/union token-params user-params)
-        duplicated-params (set/intersection token-params user-params)]
-    (doseq [[param status] object-embedding-params]
-      (case status
-        ;; disabled means a param is not allowed to be specified by either token or user
-        "disabled" (api/check (not (contains? all-params param))
-                              [400 (tru "You''re not allowed to specify a value for {0}." param)])
-        ;; enabled means either JWT *or* user can specify the param, but not both. Param is *not* required
-        "enabled"  (api/check (not (contains? duplicated-params param))
-                              [400 (tru "You can''t specify a value for {0} if it''s already set in the JWT." param)])
-        ;; locked means JWT must specify param
-        "locked"   (api/check
-                    (contains? token-params param)      [400 (tru "You must specify a value for {0} in the JWT." param)]
-                    (not (contains? user-params param)) [400 (tru "You can only specify a value for {0} in the JWT." param)])))))
-
-(defn- check-params-exist
-  "Make sure all the params specified are specified in `object-embedding-params`."
-  [object-embedding-params all-params]
-  (let [embedding-params (set (keys object-embedding-params))]
-    (doseq [k all-params]
-      (api/check (contains? embedding-params k)
-        [400 (format "Unknown parameter %s." k)]))))
-
-(defn- check-param-sets
-  "Validate that sets of params passed as part of the JWT token and by the user (as query params, i.e. as part of the
-  URL) are valid for the `object-embedding-params`. `token-params` and `user-params` should be sets of all valid param
-  keys specified in the JWT or by the user, respectively."
-  [object-embedding-params token-params user-params]
-  ;; TODO - maybe make this log/debug once embedding is wrapped up
-  (log/debug "Validating params for embedded object:\n"
-             "object embedding params:" object-embedding-params
-             "token params:"            token-params
-             "user params:"             user-params)
-  (check-params-are-allowed object-embedding-params token-params user-params)
-  (check-params-exist object-embedding-params (set/union token-params user-params)))
-
-(defn- valid-param?
-  "Is V a valid param value? (If it is a String, is it non-blank?)"
-  [v]
-  (or (not (string? v))
-      (not (str/blank? v))))
-
-(mu/defn ^:private validate-and-merge-params :- [:map-of :keyword :any]
-  "Validate that the `token-params` passed in the JWT and the `user-params` (passed as part of the URL) are allowed, and
-  that ones that are required are specified by checking them against a Card or Dashboard's `object-embedding-params`
-  (the object's value of `:embedding_params`). Throws a 400 if any of the checks fail. If all checks are successful,
-  returns a *merged* parameters map."
-  [object-embedding-params :- ms/EmbeddingParams
-   token-params            :- [:map-of :keyword :any]
-   user-params             :- [:map-of :keyword :any]]
-  (check-param-sets object-embedding-params
-                    (set (keys (m/filter-vals valid-param? token-params)))
-                    (set (keys (m/filter-vals valid-param? user-params))))
-  ;; ok, everything checks out, now return the merged params map
-  (merge user-params token-params))
-
-
-;;; ---------------------------------------------- Other Param Util Fns ----------------------------------------------
-
-(defn- remove-params-in-set
-  "Remove any `params` from the list whose `:slug` is in the `params-to-remove` set."
-  [params params-to-remove]
-  (for [param params
-        :when (not (contains? params-to-remove (keyword (:slug param))))]
-    param))
-
-(defn- classify-params-as-keep-or-remove
-  "Classifies the params in the `dashboard-or-card-params` seq and the param slugs in `embedding-params` map according to:
-  Parameters in `dashboard-or-card-params` whose slugs are NOT in the `embedding-params` map must be removed.
-  Parameter slugs in `embedding-params` with the value 'enabled' are kept, 'disabled' or 'locked' are not kept.
-
-  The resulting classification is returned as a map with keys :keep and :remove whose values are sets of parameter slugs."
-  [dashboard-or-card-params embedding-params]
-  (let [param-slugs                   (map #(keyword (:slug %)) dashboard-or-card-params)
-        grouped-param-slugs           {:remove (remove (fn [k] (contains? embedding-params k)) param-slugs)}
-        grouped-embedding-param-slugs (-> (group-by #(= (second %) "enabled") embedding-params)
-                                          (update-keys {true :keep false :remove})
-                                          (update-vals #(into #{} (map first) %)))]
-    (merge-with (comp set concat)
-                {:keep #{} :remove #{}}
-                grouped-param-slugs
-                grouped-embedding-param-slugs)))
-
-(defn- get-params-to-remove
-  [dashboard-or-card-params embedding-params]
-  (:remove (classify-params-as-keep-or-remove dashboard-or-card-params embedding-params)))
-
-(mu/defn ^:private remove-locked-and-disabled-params
-  "Remove the `:parameters` for `dashboard-or-card` that listed as `disabled` or `locked` in the `embedding-params`
-  whitelist, or not present in the whitelist. This is done so the frontend doesn't display widgets for params the user
-  can't set."
-  [dashboard-or-card embedding-params :- ms/EmbeddingParams]
-  (let [params-to-remove (get-params-to-remove (:parameters dashboard-or-card) embedding-params)]
-    (update dashboard-or-card :parameters remove-params-in-set params-to-remove)))
-
-(defn- remove-token-parameters
-  "Removes any parameters with slugs matching keys provided in `token-params`, as these should not be exposed to the
-  user."
-  [dashboard-or-card token-params]
-  (update dashboard-or-card :parameters remove-params-in-set (set (keys token-params))))
-
-(defn- substitute-token-parameters-in-text
-  "For any dashboard parameters with slugs matching keys provided in `token-params`, substitute their values from the
-  token into any Markdown dashboard cards with linked variables. This needs to be done on the backend because we don't
-  make these parameters visible at all to the frontend."
-  [dashboard token-params]
-  (let [params             (:parameters dashboard)
-        dashcards      (:dashcards dashboard)
-        params-with-values (reduce
-                            (fn [acc param]
-                             (if-let [value (get token-params (keyword (:slug param)))]
-                                (conj acc (assoc param :value value))
-                                acc))
-                            []
-                            params)]
-    (assoc dashboard
-           :dashcards
-           (map
-            (fn [card]
-              (if (-> card :visualization_settings :virtual_card)
-                (pulse-params/process-virtual-dashcard card params-with-values)
-                card))
-            dashcards))))
-
-(mu/defn ^:private apply-slug->value :- [:maybe [:sequential
-                                                 [:map
-                                                  [:slug ms/NonBlankString]
-                                                  [:type :keyword]
-                                                  [:target :any]
-                                                  [:value :any]]]]
-  "Adds `value` to parameters with `slug` matching a key in `merged-slug->value` and removes parameters without a
-   `value`."
-  [parameters slug->value]
-  (when (seq parameters)
-    (for [param parameters
-          :let  [slug  (keyword (:slug param))
-                 value (get slug->value slug)
-                 ;; operator parameters expect a sequence of values so if we get a lone value (e.g. from a single URL
-                 ;; query parameter) wrap it in a sequence
-                 value (if (and (some? value)
-                                (params.ops/operator? (:type param)))
-                         (u/one-or-many value)
-                         value)]
-          :when (contains? slug->value slug)]
-      (assoc (select-keys param [:type :target :slug])
-             :value value))))
-
-(defn- resolve-card-parameters
-  "Returns parameters for a card (HUH?)" ; TODO - better docstring
-  [card-or-id]
-  (-> (t2/select-one [Card :dataset_query :parameters], :id (u/the-id card-or-id))
-      api.public/combine-parameters-and-template-tags
-      :parameters))
-
-(mu/defn ^:private resolve-dashboard-parameters :- [:sequential api.dashboard/ParameterWithID]
-  "Given a `dashboard-id` and parameters map in the format `slug->value`, return a sequence of parameters with `:id`s
-  that can be passed to various functions in the `metabase.api.dashboard` namespace such as
-  [[metabase.api.dashboard/process-query-for-dashcard]]."
-  [dashboard-id :- ms/PositiveInt
-   slug->value  :- :map]
-  (let [parameters (t2/select-one-fn :parameters Dashboard :id dashboard-id)
-        slug->id   (into {} (map (juxt :slug :id)) parameters)]
-    (vec (for [[slug value] slug->value
-               :let         [slug (u/qualified-name slug)]]
-           {:slug  slug
-            :id    (or (get slug->id slug)
-                       (throw (ex-info (tru "No matching parameter with slug {0}. Found: {1}" (pr-str slug) (pr-str (keys slug->id)))
-                                       {:status-code          400
-                                        :slug                 slug
-                                        :dashboard-parameters parameters})))
-            :value value}))))
-
-(mu/defn ^:private normalize-query-params :- [:map-of :keyword :any]
-  "Take a map of `query-params` and make sure they're in the right format for the rest of our code. Our
-  `wrap-keyword-params` middleware normally converts all query params keys to keywords, but only if they seem like
-  ones that make sense as keywords. Some params, such as ones that start with a number, do not pass this test, and are
-  not automatically converted. Thus we must do it ourselves here to make sure things are done as we'd expect.
-  Also, any param values that are blank strings should be parsed as nil, representing the absence of a value."
-  [query-params]
-  (-> query-params
-      (update-keys keyword)
-      (update-vals (fn [v] (if (= v "") nil v)))))
-
-
-;;; ---------------------------- Card Fns used by both /api/embed and /api/preview_embed -----------------------------
-
-(defn card-for-unsigned-token
-  "Return the info needed for embedding about Card specified in `token`. Additional `constraints` can be passed to the
-  `public-card` function that fetches the Card."
-  [unsigned-token & {:keys [embedding-params constraints]}]
-  {:pre [((some-fn empty? sequential?) constraints) (even? (count constraints))]}
-  (let [card-id      (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])
-        token-params (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
-    (-> (apply api.public/public-card :id card-id, constraints)
-        api.public/combine-parameters-and-template-tags
-        (remove-token-parameters token-params)
-        (remove-locked-and-disabled-params (or embedding-params
-                                               (t2/select-one-fn :embedding_params Card :id card-id))))))
-
-(defn process-query-for-card-with-params
-  "Run the query associated with Card with `card-id` using JWT `token-params`, user-supplied URL `query-params`,
-   an `embedding-params` whitelist, and additional query `options`. Returns `StreamingResponse` that should be
-  returned as the API endpoint result."
-  [& {:keys [export-format card-id embedding-params token-params query-params qp constraints options]
-      :or   {qp qp.card/process-query-for-card-default-qp}}]
-  {:pre [(integer? card-id) (u/maybe? map? embedding-params) (map? token-params) (map? query-params)]}
-  (let [merged-slug->value (validate-and-merge-params embedding-params token-params (normalize-query-params query-params))
-        parameters         (apply-slug->value (resolve-card-parameters card-id) merged-slug->value)]
-    (m/mapply api.public/process-query-for-card-with-id
-              card-id export-format parameters
-              :context     :embedded-question
-              :constraints constraints
-              :qp          qp
-              options)))
-
-
-;;; -------------------------- Dashboard Fns used by both /api/embed and /api/preview_embed --------------------------
-
-(defn- remove-linked-filters-param-values [dashboard]
-  (let [param-ids (set (map :id (:parameters dashboard)))
-        param-ids-to-remove (set (for [{param-id :id
-                                        filtering-parameters :filteringParameters} (:parameters dashboard)
-                                       filtering-parameter-id filtering-parameters
-                                       :when (not (contains? param-ids filtering-parameter-id))]
-                                   param-id))
-        linked-field-ids (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-remove))]
-    (update dashboard :param_values #(->> %
-                                          (map (fn [[param-id param]]
-                                                 {param-id (cond-> param
-                                                             (contains? linked-field-ids param-id) ;; is param linked?
-                                                             (assoc :values []))}))
-                                          (into {})))))
-
-(defn- remove-locked-parameters [dashboard embedding-params]
-  (let [params                    (:parameters dashboard)
-        {params-to-remove :remove
-         params-to-keep   :keep}  (classify-params-as-keep-or-remove params embedding-params)
-        param-ids-to-remove       (set (keep (fn [{:keys [slug id]}]
-                                               (when (contains? params-to-remove (keyword slug)) id))
-                                             params))
-        param-ids-to-keep         (set (keep (fn [{:keys [slug id]}]
-                                               (when (contains? params-to-keep (keyword slug)) id))
-                                             params))
-        field-ids-to-maybe-remove (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-remove))
-        field-ids-to-keep         (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-keep))
-        field-ids-to-remove       (set/difference field-ids-to-maybe-remove field-ids-to-keep)
-        remove-parameters         (fn [dashcard]
-                                    (update dashcard :parameter_mappings
-                                            (fn [param-mappings]
-                                              (remove (fn [{:keys [parameter_id]}]
-                                                        (contains? param-ids-to-remove parameter_id)) param-mappings))))]
-    (-> dashboard
-        (update :dashcards #(map remove-parameters %))
-        (update :param_fields #(apply dissoc % field-ids-to-remove))
-        (update :param_values #(apply dissoc % field-ids-to-remove)))))
-
-(defn dashboard-for-unsigned-token
-  "Return the info needed for embedding about Dashboard specified in `token`. Additional `constraints` can be passed to
-  the `public-dashboard` function that fetches the Dashboard."
-  [unsigned-token & {:keys [embedding-params constraints]}]
-  {:pre [((some-fn empty? sequential?) constraints) (even? (count constraints))]}
-  (let [dashboard-id (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
-        embedding-params (or embedding-params
-                             (t2/select-one-fn :embedding_params Dashboard, :id dashboard-id))
-        token-params (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
-    (-> (apply api.public/public-dashboard :id dashboard-id, constraints)
-        (substitute-token-parameters-in-text token-params)
-        (remove-locked-parameters embedding-params)
-        (remove-token-parameters token-params)
-        (remove-locked-and-disabled-params embedding-params)
-        (remove-linked-filters-param-values))))
-
-(defn- get-embed-dashboard-context
-  "If a certain export-format is given, return the correct embedded dashboard context."
-  [export-format]
-  (case export-format
-    "csv"  :embedded-csv-download
-    "xlsx" :embedded-xlsx-download
-    "json" :embedded-json-download
-    :embedded-dashboard))
-
-(defn process-query-for-dashcard
-  "Return results for running the query belonging to a DashboardCard. Returns a `StreamingResponse`."
-  [& {:keys [dashboard-id dashcard-id card-id export-format embedding-params token-params middleware
-             query-params constraints qp]
-      :or   {constraints (qp.constraints/default-query-constraints)
-             qp          qp.card/process-query-for-card-default-qp}}]
-  {:pre [(integer? dashboard-id) (integer? dashcard-id) (integer? card-id) (u/maybe? map? embedding-params)
-         (map? token-params) (map? query-params)]}
-  (let [slug->value (validate-and-merge-params embedding-params token-params (normalize-query-params query-params))
-        parameters  (resolve-dashboard-parameters dashboard-id slug->value)]
-    (api.public/process-query-for-dashcard
-     :dashboard-id  dashboard-id
-     :card-id       card-id
-     :dashcard-id   dashcard-id
-     :export-format export-format
-     :parameters    parameters
-     :qp            qp
-     :context       (get-embed-dashboard-context export-format)
-     :constraints   constraints
-     :middleware    middleware)))
-
-
-;;; ------------------------------------- Other /api/embed-specific utility fns --------------------------------------
-
-(defn- check-embedding-enabled-for-object
-  "Check that embedding is enabled, that `object` exists, and embedding for `object` is enabled."
-  ([entity id]
-   (api/check (pos-int? id)
-     [400 (tru "{0} id should be a positive integer." (name entity))])
-   (check-embedding-enabled-for-object (t2/select-one [entity :enable_embedding] :id id)))
-
-  ([object]
-   (validation/check-embedding-enabled)
-   (api/check-404 object)
-   (api/check-not-archived object)
-   (api/check (:enable_embedding object)
-     [400 (tru "Embedding is not enabled for this object.")])))
-
-(def ^:private ^{:arglists '([dashboard-id])} check-embedding-enabled-for-dashboard
-  "Runs check-embedding-enabled-for-object for a given Dashboard id"
-  (partial check-embedding-enabled-for-object Dashboard))
-
-(def ^:private ^{:arglists '([card-id])} check-embedding-enabled-for-card
-  "Runs check-embedding-enabled-for-object for a given Card id"
-  (partial check-embedding-enabled-for-object Card))
-
-
 ;;; ------------------------------------------- /api/embed/card endpoints --------------------------------------------
 
 (api/defendpoint GET "/card/:token"
@@ -389,8 +44,8 @@
      {:resource {:question <card-id>}}"
   [token]
   (let [unsigned (embed/unsign token)]
-    (check-embedding-enabled-for-card (embed/get-in-unsigned-token-or-throw unsigned [:resource :question]))
-    (u/prog1 (card-for-unsigned-token unsigned, :constraints [:enable_embedding true])
+    (api.embed.common/check-embedding-enabled-for-card (embed/get-in-unsigned-token-or-throw unsigned [:resource :question]))
+    (u/prog1 (api.embed.common/card-for-unsigned-token unsigned, :constraints [:enable_embedding true])
       (events/publish-event! :event/card-read {:object <>, :user-id api/*current-user-id*}))))
 
 (defn ^:private run-query-for-unsigned-token-async
@@ -401,8 +56,8 @@
                                                        qp          qp.card/process-query-for-card-default-qp}
                                                 :as   options}]
   (let [card-id (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (check-embedding-enabled-for-card card-id)
-    (process-query-for-card-with-params
+    (api.embed.common/check-embedding-enabled-for-card card-id)
+    (api.embed.common/process-query-for-card-with-params
       :export-format     export-format
       :card-id           card-id
       :token-params      (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
@@ -447,8 +102,8 @@
      {:resource {:dashboard <dashboard-id>}}"
   [token]
   (let [unsigned (embed/unsign token)]
-    (check-embedding-enabled-for-dashboard (embed/get-in-unsigned-token-or-throw unsigned [:resource :dashboard]))
-    (u/prog1 (dashboard-for-unsigned-token unsigned, :constraints [:enable_embedding true])
+    (api.embed.common/check-embedding-enabled-for-dashboard (embed/get-in-unsigned-token-or-throw unsigned [:resource :dashboard]))
+    (u/prog1 (api.embed.common/dashboard-for-unsigned-token unsigned, :constraints [:enable_embedding true])
       (events/publish-event! :event/dashboard-read {:user-id api/*current-user-id*
                                                     :object <>}))))
 
@@ -470,8 +125,8 @@
              qp          qp.card/process-query-for-card-default-qp}}]
   (let [unsigned-token (embed/unsign token)
         dashboard-id   (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
-    (check-embedding-enabled-for-dashboard dashboard-id)
-    (process-query-for-dashcard
+    (api.embed.common/check-embedding-enabled-for-dashboard dashboard-id)
+    (api.embed.common/process-query-for-dashcard
       :export-format    export-format
       :dashboard-id     dashboard-id
       :dashcard-id      dashcard-id
@@ -504,7 +159,7 @@
   {field-id ms/PositiveInt}
   (let [unsigned-token (embed/unsign token)
         card-id        (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (check-embedding-enabled-for-card card-id)
+    (api.embed.common/check-embedding-enabled-for-card card-id)
     (api.public/card-and-field-id->values card-id field-id)))
 
 (api/defendpoint GET "/dashboard/:token/field/:field-id/values"
@@ -513,7 +168,7 @@
   {field-id ms/PositiveInt}
   (let [unsigned-token (embed/unsign token)
         dashboard-id   (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
-    (check-embedding-enabled-for-dashboard dashboard-id)
+    (api.embed.common/check-embedding-enabled-for-dashboard dashboard-id)
     (api.public/dashboard-and-field-id->values dashboard-id field-id)))
 
 
@@ -528,7 +183,7 @@
    limit           [:maybe ms/PositiveInt]}
   (let [unsigned-token (embed/unsign token)
         card-id        (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (check-embedding-enabled-for-card card-id)
+    (api.embed.common/check-embedding-enabled-for-card card-id)
     (api.public/search-card-fields card-id field-id search-field-id value (when limit (Integer/parseInt limit)))))
 
 (api/defendpoint GET "/dashboard/:token/field/:field-id/search/:search-field-id"
@@ -540,7 +195,7 @@
    limit           [:maybe ms/PositiveInt]}
   (let [unsigned-token (embed/unsign token)
         dashboard-id   (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
-    (check-embedding-enabled-for-dashboard dashboard-id)
+    (api.embed.common/check-embedding-enabled-for-dashboard dashboard-id)
     (api.public/search-dashboard-fields dashboard-id field-id search-field-id value (when limit
                                                                                       (Integer/parseInt limit)))))
 
@@ -556,7 +211,7 @@
    value       ms/NonBlankString}
   (let [unsigned-token (embed/unsign token)
         card-id        (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (check-embedding-enabled-for-card card-id)
+    (api.embed.common/check-embedding-enabled-for-card card-id)
     (api.public/card-field-remapped-values card-id field-id remapped-id value)))
 
 (api/defendpoint GET "/dashboard/:token/field/:field-id/remapping/:remapped-id"
@@ -568,7 +223,7 @@
    value       ms/NonBlankString}
   (let [unsigned-token (embed/unsign token)
         dashboard-id   (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])]
-    (check-embedding-enabled-for-dashboard dashboard-id)
+    (api.embed.common/check-embedding-enabled-for-dashboard dashboard-id)
     (api.public/dashboard-field-remapped-values dashboard-id field-id remapped-id value)))
 
 (api/defendpoint GET ["/dashboard/:token/dashcard/:dashcard-id/card/:card-id/:export-format"
@@ -599,117 +254,16 @@
 ;; variables whose name includes `id-` e.g. `id-query-params` below are ones that are keyed by ID; ones whose name
 ;; includes `slug-` are keyed by slug.
 
-(mu/defn ^:private param-values-merged-params :- [:map-of ms/NonBlankString :any]
-  [id->slug slug->id embedding-params token-params id-query-params]
-  (let [slug-query-params  (into {}
-                                 (for [[id v] id-query-params]
-                                   [(or (get id->slug (name id))
-                                        (throw (ex-info (tru "Invalid query params: could not determine slug for parameter with ID {0}"
-                                                             (pr-str id))
-                                                        {:id              (name id)
-                                                         :id->slug        id->slug
-                                                         :id-query-params id-query-params})))
-                                    v]))
-        slug-query-params  (normalize-query-params slug-query-params)
-        merged-slug->value (validate-and-merge-params embedding-params token-params slug-query-params)]
-    (into {} (for [[slug value] merged-slug->value]
-               [(get slug->id (name slug)) value]))))
-
-(defn card-param-values
-  "Search for card parameter values. Does security checks to ensure the parameter is on the card and then gets param
-  values according to [[api.card/param-values]]."
-  [{:keys [unsigned-token card param-key search-prefix]}]
-  (let [slug-token-params   (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
-        parameters          (or (seq (:parameters card))
-                                (card/template-tag-parameters card))
-        id->slug            (into {} (map (juxt :id :slug) parameters))
-        slug->id            (into {} (map (juxt :slug :id) parameters))
-        searched-param-slug (get id->slug param-key)
-        embedding-params    (:embedding_params card)]
-    (try
-      (when-not (= (get embedding-params (keyword searched-param-slug)) "enabled")
-        (throw (ex-info (tru "Cannot search for values: {0} is not an enabled parameter."
-                             (pr-str searched-param-slug))
-                        {:status-code 400})))
-      (when (get slug-token-params (keyword searched-param-slug))
-        (throw (ex-info (tru "You can''t specify a value for {0} if it's already set in the JWT." (pr-str searched-param-slug))
-                        {:status-code 400})))
-      (try
-        (binding [api/*current-user-permissions-set* (atom #{"/"})
-                  api/*is-superuser?* true]
-          (api.card/param-values card param-key search-prefix))
-        (catch Throwable e
-          (throw (ex-info (.getMessage e)
-                          {:card-id       (u/the-id card)
-                           :param-key     param-key
-                           :search-prefix search-prefix}
-                          e))))
-      (catch Throwable e
-        (let [e (ex-info (.getMessage e)
-                         {:card-id (u/the-id card)
-                          :card-params (:parametres card)
-                          :allowed-param-slugs embedding-params
-                          :slug->id            slug->id
-                          :id->slug            id->slug
-                          :param-id            param-key
-                          :param-slug          searched-param-slug
-                          :token-params        slug-token-params}
-                         e)]
-          (log/errorf e "embedded card-param-values error\n%s"
-                      (u/pprint-to-str (u/all-ex-data e)))
-          (throw e))))))
-
-(defn- dashboard-param-values [token searched-param-id prefix id-query-params]
-  (let [unsigned-token                       (embed/unsign token)
-        dashboard-id                         (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
-        _                                    (check-embedding-enabled-for-dashboard dashboard-id)
-        slug-token-params                    (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
-        {parameters       :parameters
-         embedding-params :embedding_params} (t2/select-one Dashboard :id dashboard-id)
-        id->slug                             (into {} (map (juxt :id :slug) parameters))
-        slug->id                             (into {} (map (juxt :slug :id) parameters))
-        searched-param-slug                  (get id->slug searched-param-id)]
-    (try
-      ;; you can only search for values of a parameter if it is ENABLED and NOT PRESENT in the JWT.
-      (when-not (= (get embedding-params (keyword searched-param-slug)) "enabled")
-        (throw (ex-info (tru "Cannot search for values: {0} is not an enabled parameter." (pr-str searched-param-slug))
-                        {:status-code 400})))
-      (when (get slug-token-params (keyword searched-param-slug))
-        (throw (ex-info (tru "You can''t specify a value for {0} if it's already set in the JWT." (pr-str searched-param-slug))
-                        {:status-code 400})))
-      ;; ok, at this point we can run the query
-      (let [merged-id-params (param-values-merged-params id->slug slug->id embedding-params slug-token-params id-query-params)]
-        (try
-          (binding [api/*current-user-permissions-set* (atom #{"/"})
-                    api/*is-superuser?*                 true]
-            (api.dashboard/param-values (t2/select-one Dashboard :id dashboard-id) searched-param-id merged-id-params prefix))
-          (catch Throwable e
-            (throw (ex-info (.getMessage e)
-                            {:merged-id-params merged-id-params}
-                            e)))))
-      (catch Throwable e
-        (let [e (ex-info (.getMessage e)
-                         {:dashboard-id        dashboard-id
-                          :dashboard-params    parameters
-                          :allowed-param-slugs embedding-params
-                          :slug->id            slug->id
-                          :id->slug            id->slug
-                          :param-id            searched-param-id
-                          :param-slug          searched-param-slug
-                          :token-params        slug-token-params}
-                         e)]
-          (log/errorf e "Chain filter error\n%s" (u/pprint-to-str (u/all-ex-data e)))
-          (throw e))))))
 
 (api/defendpoint GET "/dashboard/:token/params/:param-key/values"
   "Embedded version of chain filter values endpoint."
   [token param-key :as {:keys [query-params]}]
-  (dashboard-param-values token param-key nil query-params))
+  (api.embed.common/dashboard-param-values token param-key nil query-params))
 
 (api/defendpoint GET "/dashboard/:token/params/:param-key/search/:prefix"
   "Embedded version of chain filter search endpoint."
   [token param-key prefix :as {:keys [query-params]}]
-  (dashboard-param-values token param-key prefix query-params))
+  (api.embed.common/dashboard-param-values token param-key prefix query-params))
 
 (api/defendpoint GET "/card/:token/params/:param-key/values"
   "Embedded version of api.card filter values endpoint."
@@ -717,8 +271,8 @@
   (let [unsigned (embed/unsign token)
         card-id  (embed/get-in-unsigned-token-or-throw unsigned [:resource :question])
         card     (t2/select-one Card :id card-id)]
-    (check-embedding-enabled-for-card card-id)
-    (card-param-values {:unsigned-token unsigned
+    (api.embed.common/check-embedding-enabled-for-card card-id)
+    (api.embed.common/card-param-values {:unsigned-token unsigned
                         :card           card
                         :param-key      param-key})))
 
@@ -728,8 +282,8 @@
   (let [unsigned (embed/unsign token)
         card-id  (embed/get-in-unsigned-token-or-throw unsigned [:resource :question])
         card     (t2/select-one Card :id card-id)]
-    (check-embedding-enabled-for-card card-id)
-    (card-param-values {:unsigned-token unsigned
+    (api.embed.common/check-embedding-enabled-for-card card-id)
+    (api.embed.common/card-param-values {:unsigned-token unsigned
                         :card           card
                         :param-key      param-key
                         :search-prefix  prefix})))
diff --git a/src/metabase/api/embed/common.clj b/src/metabase/api/embed/common.clj
new file mode 100644
index 00000000000..3f25b55db1c
--- /dev/null
+++ b/src/metabase/api/embed/common.clj
@@ -0,0 +1,478 @@
+(ns metabase.api.embed.common
+  (:require
+   [clojure.set :as set]
+   [clojure.string :as str]
+   [medley.core :as m]
+   [metabase.api.card :as api.card]
+   [metabase.api.common :as api]
+   [metabase.api.common.validation :as validation]
+   [metabase.api.dashboard :as api.dashboard]
+   [metabase.api.public :as api.public]
+   [metabase.driver.common.parameters.operators :as params.ops]
+   [metabase.models.card :as card]
+   [metabase.models.params :as params]
+   [metabase.pulse.parameters :as pulse-params]
+   [metabase.query-processor.card :as qp.card]
+   [metabase.query-processor.middleware.constraints :as qp.constraints]
+   [metabase.util :as u]
+   [metabase.util.embed :as embed]
+   [metabase.util.i18n
+    :as i18n
+    :refer [tru]]
+   [metabase.util.log :as log]
+   [metabase.util.malli :as mu]
+   [metabase.util.malli.schema :as ms]
+   [toucan2.core :as t2]))
+
+(set! *warn-on-reflection* true)
+
+(defn- valid-param?
+  "Is V a valid param value? (If it is a String, is it non-blank?)"
+  [v]
+  (or (not (string? v))
+      (not (str/blank? v))))
+
+(defn- check-params-are-allowed
+  "Check that the conditions specified by `object-embedding-params` are satisfied."
+  [object-embedding-params token-params user-params]
+  (let [all-params        (set/union token-params user-params)
+        duplicated-params (set/intersection token-params user-params)]
+    (doseq [[param status] object-embedding-params]
+      (case status
+        ;; disabled means a param is not allowed to be specified by either token or user
+        "disabled" (api/check (not (contains? all-params param))
+                              [400 (tru "You''re not allowed to specify a value for {0}." param)])
+        ;; enabled means either JWT *or* user can specify the param, but not both. Param is *not* required
+        "enabled"  (api/check (not (contains? duplicated-params param))
+                              [400 (tru "You can''t specify a value for {0} if it''s already set in the JWT." param)])
+        ;; locked means JWT must specify param
+        "locked"   (api/check
+                    (contains? token-params param)      [400 (tru "You must specify a value for {0} in the JWT." param)]
+                    (not (contains? user-params param)) [400 (tru "You can only specify a value for {0} in the JWT." param)])))))
+
+(defn- check-params-exist
+  "Make sure all the params specified are specified in `object-embedding-params`."
+  [object-embedding-params all-params]
+  (let [embedding-params (set (keys object-embedding-params))]
+    (doseq [k all-params]
+      (api/check (contains? embedding-params k)
+                 [400 (format "Unknown parameter %s." k)]))))
+
+(defn- check-param-sets
+  "Validate that sets of params passed as part of the JWT token and by the user (as query params, i.e. as part of the
+  URL) are valid for the `object-embedding-params`. `token-params` and `user-params` should be sets of all valid param
+  keys specified in the JWT or by the user, respectively."
+  [object-embedding-params token-params user-params]
+  ;; TODO - maybe make this log/debug once embedding is wrapped up
+  (log/debug "Validating params for embedded object:\n"
+             "object embedding params:" object-embedding-params
+             "token params:"            token-params
+             "user params:"             user-params)
+  (check-params-are-allowed object-embedding-params token-params user-params)
+  (check-params-exist object-embedding-params (set/union token-params user-params)))
+
+(defn- check-embedding-enabled-for-object
+  "Check that embedding is enabled, that `object` exists, and embedding for `object` is enabled."
+  ([entity id]
+   (api/check (pos-int? id)
+              [400 (tru "{0} id should be a positive integer." (name entity))])
+   (check-embedding-enabled-for-object (t2/select-one [entity :enable_embedding] :id id)))
+
+  ([object]
+   (validation/check-embedding-enabled)
+   (api/check-404 object)
+   (api/check-not-archived object)
+   (api/check (:enable_embedding object)
+              [400 (tru "Embedding is not enabled for this object.")])))
+
+(def ^{:arglists '([card-id])} check-embedding-enabled-for-card
+  "Runs check-embedding-enabled-for-object for a given Card id"
+  (partial check-embedding-enabled-for-object :model/Card))
+
+(def ^{:arglists '([dashboard-id])} check-embedding-enabled-for-dashboard
+  "Runs check-embedding-enabled-for-object for a given Dashboard id"
+  (partial check-embedding-enabled-for-object :model/Dashboard))
+
+(defn- resolve-card-parameters
+  "Returns parameters for a card (HUH?)" ; TODO - better docstring
+  [card-or-id]
+  (-> (t2/select-one [:model/Card :dataset_query :parameters], :id (u/the-id card-or-id))
+      api.public/combine-parameters-and-template-tags
+      :parameters))
+
+(mu/defn ^:private resolve-dashboard-parameters :- [:sequential api.dashboard/ParameterWithID]
+  "Given a `dashboard-id` and parameters map in the format `slug->value`, return a sequence of parameters with `:id`s
+  that can be passed to various functions in the `metabase.api.dashboard` namespace such as
+  [[metabase.api.dashboard/process-query-for-dashcard]]."
+  [dashboard-id :- ms/PositiveInt
+   slug->value  :- :map]
+  (let [parameters (t2/select-one-fn :parameters :model/Dashboard :id dashboard-id)
+        slug->id   (into {} (map (juxt :slug :id)) parameters)]
+    (vec (for [[slug value] slug->value
+               :let         [slug (u/qualified-name slug)]]
+           {:slug  slug
+            :id    (or (get slug->id slug)
+                       (throw (ex-info (tru "No matching parameter with slug {0}. Found: {1}" (pr-str slug) (pr-str (keys slug->id)))
+                                       {:status-code          400
+                                        :slug                 slug
+                                        :dashboard-parameters parameters})))
+            :value value}))))
+
+(mu/defn normalize-query-params :- [:map-of :keyword :any]
+  "Take a map of `query-params` and make sure they're in the right format for the rest of our code. Our
+  `wrap-keyword-params` middleware normally converts all query params keys to keywords, but only if they seem like
+  ones that make sense as keywords. Some params, such as ones that start with a number, do not pass this test, and are
+  not automatically converted. Thus we must do it ourselves here to make sure things are done as we'd expect.
+  Also, any param values that are blank strings should be parsed as nil, representing the absence of a value."
+  [query-params]
+  (-> query-params
+      (update-keys keyword)
+      (update-vals (fn [v] (if (= v "") nil v)))))
+
+(mu/defn validate-and-merge-params :- [:map-of :keyword :any]
+  "Validate that the `token-params` passed in the JWT and the `user-params` (passed as part of the URL) are allowed, and
+  that ones that are required are specified by checking them against a Card or Dashboard's `object-embedding-params`
+  (the object's value of `:embedding_params`). Throws a 400 if any of the checks fail. If all checks are successful,
+  returns a *merged* parameters map."
+  [object-embedding-params :- ms/EmbeddingParams
+   token-params            :- [:map-of :keyword :any]
+   user-params             :- [:map-of :keyword :any]]
+  (check-param-sets object-embedding-params
+                    (set (keys (m/filter-vals valid-param? token-params)))
+                    (set (keys (m/filter-vals valid-param? user-params))))
+  ;; ok, everything checks out, now return the merged params map
+  (merge user-params token-params))
+
+(mu/defn ^:private param-values-merged-params :- [:map-of ms/NonBlankString :any]
+  [id->slug slug->id embedding-params token-params id-query-params]
+  (let [slug-query-params  (into {}
+                                 (for [[id v] id-query-params]
+                                   [(or (get id->slug (name id))
+                                        (throw (ex-info (tru "Invalid query params: could not determine slug for parameter with ID {0}"
+                                                             (pr-str id))
+                                                        {:id              (name id)
+                                                         :id->slug        id->slug
+                                                         :id-query-params id-query-params})))
+                                    v]))
+        slug-query-params  (normalize-query-params slug-query-params)
+        merged-slug->value (validate-and-merge-params embedding-params token-params slug-query-params)]
+    (into {} (for [[slug value] merged-slug->value]
+               [(get slug->id (name slug)) value]))))
+
+
+
+;;; ---------------------------------------------- Other Param Util Fns ----------------------------------------------
+
+(defn- remove-params-in-set
+  "Remove any `params` from the list whose `:slug` is in the `params-to-remove` set."
+  [params params-to-remove]
+  (for [param params
+        :when (not (contains? params-to-remove (keyword (:slug param))))]
+    param))
+
+(defn- classify-params-as-keep-or-remove
+  "Classifies the params in the `dashboard-or-card-params` seq and the param slugs in `embedding-params` map according to:
+  Parameters in `dashboard-or-card-params` whose slugs are NOT in the `embedding-params` map must be removed.
+  Parameter slugs in `embedding-params` with the value 'enabled' are kept, 'disabled' or 'locked' are not kept.
+
+  The resulting classification is returned as a map with keys :keep and :remove whose values are sets of parameter slugs."
+  [dashboard-or-card-params embedding-params]
+  (let [param-slugs                   (map #(keyword (:slug %)) dashboard-or-card-params)
+        grouped-param-slugs           {:remove (remove (fn [k] (contains? embedding-params k)) param-slugs)}
+        grouped-embedding-param-slugs (-> (group-by #(= (second %) "enabled") embedding-params)
+                                          (update-keys {true :keep false :remove})
+                                          (update-vals #(into #{} (map first) %)))]
+    (merge-with (comp set concat)
+                {:keep #{} :remove #{}}
+                grouped-param-slugs
+                grouped-embedding-param-slugs)))
+
+(defn- get-params-to-remove
+  [dashboard-or-card-params embedding-params]
+  (:remove (classify-params-as-keep-or-remove dashboard-or-card-params embedding-params)))
+
+(mu/defn ^:private remove-locked-and-disabled-params
+  "Remove the `:parameters` for `dashboard-or-card` that listed as `disabled` or `locked` in the `embedding-params`
+  whitelist, or not present in the whitelist. This is done so the frontend doesn't display widgets for params the user
+  can't set."
+  [dashboard-or-card embedding-params :- ms/EmbeddingParams]
+  (let [params-to-remove (get-params-to-remove (:parameters dashboard-or-card) embedding-params)]
+    (update dashboard-or-card :parameters remove-params-in-set params-to-remove)))
+
+(defn- remove-token-parameters
+  "Removes any parameters with slugs matching keys provided in `token-params`, as these should not be exposed to the
+  user."
+  [dashboard-or-card token-params]
+  (update dashboard-or-card :parameters remove-params-in-set (set (keys token-params))))
+
+(defn- substitute-token-parameters-in-text
+  "For any dashboard parameters with slugs matching keys provided in `token-params`, substitute their values from the
+  token into any Markdown dashboard cards with linked variables. This needs to be done on the backend because we don't
+  make these parameters visible at all to the frontend."
+  [dashboard token-params]
+  (let [params             (:parameters dashboard)
+        dashcards      (:dashcards dashboard)
+        params-with-values (reduce
+                            (fn [acc param]
+                             (if-let [value (get token-params (keyword (:slug param)))]
+                                (conj acc (assoc param :value value))
+                                acc))
+                            []
+                            params)]
+    (assoc dashboard
+           :dashcards
+           (map
+            (fn [card]
+              (if (-> card :visualization_settings :virtual_card)
+                (pulse-params/process-virtual-dashcard card params-with-values)
+                card))
+            dashcards))))
+
+(mu/defn ^:private apply-slug->value :- [:maybe [:sequential
+                                                 [:map
+                                                  [:slug ms/NonBlankString]
+                                                  [:type :keyword]
+                                                  [:target :any]
+                                                  [:value :any]]]]
+  "Adds `value` to parameters with `slug` matching a key in `merged-slug->value` and removes parameters without a
+   `value`."
+  [parameters slug->value]
+  (when (seq parameters)
+    (for [param parameters
+          :let  [slug  (keyword (:slug param))
+                 value (get slug->value slug)
+                 ;; operator parameters expect a sequence of values so if we get a lone value (e.g. from a single URL
+                 ;; query parameter) wrap it in a sequence
+                 value (if (and (some? value)
+                                (params.ops/operator? (:type param)))
+                         (u/one-or-many value)
+                         value)]
+          :when (contains? slug->value slug)]
+      (assoc (select-keys param [:type :target :slug])
+             :value value))))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+;;; ---------------------------- Card Fns used by both /api/embed and /api/preview_embed -----------------------------
+
+(defn card-for-unsigned-token
+  "Return the info needed for embedding about Card specified in `token`. Additional `constraints` can be passed to the
+  `public-card` function that fetches the Card."
+  [unsigned-token & {:keys [embedding-params constraints]}]
+  {:pre [((some-fn empty? sequential?) constraints) (even? (count constraints))]}
+  (let [card-id      (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])
+        token-params (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
+    (-> (apply api.public/public-card :id card-id, constraints)
+        api.public/combine-parameters-and-template-tags
+        (remove-token-parameters token-params)
+        (remove-locked-and-disabled-params (or embedding-params
+                                               (t2/select-one-fn :embedding_params :model/Card :id card-id))))))
+
+(defn process-query-for-card-with-params
+  "Run the query associated with Card with `card-id` using JWT `token-params`, user-supplied URL `query-params`,
+   an `embedding-params` whitelist, and additional query `options`. Returns `StreamingResponse` that should be
+  returned as the API endpoint result."
+  [& {:keys [export-format card-id embedding-params token-params query-params qp constraints options]
+      :or   {qp qp.card/process-query-for-card-default-qp}}]
+  {:pre [(integer? card-id) (u/maybe? map? embedding-params) (map? token-params) (map? query-params)]}
+  (let [merged-slug->value (validate-and-merge-params embedding-params token-params (normalize-query-params query-params))
+        parameters         (apply-slug->value (resolve-card-parameters card-id) merged-slug->value)]
+    (m/mapply api.public/process-query-for-card-with-id
+              card-id export-format parameters
+              :context     :embedded-question
+              :constraints constraints
+              :qp          qp
+              options)))
+
+;;; -------------------------- Dashboard Fns used by both /api/embed and /api/preview_embed --------------------------
+
+(defn- remove-linked-filters-param-values [dashboard]
+  (let [param-ids (set (map :id (:parameters dashboard)))
+        param-ids-to-remove (set (for [{param-id :id
+                                        filtering-parameters :filteringParameters} (:parameters dashboard)
+                                       filtering-parameter-id filtering-parameters
+                                       :when (not (contains? param-ids filtering-parameter-id))]
+                                   param-id))
+        linked-field-ids (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-remove))]
+    (update dashboard :param_values #(->> %
+                                          (map (fn [[param-id param]]
+                                                 {param-id (cond-> param
+                                                             (contains? linked-field-ids param-id) ;; is param linked?
+                                                             (assoc :values []))}))
+                                          (into {})))))
+
+(defn- remove-locked-parameters [dashboard embedding-params]
+  (let [params                    (:parameters dashboard)
+        {params-to-remove :remove
+         params-to-keep   :keep}  (classify-params-as-keep-or-remove params embedding-params)
+        param-ids-to-remove       (set (keep (fn [{:keys [slug id]}]
+                                               (when (contains? params-to-remove (keyword slug)) id))
+                                             params))
+        param-ids-to-keep         (set (keep (fn [{:keys [slug id]}]
+                                               (when (contains? params-to-keep (keyword slug)) id))
+                                             params))
+        field-ids-to-maybe-remove (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-remove))
+        field-ids-to-keep         (set (mapcat (params/get-linked-field-ids (:dashcards dashboard)) param-ids-to-keep))
+        field-ids-to-remove       (set/difference field-ids-to-maybe-remove field-ids-to-keep)
+        remove-parameters         (fn [dashcard]
+                                    (update dashcard :parameter_mappings
+                                            (fn [param-mappings]
+                                              (remove (fn [{:keys [parameter_id]}]
+                                                        (contains? param-ids-to-remove parameter_id)) param-mappings))))]
+    (-> dashboard
+        (update :dashcards #(map remove-parameters %))
+        (update :param_fields #(apply dissoc % field-ids-to-remove))
+        (update :param_values #(apply dissoc % field-ids-to-remove)))))
+
+(defn dashboard-for-unsigned-token
+  "Return the info needed for embedding about Dashboard specified in `token`. Additional `constraints` can be passed to
+  the `public-dashboard` function that fetches the Dashboard."
+  [unsigned-token & {:keys [embedding-params constraints]}]
+  {:pre [((some-fn empty? sequential?) constraints) (even? (count constraints))]}
+  (let [dashboard-id (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
+        embedding-params (or embedding-params
+                             (t2/select-one-fn :embedding_params :model/Dashboard, :id dashboard-id))
+        token-params (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
+    (-> (apply api.public/public-dashboard :id dashboard-id, constraints)
+        (substitute-token-parameters-in-text token-params)
+        (remove-locked-parameters embedding-params)
+        (remove-token-parameters token-params)
+        (remove-locked-and-disabled-params embedding-params)
+        (remove-linked-filters-param-values))))
+
+(defn- get-embed-dashboard-context
+  "If a certain export-format is given, return the correct embedded dashboard context."
+  [export-format]
+  (case export-format
+    "csv"  :embedded-csv-download
+    "xlsx" :embedded-xlsx-download
+    "json" :embedded-json-download
+    :embedded-dashboard))
+
+(defn process-query-for-dashcard
+  "Return results for running the query belonging to a DashboardCard. Returns a `StreamingResponse`."
+  [& {:keys [dashboard-id dashcard-id card-id export-format embedding-params token-params middleware
+             query-params constraints qp]
+      :or   {constraints (qp.constraints/default-query-constraints)
+             qp          qp.card/process-query-for-card-default-qp}}]
+  {:pre [(integer? dashboard-id) (integer? dashcard-id) (integer? card-id) (u/maybe? map? embedding-params)
+         (map? token-params) (map? query-params)]}
+  (let [slug->value (validate-and-merge-params embedding-params token-params (normalize-query-params query-params))
+        parameters  (resolve-dashboard-parameters dashboard-id slug->value)]
+    (api.public/process-query-for-dashcard
+     :dashboard-id  dashboard-id
+     :card-id       card-id
+     :dashcard-id   dashcard-id
+     :export-format export-format
+     :parameters    parameters
+     :qp            qp
+     :context       (get-embed-dashboard-context export-format)
+     :constraints   constraints
+     :middleware    middleware)))
+
+(defn card-param-values
+  "Search for card parameter values. Does security checks to ensure the parameter is on the card and then gets param
+  values according to [[api.card/param-values]]."
+  [{:keys [unsigned-token card param-key search-prefix]}]
+  (let [slug-token-params   (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
+        parameters          (or (seq (:parameters card))
+                                (card/template-tag-parameters card))
+        id->slug            (into {} (map (juxt :id :slug) parameters))
+        slug->id            (into {} (map (juxt :slug :id) parameters))
+        searched-param-slug (get id->slug param-key)
+        embedding-params    (:embedding_params card)]
+    (try
+      (when-not (= (get embedding-params (keyword searched-param-slug)) "enabled")
+        (throw (ex-info (tru "Cannot search for values: {0} is not an enabled parameter."
+                             (pr-str searched-param-slug))
+                        {:status-code 400})))
+      (when (get slug-token-params (keyword searched-param-slug))
+        (throw (ex-info (tru "You can''t specify a value for {0} if it's already set in the JWT." (pr-str searched-param-slug))
+                        {:status-code 400})))
+      (try
+        (binding [api/*current-user-permissions-set* (atom #{"/"})
+                  api/*is-superuser?* true]
+          (api.card/param-values card param-key search-prefix))
+        (catch Throwable e
+          (throw (ex-info (.getMessage e)
+                          {:card-id       (u/the-id card)
+                           :param-key     param-key
+                           :search-prefix search-prefix}
+                          e))))
+      (catch Throwable e
+        (let [e (ex-info (.getMessage e)
+                         {:card-id (u/the-id card)
+                          :card-params (:parametres card)
+                          :allowed-param-slugs embedding-params
+                          :slug->id            slug->id
+                          :id->slug            id->slug
+                          :param-id            param-key
+                          :param-slug          searched-param-slug
+                          :token-params        slug-token-params}
+                         e)]
+          (log/errorf e "embedded card-param-values error\n%s"
+                      (u/pprint-to-str (u/all-ex-data e)))
+          (throw e))))))
+
+(defn dashboard-param-values
+  "Common implementation for fetching parameter values for embedding and preview-embedding."
+  [token searched-param-id prefix id-query-params]
+  (let [unsigned-token                       (embed/unsign token)
+        dashboard-id                         (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
+        _                                    (check-embedding-enabled-for-dashboard dashboard-id)
+        slug-token-params                    (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
+        {parameters       :parameters
+         embedding-params :embedding_params} (t2/select-one :model/Dashboard :id dashboard-id)
+        id->slug                             (into {} (map (juxt :id :slug) parameters))
+        slug->id                             (into {} (map (juxt :slug :id) parameters))
+        searched-param-slug                  (get id->slug searched-param-id)]
+    (try
+      ;; you can only search for values of a parameter if it is ENABLED and NOT PRESENT in the JWT.
+      (when-not (= (get embedding-params (keyword searched-param-slug)) "enabled")
+        (throw (ex-info (tru "Cannot search for values: {0} is not an enabled parameter." (pr-str searched-param-slug))
+                        {:status-code 400})))
+      (when (get slug-token-params (keyword searched-param-slug))
+        (throw (ex-info (tru "You can''t specify a value for {0} if it's already set in the JWT." (pr-str searched-param-slug))
+                        {:status-code 400})))
+      ;; ok, at this point we can run the query
+      (let [merged-id-params (param-values-merged-params id->slug slug->id embedding-params slug-token-params id-query-params)]
+        (try
+          (binding [api/*current-user-permissions-set* (atom #{"/"})
+                    api/*is-superuser?*                 true]
+            (api.dashboard/param-values (t2/select-one :model/Dashboard :id dashboard-id) searched-param-id merged-id-params prefix))
+          (catch Throwable e
+            (throw (ex-info (.getMessage e)
+                            {:merged-id-params merged-id-params}
+                            e)))))
+      (catch Throwable e
+        (let [e (ex-info (.getMessage e)
+                         {:dashboard-id        dashboard-id
+                          :dashboard-params    parameters
+                          :allowed-param-slugs embedding-params
+                          :slug->id            slug->id
+                          :id->slug            id->slug
+                          :param-id            searched-param-id
+                          :param-slug          searched-param-slug
+                          :token-params        slug-token-params}
+                         e)]
+          (log/errorf e "Chain filter error\n%s" (u/pprint-to-str (u/all-ex-data e)))
+          (throw e))))))
diff --git a/src/metabase/api/preview_embed.clj b/src/metabase/api/preview_embed.clj
index be5a6456210..90d58fa6aad 100644
--- a/src/metabase/api/preview_embed.clj
+++ b/src/metabase/api/preview_embed.clj
@@ -12,7 +12,7 @@
    [compojure.core :refer [GET]]
    [metabase.api.common :as api]
    [metabase.api.common.validation :as validation]
-   [metabase.api.embed :as api.embed]
+   [metabase.api.embed.common :as api.embed.common]
    [metabase.query-processor.pivot :as qp.pivot]
    [metabase.util.embed :as embed]
    [metabase.util.malli.schema :as ms]))
@@ -27,7 +27,7 @@
   [token]
   {token ms/NonBlankString}
   (let [unsigned-token (check-and-unsign token)]
-    (api.embed/card-for-unsigned-token unsigned-token
+    (api.embed.common/card-for-unsigned-token unsigned-token
       :embedding-params (embed/get-in-unsigned-token-or-throw unsigned-token [:_embedding_params]))))
 
 (def ^:private max-results
@@ -40,7 +40,7 @@
   {token ms/NonBlankString}
   (let [unsigned-token (check-and-unsign token)
         card-id        (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (api.embed/process-query-for-card-with-params
+    (api.embed.common/process-query-for-card-with-params
       :export-format    :api
       :card-id          card-id
       :token-params     (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
@@ -53,9 +53,14 @@
   [token]
   {token ms/NonBlankString}
   (let [unsigned-token (check-and-unsign token)]
-    (api.embed/dashboard-for-unsigned-token unsigned-token
+    (api.embed.common/dashboard-for-unsigned-token unsigned-token
       :embedding-params (embed/get-in-unsigned-token-or-throw unsigned-token [:_embedding_params]))))
 
+(api/defendpoint GET "/dashboard/:token/params/:param-key/values"
+  "Embedded version of chain filter values endpoint."
+  [token param-key :as {:keys [query-params]}]
+  (api.embed.common/dashboard-param-values token param-key nil query-params))
+
 (api/defendpoint GET "/dashboard/:token/dashcard/:dashcard-id/card/:card-id"
   "Fetch the results of running a Card belonging to a Dashboard you're considering embedding with JWT `token`."
   [token dashcard-id card-id & query-params]
@@ -66,14 +71,14 @@
         dashboard-id     (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
         embedding-params (embed/get-in-unsigned-token-or-throw unsigned-token [:_embedding_params])
         token-params     (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
-    (api.embed/process-query-for-dashcard
-      :export-format    :api
-      :dashboard-id     dashboard-id
-      :dashcard-id      dashcard-id
-      :card-id          card-id
-      :embedding-params embedding-params
-      :token-params     token-params
-      :query-params     query-params)))
+    (api.embed.common/process-query-for-dashcard
+     :export-format    :api
+     :dashboard-id     dashboard-id
+     :dashcard-id      dashcard-id
+     :card-id          card-id
+     :embedding-params embedding-params
+     :token-params     token-params
+     :query-params     query-params)))
 
 (api/defendpoint GET "/pivot/card/:token/query"
   "Fetch the query results for a Card you're considering embedding by passing a JWT `token`."
@@ -81,7 +86,7 @@
   {token ms/NonBlankString}
   (let [unsigned-token (check-and-unsign token)
         card-id        (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :question])]
-    (api.embed/process-query-for-card-with-params
+    (api.embed.common/process-query-for-card-with-params
       :export-format    :api
       :card-id          card-id
       :token-params     (embed/get-in-unsigned-token-or-throw unsigned-token [:params])
@@ -99,7 +104,7 @@
         dashboard-id     (embed/get-in-unsigned-token-or-throw unsigned-token [:resource :dashboard])
         embedding-params (embed/get-in-unsigned-token-or-throw unsigned-token [:_embedding_params])
         token-params     (embed/get-in-unsigned-token-or-throw unsigned-token [:params])]
-    (api.embed/process-query-for-dashcard
+    (api.embed.common/process-query-for-dashcard
       :export-format    :api
       :dashboard-id     dashboard-id
       :dashcard-id      dashcard-id
diff --git a/test/metabase/api/embed_test.clj b/test/metabase/api/embed_test.clj
index 97f1b2a6997..9967ce5b5c6 100644
--- a/test/metabase/api/embed_test.clj
+++ b/test/metabase/api/embed_test.clj
@@ -11,7 +11,7 @@
    [dk.ative.docjure.spreadsheet :as spreadsheet]
    [metabase.api.card-test :as api.card-test]
    [metabase.api.dashboard-test :as api.dashboard-test]
-   [metabase.api.embed :as api.embed]
+   [metabase.api.embed.common :as api.embed.common]
    [metabase.api.pivots :as api.pivots]
    [metabase.api.public-test :as public-test]
    [metabase.config :as config]
@@ -940,7 +940,7 @@
   (testing (str "parameters that are not in the `embedding-params` map at all should get removed by "
                 "`remove-locked-and-disabled-params`")
     (is (= {:parameters []}
-           (#'api.embed/remove-locked-and-disabled-params {:parameters {:slug "foo"}} {})))))
+           (#'api.embed.common/remove-locked-and-disabled-params {:parameters {:slug "foo"}} {})))))
 
 
 (deftest make-sure-that-multiline-series-word-as-expected---4768-
@@ -1567,18 +1567,18 @@
 
 (deftest apply-slug->value-test
   (testing "For operator filter types treat a lone value as a one-value sequence (#20438)"
-    (is (= (#'api.embed/apply-slug->value [{:type    :string/=
-                                            :target  [:dimension [:template-tag "NAME"]]
-                                            :name    "Name"
-                                            :slug    "NAME"
-                                            :default nil}]
-                                          {:NAME ["Aaron Hand"]})
-           (#'api.embed/apply-slug->value [{:type    :string/=
-                                            :target  [:dimension [:template-tag "NAME"]]
-                                            :name    "Name"
-                                            :slug    "NAME"
-                                            :default nil}]
-                                          {:NAME "Aaron Hand"})))))
+    (is (= (#'api.embed.common/apply-slug->value [{:type    :string/=
+                                                   :target  [:dimension [:template-tag "NAME"]]
+                                                   :name    "Name"
+                                                   :slug    "NAME"
+                                                   :default nil}]
+                                                 {:NAME ["Aaron Hand"]})
+           (#'api.embed.common/apply-slug->value [{:type    :string/=
+                                                   :target  [:dimension [:template-tag "NAME"]]
+                                                   :name    "Name"
+                                                   :slug    "NAME"
+                                                   :default nil}]
+                                                 {:NAME "Aaron Hand"})))))
 
 (deftest handle-single-params-for-operator-filters-test
   (testing "Query endpoints should work with a single URL parameter for an operator filter (#20438)"
diff --git a/test/metabase/api/preview_embed_test.clj b/test/metabase/api/preview_embed_test.clj
index 330f34bd1ba..fad4b6d5bf2 100644
--- a/test/metabase/api/preview_embed_test.clj
+++ b/test/metabase/api/preview_embed_test.clj
@@ -1,6 +1,9 @@
 (ns metabase.api.preview-embed-test
   (:require
+   [buddy.sign.jwt :as jwt]
    [clojure.test :refer :all]
+   [crypto.random :as crypto-random]
+   [metabase.api.dashboard-test :as api.dashboard-test]
    [metabase.api.embed-test :as embed-test]
    [metabase.api.pivots :as api.pivots]
    [metabase.api.preview-embed :as api.preview-embed]
@@ -9,6 +12,7 @@
    [metabase.models.dashboard-card :refer [DashboardCard]]
    [metabase.test :as mt]
    [metabase.util :as u]
+   [toucan2.core :as t2]
    [toucan2.tools.with-temp :as t2.with-temp]))
 
 ;;; --------------------------------------- GET /api/preview_embed/card/:token ---------------------------------------
@@ -531,3 +535,45 @@
                 (is (= [[1]]
                        (mt/rows (mt/user-http-request :crowberto :get 202 url :name "Hudson Borer"))
                        (mt/rows (mt/user-http-request :crowberto :get 202 url :name "Hudson Borer" :name "x"))))))))))))
+
+;;; ------------------------------------------------ Chain filtering -------------------------------------------------
+
+(defn random-embedding-secret-key [] (crypto-random/hex 32))
+
+(def ^:dynamic *secret-key* nil)
+
+(defn sign [claims] (jwt/sign claims *secret-key*))
+
+(defn do-with-new-secret-key [f]
+  (binding [*secret-key* (random-embedding-secret-key)]
+    (mt/with-temporary-setting-values [embedding-secret-key *secret-key*]
+      (f))))
+
+(defmacro with-new-secret-key {:style/indent 0} [& body]
+  `(do-with-new-secret-key (fn [] ~@body)))
+
+(defmacro with-embedding-enabled-and-new-secret-key {:style/indent 0} [& body]
+  `(mt/with-temporary-setting-values [~'enable-embedding true]
+     (with-new-secret-key
+       ~@body)))
+
+(defn dash-token
+  [dash-or-id & [additional-token-params]]
+  (sign (merge {:resource {:dashboard (u/the-id dash-or-id)}
+                :params   {}}
+               additional-token-params)))
+
+(deftest params-with-static-list-test
+  (testing "embedding with parameter that has source is a static list"
+    (with-embedding-enabled-and-new-secret-key
+      (api.dashboard-test/with-chain-filter-fixtures [{:keys [dashboard]}]
+        (t2/update! Dashboard (u/the-id dashboard) {:enable_embedding true
+                                                    :embedding_params {"static_category"       "enabled"
+                                                                       "static_category_label" "enabled"}})
+        (let [signed-token (dash-token dashboard)
+              url            (format "preview_embed/dashboard/%s/params/%s/values" signed-token "_STATIC_CATEGORY_")]
+          (testing "Should work if the param we're fetching values for is enabled"
+            (testing "\nGET /api/preview-embed/dashboard/:token/params/:param-key/values"
+              (is (= {:values          [["African"] ["American"] ["Asian"]]
+                      :has_more_values false}
+                     (mt/user-http-request :rasta :get 200 url))))))))))
-- 
GitLab