Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enterprise settings #23441

Merged
merged 4 commits into from
Jun 23, 2022
Merged

Enterprise settings #23441

merged 4 commits into from
Jun 23, 2022

Conversation

dpsutton
Copy link
Contributor

@dpsutton dpsutton commented Jun 20, 2022

Fixes #23414

To correctly implement a setting into OSS code that allows for enhancements with enterprise, there is a lot of ceremony involved. We put the setting in enterprise code, we then try to require it, and put it behind a second var that can delegate either to the enterprise setting if it exists otherwise some default value or nil.

;; custom namespace metabase-enterprise.embedding.utils that only contains a single setting

(defsetting notification-link-base-url
  (deferred-tru "By default \"Site Url\" is used in notification links, but can be overridden.")
  :visibility :internal
  :getter (fn []
            (when (premium-features/hide-embed-branding?)
              (or (setting/get-value-of-type :string :notification-link-base-url)
                  (public-settings/site-url)))))

;; extra var for indirection
(def ^:private site-url*
  (or (u/ignore-exceptions
        (classloader/require 'metabase-enterprise.embedding.utils)
        (resolve 'metabase-enterprise.embedding.utils/notification-link-base-url))
      (constantly nil)))

And it was important to remember to put the (when (premium-features/hide-embed-branding?) ...) check in there.

Notably many settings related to appearance did not remember to do this check. (application-font, application-colors). These are enterprise changes that affect regular OSS code. But to do it right means the regular OSS code has to jump through hoops conditionally requiring setting namespaces, checking if the feature is enabled, etc.

I originally wanted this to have use a more enterprise related key:

(defsetting application-name
  (deferred-tru "This will replace the word \"Metabase\" wherever it appears.")
  :visibility :public
  :type       :string
  :enterprise :whitelabel
  :default    "Metabase")

But this ran into circularity issues. The enterprise token is a setting. So checking this token in the defsetting macro was circular. Could have hacked around defsetting depending on (defsetting premium-embedding-token ...) , done a delay or something but that's rather gross. So instead it just takes a predicate function:

(defsetting application-name
  (deferred-tru "This will replace the word \"Metabase\" wherever it appears.")
  :visibility :public
  :type       :string
  :enabled?   premium-features/enable-whitelabeling?
  :default    "Metabase")

and update the get function in models.setting to check for that:

(defn get
  "Fetch the value of `setting-definition-or-name`. What this means depends on the Setting's `:getter`; by default, this
  looks for first for a corresponding env var, then checks the cache, then returns the default value of the Setting,
  if any."
  [setting-definition-or-name]
  (let [{:keys [cache? getter enabled? default]} (resolve-setting setting-definition-or-name)
        disable-cache?                           (not cache?)]
    (if (or (nil? enabled?) (enabled?))
      (if (= *disable-cache* disable-cache?)
        (getter)
        (binding [*disable-cache* disable-cache?]
          (getter)))
      default)))

This does mean that all settings value checks need to go through metabase.models.setting/get rather than going to lower-level apis like get-value-of-type. If this becomes an issue we'll have to fixup the :getter to include this information. Not as fun because its inside the macro instead of in a nice helper. But shouldn't be too hard to write some fn that wraps the getter.

dpsutton added 3 commits June 20, 2022 11:10
Disabled settings will return their default value (else nil if no
default is set). This allows us to have enterprise override settings and
use them from regular OSS code without classloaders, extra vars,
remembering to check if the feature is enabled, etc.

Motivating examples are the appearance settings. We allow
`application-font` setting to change the font of the application. This
is an enterprise feature, but anyone can post to
`api/setting/application-font` and set a new value or startup as
`MB_APPLICATION_FONT=comic-sans java -jar metabase.jar` and have the
functionality.

Same thing for application colors in static viz. The calling code just
calls `(settings/application-colors)` and uses them but doesn't check if
the enterprise settings are enabled. To do this correctly, you have to
remember to implement the following onerous procedure:

A whole namespace for a setting
```clojure
(ns metabase-enterprise.embedding.utils
  (:require [metabase.models.setting :as setting :refer [defsetting]]
            [metabase.public-settings :as public-settings]
            [metabase.public-settings.premium-features :as premium-features]
            [metabase.util.i18n :refer [deferred-tru]]))

(defsetting notification-link-base-url
  (deferred-tru "By default \"Site Url\" is used in notification links, but can be overridden.")
  :visibility :internal
  :getter (fn []
            (when (premium-features/hide-embed-branding?)
              (or (setting/get-value-of-type :string :notification-link-base-url)
                  (public-settings/site-url)))))
```

And then in the calling code you have to do the procedure to
conditionally require it and put it behind a var that can handle it
being nil:

```clojure
;; we want to load this at the top level so the Setting the namespace defines gets loaded
(def ^:private site-url*
  (or (u/ignore-exceptions
        (classloader/require 'metabase-enterprise.embedding.utils)
        (resolve 'metabase-enterprise.embedding.utils/notification-link-base-url))
      (constantly nil)))

;; and then the usage
(defn- site-url
  "Return the Notification Link Base URL if set by enterprise env var, or Site URL."
  []
  (or (site-url*) (public-settings/site-url)))
```

Far nicer to just place the following into the regular public-settings
namespace:

```clojure
(defsetting notification-link-base-url
  (deferred-tru "By default \"Site Url\" is used in notification links, but can be overridden.")
  :visibility :internal
  :enabled?    premium-features/hide-embed-branding?)
```

Then no need for a custom namespace to hold this setting, no need to
have an extra var to point to the setting else a fallback value.

Note that this feature is not required on every enterprise feature we
have. We a namespace `metabase-enterprise.sso.integrations.sso-settings`
that has 24 settings in it, all of which are enterprise features. But
these features are used in our enterprise sso offerings and are directly
referenced from the enterprise features. No need for the extra var to
point to them and the flag checks happen in other parts.
Mark the following settings as requiring
premium-settings/enable-whitelabeling? (aka token check)

- application-name
- loading-message (override of "doing science")
- show-metabot (override of showing our friendly metabot)
- application-colors
- application-font
- application-logo-url
- application-favicon-url

Updates the helper functions for colors to use the setting rather than
feeling entitled to use a lower level `setting/get-value-of-type`. We
need the higher level api so it takes into account if its enabled or
not.
@dpsutton dpsutton requested a review from a team June 20, 2022 17:52
@codecov
Copy link

codecov bot commented Jun 20, 2022

Codecov Report

Merging #23441 (ebadb6e) into master (70ba4d5) will decrease coverage by 0.00%.
The diff coverage is 76.92%.

@@            Coverage Diff             @@
##           master   #23441      +/-   ##
==========================================
- Coverage   64.29%   64.29%   -0.01%     
==========================================
  Files        2635     2634       -1     
  Lines       82720    82722       +2     
  Branches    10269    10268       -1     
==========================================
+ Hits        53183    53184       +1     
- Misses      25326    25329       +3     
+ Partials     4211     4209       -2     
Flag Coverage Δ
back-end 85.48% <76.92%> (+<0.01%) ⬆️
front-end 44.94% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/metabase/util/urls.clj 82.35% <0.00%> (+0.53%) ⬆️
src/metabase/public_settings.clj 94.11% <83.33%> (+0.24%) ⬆️
...ase/parameters/components/ParameterValueWidget.jsx 4.76% <0.00%> (-0.33%) ⬇️
frontend/src/metabase/parameters/utils/fields.js 100.00% <0.00%> (ø)
...ontend/src/metabase/parameters/utils/dashboards.ts 92.40% <0.00%> (+0.09%) ⬆️
src/metabase/driver/impl.clj 70.21% <0.00%> (+1.06%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 70ba4d5...ebadb6e. Read the comment docs.

Copy link
Member

@noahmoss noahmoss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current code looks good to me, so stamping to unblock. But should we prevent writing a disabled setting as well?

I'm not sure about the answer since there are some cases where we need to write enterprise feature data in OSS, so that things make sense when an instance upgrades to enterprise (this happens with download permissions, for example). But I'm not sure if that would ever apply to simple settings.

@@ -744,7 +749,8 @@
:cache? true
:database-local :never
:user-local :never
:deprecated nil}
:deprecated nil
:enabled? nil}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option could be to make the default value for enabled? (constantly true) so that you don't need to worry about the (nil? enabled) check in the getter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to leave it as is. My thinking is that enabled? is an optional property and the default is of course settings just have values. I like that its an optional policy and we aren't adding lots of functions to most of them. If we live to regreat this we can easily add the (constantly true) and drop the nil checks.

@dpsutton dpsutton merged commit 9c4e738 into master Jun 23, 2022
@dpsutton dpsutton deleted the enterprise-settings branch June 23, 2022 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

defsetting should be aware of enterprise restrictions
2 participants