diff --git a/enterprise/backend/test/metabase_enterprise/sandbox/api/permissions_test.clj b/enterprise/backend/test/metabase_enterprise/sandbox/api/permissions_test.clj
index ac1a7c5c19207b1ab933ee8e145e743e8f9e5641..1b5e25ed23f06c631bf31ea062bcf94e0c9152f5 100644
--- a/enterprise/backend/test/metabase_enterprise/sandbox/api/permissions_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/sandbox/api/permissions_test.clj
@@ -1,8 +1,13 @@
 (ns metabase-enterprise.sandbox.api.permissions-test
-  (:require [clojure.test :refer :all]
+  (:require [cheshire.core :as json]
+            [clojure.string :as str]
+            [clojure.test :refer :all]
             [metabase-enterprise.sandbox.models.group-table-access-policy :refer [GroupTableAccessPolicy]]
-            [metabase.models :refer [Database PermissionsGroup Table]]
+            [metabase.models :refer [Card Database PermissionsGroup
+                                     PersistedInfo Table]]
             [metabase.models.permissions-group :as perms-group]
+            [metabase.models.persisted-info :as persisted-info]
+            [metabase.query-processor :as qp]
             [metabase.test :as mt]
             [metabase.util :as u]
             [metabase.util.schema :as su]
@@ -128,3 +133,48 @@
             (mt/user-http-request :crowberto :put 200 "permissions/graph" graph')
             (testing "GTAP should not have been deleted"
               (is (db/exists? GroupTableAccessPolicy :group_id (u/the-id (perms-group/all-users)), :table_id (mt/id :venues))))))))))
+
+(defn- fake-persist-card! [card]
+  (let [persisted-info (persisted-info/make-ready! (mt/user->id :rasta) card)]
+    (db/update-where! PersistedInfo {:card_id (u/the-id card)}
+                      :definition (json/encode
+                                    (persisted-info/metadata->definition
+                                      (:result_metadata card)
+                                      (:table_name persisted-info)))
+                      :active true
+                      :state "persisted"
+                      :query_hash (persisted-info/query-hash (:dataset_query card)))))
+
+(deftest persistence-and-permissions
+  (mt/with-model-cleanup [PersistedInfo]
+    (testing "Queries from cache if not sandboxed"
+      (mt/with-current-user
+        (mt/user->id :rasta)
+        (mt/with-temp*
+          [Card [card {:dataset_query (mt/mbql-query venues)
+                       :dataset true
+                       :database_id (mt/id)}]]
+          (fake-persist-card! card)
+          (is (str/includes?
+                (:query (qp/compile
+
+                          {:database (mt/id)
+                           :query {:source-table (str "card__" (u/the-id card))}
+                           :type :query}))
+                "metabase_cache")))))
+    (testing "Queries from source if sandboxed"
+      (mt/with-gtaps
+        {:gtaps {:venues {:query (mt/mbql-query venues)
+                          :remappings {:cat ["variable" [:field (mt/id :venues :category_id) nil]]}}}
+         :attributes {"cat" 50}}
+        (mt/with-temp*
+          [Card [card {:dataset_query (mt/mbql-query venues)
+                       :dataset true
+                       :database_id (mt/id)}]]
+          (fake-persist-card! card)
+          (is (not (str/includes?
+                     (:query (qp/compile
+                               {:database (mt/id)
+                                :query {:source-table (str "card__" (u/the-id card))}
+                                :type :query}))
+                     "metabase_cache"))))))))
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index 959d22701a910659e5e889134b0b16b32f9722bd..92800efbc5517798623f6367baab84f944bfd327 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -41,6 +41,7 @@
             [metabase.query-processor.middleware.optimize-temporal-filters :as optimize-temporal-filters]
             [metabase.query-processor.middleware.parameters :as parameters]
             [metabase.query-processor.middleware.permissions :as qp.perms]
+            [metabase.query-processor.middleware.persistence :as qp.persistence]
             [metabase.query-processor.middleware.pre-alias-aggregations :as qp.pre-alias-aggregations]
             [metabase.query-processor.middleware.prevent-infinite-recursive-preprocesses :as prevent-infinite-recursive-preprocesses]
             [metabase.query-processor.middleware.process-userland-query :as process-userland-query]
