From 793577c894c62ca3c315014b21510821b051d9ef Mon Sep 17 00:00:00 2001 From: Braden Shepherdson <braden@metabase.com> Date: Tue, 11 Jul 2023 09:57:58 -0400 Subject: [PATCH] Mechanism for internal use of premium features (#32240) Some premium features (and in theory even non-premium EE features) use parts of others, and the token might not have those other features enabled. This mechanism allows dynamically overriding the set of features the token is considered to have, enabling eg. `:audit-app` code to use `:serialization` internally even if the user's token doesn't support `:serialization`. ```clojure (premium-features/with-premium-feature-overiddes [:foo :bar] (has-feature? :foo)) ;=> true ``` --- .../src/metabase_enterprise/audit_db.clj | 6 ++++-- .../public_settings/premium_features.clj | 21 ++++++++++++++++++- .../public_settings/premium_features_test.clj | 20 ++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/enterprise/backend/src/metabase_enterprise/audit_db.clj b/enterprise/backend/src/metabase_enterprise/audit_db.clj index 65a8cf10ae5..b3b8bd61570 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 f109b4c94b5..022254c15cb 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 2852e325597..d0565406ac4 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 -- GitLab