diff --git a/enterprise/backend/src/metabase_enterprise/audit_db.clj b/enterprise/backend/src/metabase_enterprise/audit_db.clj index 65a8cf10ae54e7f081cd028805e7e4e72f092a53..b3b8bd6157030560e78d3dd452dc900174ad7c86 100644 --- a/enterprise/backend/src/metabase_enterprise/audit_db.clj +++ b/enterprise/backend/src/metabase_enterprise/audit_db.clj @@ -4,7 +4,7 @@ [metabase.config :as config] [metabase.db.env :as mdb.env] [metabase.models.database :refer [Database]] - [metabase.public-settings.premium-features :refer [defenterprise]] + [metabase.public-settings.premium-features :refer [defenterprise] :as premium-features] [metabase.sync.sync-metadata :as sync-metadata] [metabase.util :as u] [metabase.util.log :as log] @@ -77,7 +77,9 @@ ;; load instance analytics content (collections/dashboards/cards/etc.) when the resource exists: (when analytics-root-dir-resource (log/info (str "Loading Analytics Content from: " analytics-root-dir-resource)) - (let [report (log/with-no-logs (serialization.cmd/v2-load analytics-root-dir-resource {}))] + ;; The EE token might not have :serialization enabled, but audit features should still be able to use it. + (let [report (premium-features/with-premium-feature-overrides [:serialization] + (log/with-no-logs (serialization.cmd/v2-load analytics-root-dir-resource {})))] (if (not-empty (:errors report)) (log/info (str "Error Loading Analytics Content: " (pr-str report))) (log/info (str "Loading Analytics Content Complete (" (count (:seen report)) ") entities synchronized."))))))) diff --git a/src/metabase/public_settings/premium_features.clj b/src/metabase/public_settings/premium_features.clj index f109b4c94b5d8d59dddd7d3042489ed07af3211c..022254c15cbbcb0d12d577d63fe8c4b4fd1816c0 100644 --- a/src/metabase/public_settings/premium_features.clj +++ b/src/metabase/public_settings/premium_features.clj @@ -236,13 +236,32 @@ [] (boolean (seq (token-features)))) +(def ^:dynamic *premium-feature-overrides* + "Dynamic var holding a set of tokens which are temporarily considered to be enabled, even if the user's token does + not have that feature. + + This allows eg. `:audit-app` functionality to use `:serialization` internally, even if the token only has + `:audit-app`. + + Don't touch this directly - prefer to use [[with-premium-feature-overrides]]." + #{}) + +(defmacro with-premium-feature-overrides + "Helper to dynamically override [[*premium-feature-overrides*]] and properly merge any existing value. + + Used like `(with-premium-feature-overrides [:serialization] (something-using-serdes ...))`." + [features & body] + `(binding [*premium-feature-overrides* (into *premium-feature-overrides* ~features)] + ~@body)) + (defn has-feature? "Does this instance's premium token have `feature`? (has-feature? :sandboxes) ; -> true (has-feature? :toucan-management) ; -> false" [feature] - (contains? (token-features) (name feature))) + (or (contains? (token-features) (name feature)) + (*premium-feature-overrides* feature))) (defn- default-premium-feature-getter [feature] (fn [] diff --git a/test/metabase/public_settings/premium_features_test.clj b/test/metabase/public_settings/premium_features_test.clj index 2852e32559725b58129b9bd48532378bf33a9412..d0565406ac46ee60f575fb524bebdecbd7354a40 100644 --- a/test/metabase/public_settings/premium_features_test.clj +++ b/test/metabase/public_settings/premium_features_test.clj @@ -125,6 +125,26 @@ (is (:valid result)) (is (contains? (set (:features result)) "test"))))))) +(deftest feature-overrides-test + (let [token (random-token)] + (mt/with-temporary-raw-setting-values [:premium-embedding-token token] + (is (and (not (premium-features/has-feature? :serialization)) + (not (premium-features/has-feature? :audit-app))) + "serialization and auditing are not enabled") + (testing "with-premium-feature-overrides works" + (premium-features/with-premium-feature-overrides [:serialization] + (is (premium-features/has-feature? :serialization)) + (is (not (premium-features/has-feature? :audit-app))) + + (testing "when nested" + (premium-features/with-premium-feature-overrides [:audit-app] + (is (premium-features/has-feature? :serialization)) + (is (premium-features/has-feature? :audit-app)))))) + + (testing "and doesn't persist outside its scope" + (is (not (premium-features/has-feature? :serialization))) + (is (not (premium-features/has-feature? :audit-app))))))) + (deftest not-found-test (mt/with-log-level :fatal ;; `partial=` here in case the Cloud API starts including extra keys... this is a "dangerous" test since changes