@@ -92,6 +93,7 @@
    #'qp.add-source-metadata/add-source-metadata-for-source-queries
    #'upgrade-field-literals/upgrade-field-literals
    (resolve 'ee.sandbox.rows/apply-sandboxing)
+   #'qp.persistence/substitute-persisted-query
    #'qp.add-implicit-clauses/add-implicit-clauses
    #'qp.add-dimension-projections/add-remapped-columns
    #'qp.resolve-fields/resolve-fields
diff --git a/src/metabase/query_processor/middleware/fetch_source_query.clj b/src/metabase/query_processor/middleware/fetch_source_query.clj
index 146ce8c23aba7daab489f4ab29b9f5191d710396..cb849088fd568f4f177554ec311078ed301af961 100644
--- a/src/metabase/query_processor/middleware/fetch_source_query.clj
+++ b/src/metabase/query_processor/middleware/fetch_source_query.clj
@@ -47,7 +47,8 @@
    :database        mbql.s/DatabaseID
    :source-metadata [mbql.s/SourceQueryMetadata]
 
-   (s/optional-key :source-query/dataset?) s/Bool})
+   (s/optional-key :source-query/dataset?) s/Bool
+   (s/optional-key :persisted-info/native) s/Str})
 
 (def ^:private MapWithResolvedSourceQuery
   (s/constrained
@@ -121,8 +122,9 @@
                   (db/do-post-select Card)
                   (db/do-post-select PersistedInfo)
                   first)
-             (throw (ex-info (tru "Card {0} does not exist." card-id)
-                             {:card-id card-id})))
+           (throw (ex-info (tru "Card {0} does not exist." card-id)
+                           {:card-id card-id})))
+
          {{mbql-query                   :query
            database-id                  :database
            {template-tags :template-tags
@@ -131,42 +133,47 @@
           dataset?                               :dataset}
          card
 
-         [persisted? source-query]
-         (or (when (and persisted-info/*allow-persisted-substitution*
-                        (:active card)
-                        (:definition card)
-                        (:query_hash card)
-                        (= (:query_hash card) (persisted-info/query-hash (:dataset_query card)))
-                        (= (:definition card) (persisted-info/metadata->definition (:result_metadata card)
-                                                                                   (:table_name card)))
-                        (= (:state card) "persisted"))
-               (when log?
-                 (log/info (trs "Substituting cached query for card {0} from {1}.{2}"
-                                card-id
-                                (ddl.i/schema-name {:id database-id} (public-settings/site-uuid))
-                                (:table_name card))))
-               [true
-                {:native (format "select %s from %s.%s"
-                                 (str/join ", " (map :field-name (get-in card [:definition :field-definitions])))
-                                 (ddl.i/schema-name {:id database-id} (public-settings/site-uuid))
-                                 (:table_name card))}])
-             (when mbql-query
-               [false mbql-query])
-             (when native-query
-               ;; rename `:query` to `:native` because source queries have a slightly different shape
-               (let [native-query (set/rename-keys native-query {:query :native})]
-                 [false
-                  (cond-> native-query
-                    ;; trim trailing comments from SQL, but not other types of native queries
-                    (string? (:native native-query)) (update :native (partial trim-sql-query card-id))
-                    (empty? template-tags)           (dissoc :template-tags))]))
-             (throw (ex-info (tru "Missing source query in Card {0}" card-id)
-                             {:card card})))]
+         persisted? (and persisted-info/*allow-persisted-substitution*
+                         (:active card)
+                         (:definition card)
+                         (:query_hash card)
+                         (= (:query_hash card) (persisted-info/query-hash (:dataset_query card)))
+                         (= (:definition card) (persisted-info/metadata->definition (:result_metadata card)
+                                                                                    (:table_name card)))
+                         (= (:state card) "persisted"))
+
+         source-query (cond
+                        mbql-query
+                        mbql-query
+
+                        native-query
+                        ;; rename `:query` to `:native` because source queries have a slightly different shape
+                        (let [native-query (set/rename-keys native-query {:query :native})]
+                          (cond-> native-query
+                            ;; trim trailing comments from SQL, but not other types of native queries
+                            (string? (:native native-query)) (update :native (partial trim-sql-query card-id))
+                            (empty? template-tags)           (dissoc :template-tags)))
+
+                        :else
+                        (throw (ex-info (tru "Missing source query in Card {0}" card-id)
+                                        {:card card})))]
+     (when (and persisted? log?)
+       (log/info (trs "Found substitute cached query for card {0} from {1}.{2}"
+                      card-id
+                      (ddl.i/schema-name {:id database-id} (public-settings/site-uuid))
+                      (:table_name card))))
+
      ;; log the query at this point, it's useful for some purposes
      (log/debug (trs "Fetched source query from Card {0}:" card-id)
                 "\n"
                 (u/pprint-to-str 'yellow source-query))
-     (cond-> {:source-query    source-query
+
+     (cond-> {:source-query    (cond-> source-query
+                                 ;; This will be applied, if still appropriate, by the peristence middleware
+                                 persisted? (assoc :persisted-info/native (format "select %s from %s.%s"
+                                                                                  (str/join ", " (map :field-name (get-in card [:definition :field-definitions])))
+                                                                                  (ddl.i/schema-name {:id database-id} (public-settings/site-uuid))
+                                                                                  (:table_name card))))
               :database        database-id
               :source-metadata (cond-> (seq (map mbql.normalize/normalize-source-metadata result-metadata))
                                  persisted? sub-cached-field-refs)}
diff --git a/src/metabase/query_processor/middleware/persistence.clj b/src/metabase/query_processor/middleware/persistence.clj
new file mode 100644
index 0000000000000000000000000000000000000000..089eeda9e4da3f2d843a0d13d4fb72e1c8610215
--- /dev/null
+++ b/src/metabase/query_processor/middleware/persistence.clj
@@ -0,0 +1,16 @@
+(ns metabase.query-processor.middleware.persistence
+  (:require [metabase.mbql.util :as mbql.u]
+            [metabase.query-processor.middleware.permissions :as qp.perms]))
+
+(defn substitute-persisted-query
+  "Replaces source-query with the native query to the cache table.
+   `:persisted-info/native` is set in `fetch-source-query`.
+
+   If permissions are applied to the query (sandboxing) then do not use the cached query.
+   It may be be possible to use the persistence cache with sandboxing at a later date with further work."
+  [{::qp.perms/keys [perms] :as  query}]
+  (if perms
+    query
+    (mbql.u/replace query
+      (x :guard (every-pred map? :persisted-info/native))
+      {:native (:persisted-info/native x)})))