From 0708ce0a0f7ce75d3007504fdb40969a8f5e24a2 Mon Sep 17 00:00:00 2001
From: Ngoc Khuat <qn.khuat@gmail.com>
Date: Thu, 7 Apr 2022 15:13:43 +0700
Subject: [PATCH] Fix SSO failed to sync admin group (#20991)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix failed to sync admin group

* address noah's comments and add migration script

* document for run-with-data-migration-index

* update comments

* fix name space

* adding data_migrations tests

* add docg

* make sure we don't remove admin group if sso and ldap are not configured

* fix tests for be-ee

* fix tests for oss

* misc docs updates

* remove data-migration-index

* return some newlines

* is it failling here?

* update data_migration docs

* update data_migration docs

* fix all styling comments

* make migration to run both in oss and enterprise and make sure the tests are accounted for that

* fix failed namespace checks

* Add a comment to the test

* fix per comments

* Update permissions.clj

* tweaking with the ⚠️ icon

* refactor with-temporary-raw-setting-values

* update comments

* Add extension for cert file

* address Noah's comments
---
 .../enhancements/integrations/ldap.clj        |  8 +-
 .../sso/integrations/jwt.clj                  |  5 +-
 .../sso/integrations/saml.clj                 |  3 +-
 .../sso/integrations/saml_test.clj            | 21 +----
 src/metabase/api/session.clj                  |  2 +-
 src/metabase/db/data_migrations.clj           | 55 ++++++++++--
 src/metabase/integrations/common.clj          | 16 ++--
 .../ldap/default_implementation.clj           | 29 +++---
 src/metabase/models/permissions.clj           | 10 +--
 src/metabase/models/setting.clj               | 10 ++-
 test/metabase/db/data_migrations_test.clj     | 67 +++++++++++++-
 test/metabase/integrations/common_test.clj    | 53 +++++------
 test/metabase/test.clj                        |  1 +
 test/metabase/test/util.clj                   | 90 +++++++++++++------
 test_resources/ldap.ldif                      |  1 +
 test_resources/sso/auth0-public-idp.cert      | 17 ++++
 16 files changed, 256 insertions(+), 132 deletions(-)
 create mode 100644 test_resources/sso/auth0-public-idp.cert

diff --git a/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj b/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj
index 5b79cd6f45c..11ef9a67d09 100644
--- a/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj
+++ b/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj
@@ -30,11 +30,6 @@
   :default "userPassword,dn,distinguishedName"
   :type    :csv)
 
-(defsetting ldap-sync-admin-group
-  (deferred-tru "Sync the admin group?")
-  :type    :boolean
-  :default false)
-
 (defsetting ldap-group-membership-filter
   (deferred-tru "Group membership lookup filter. The placeholders '{dn}' and '{uid}' will be replaced by the user''s Distinguished Name and UID, respectively.")
   :default "(member={dn})")
@@ -92,8 +87,7 @@
               all-mapped-group-ids (default-impl/all-mapped-group-ids settings)]
           (integrations.common/sync-group-memberships! user
                                                        group-ids
-                                                       all-mapped-group-ids
-                                                       (ldap-sync-admin-group)))))))
+                                                       all-mapped-group-ids))))))
 
 (def ^:private impl
   (reify
diff --git a/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj b/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj
index fc946091558..198c1391306 100644
--- a/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj
+++ b/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj
@@ -62,11 +62,10 @@
   [user jwt-data]
   (when (sso-settings/jwt-group-sync)
     (when-let [groups-attribute (jwt-attribute-groups)]
-      (when-let [group-names (get jwt-data (jwt-attribute-groups))]
+      (when-let [group-names (get jwt-data groups-attribute)]
         (integrations.common/sync-group-memberships! user
                                                      (group-names->ids group-names)
-                                                     (all-mapped-group-ids)
-                                                     false)))))
+                                                     (all-mapped-group-ids))))))
 
 (defn- login-jwt-user
   [jwt {{redirect :return_to} :params, :as request}]
diff --git a/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj b/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj
index 89d22dc27f5..83a75f3585d 100644
--- a/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj
+++ b/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj
@@ -60,8 +60,7 @@
     (when group-names
       (integrations.common/sync-group-memberships! user
                                                    (group-names->ids group-names)
-                                                   (all-mapped-group-ids)
-                                                   false))))
+                                                   (all-mapped-group-ids)))))
 
 (s/defn ^:private fetch-or-create-user! :- (s/maybe {:id UUID, s/Keyword s/Any})
   "Returns a Session for the given `email`. Will create the user if needed."
diff --git a/enterprise/backend/test/metabase_enterprise/sso/integrations/saml_test.clj b/enterprise/backend/test/metabase_enterprise/sso/integrations/saml_test.clj
index b03efd7aef9..fee0665aa0d 100644
--- a/enterprise/backend/test/metabase_enterprise/sso/integrations/saml_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/sso/integrations/saml_test.clj
@@ -59,26 +59,7 @@
 (def ^:private default-idp-uri            "http://test.idp.metabase.com")
 (def ^:private default-redirect-uri       "http://localhost:3000/test")
 (def ^:private default-idp-uri-with-param (str default-idp-uri "?someparam=true"))
-
-(def ^:private default-idp-cert
-  "Public certificate from Auth0, used to validate mock SAML responses from Auth0"
-  "MIIDEzCCAfugAwIBAgIJYpjQiNMYxf1GMA0GCSqGSIb3DQEBCwUAMCcxJTAjBgNV
-BAMTHHNhbWwtbWV0YWJhc2UtdGVzdC5hdXRoMC5jb20wHhcNMTgwNTI5MjEwMDIz
-WhcNMzIwMjA1MjEwMDIzWjAnMSUwIwYDVQQDExxzYW1sLW1ldGFiYXNlLXRlc3Qu
-YXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzNcrpju4
-sILZQNe1adwg3beXtAMFGB+Buuc414+FDv2OG7X7b9OSYar/nsYfWwiazZRxEGri
-agd0Sj5mJ4Qqx+zmB/r4UgX3q/KgocRLlShvvz5gTD99hR7LonDPSWET1E9PD4XE
-1fRaq+BwftFBl45pKTcCR9QrUAFZJ2R/3g06NPZdhe4bg/lTssY5emCxaZpQEku/
-v+zzpV2nLF4by0vSj7AHsubrsLgsCfV3JvJyTxCyo1aIOlv4Vrx7h9rOgl9eEmoU
-5XJAl3D7DuvSTEOy7MyDnKF17m7l5nOPZCVOSzmCWvxSCyysijgsM5DSgAE8DPJy
-oYezV3gTX2OO2QIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSp
-B3lvrtbSDuXkB6fhbjeUpFmL2DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQEL
-BQADggEBAAEHGIAhR5GPD2JxgLtpNtZMCYiAM4Gr7hoTQMaKiXgVtdQu4iMFfbpE
-wIr6UVaDU2HKhvSRFIilOjRGmCGrIzvJgR2l+RL1Z3KrZypI1AXKJT5pF5g5FitB
-sZq+kiUpdRILl2hICzw9Q1M2Le+JSUcHcbHTVgF24xuzOZonxeE56Oc26Ju4CorL
-pM3Nb5iYaGOlQ+48/GP82cLxlVyi02va8tp7KP03ePSaZeBEKGpFtBtEN/dC3NKO
-1mmrT9284H0tvete6KLUH+dsS6bDEYGHZM5KGoSLWRr3qYlCB3AmAw+KvuiuSczL
-g9oYBkdxlhK9zZvkjCgaLCen+0aY67A=")
+(def ^:private default-idp-cert           (slurp "test_resources/sso/auth0-public-idp.cert"))
 
 (defn- do-with-some-validators-disabled
   "The sample responses all have `InResponseTo=\"_1\"` and invalid assertion signatures (they were edited by hand) so
diff --git a/src/metabase/api/session.clj b/src/metabase/api/session.clj
index f2ead741ae8..7b62de0dabc 100644
--- a/src/metabase/api/session.clj
+++ b/src/metabase/api/session.clj
@@ -104,7 +104,7 @@
         ;; password is ok, return new session if user is not deactivated
         (let [user (ldap/fetch-or-create-user! user-info)]
           (if (:is_active user)
-            (create-session! :sso (ldap/fetch-or-create-user! user-info) device-info)
+            (create-session! :sso user device-info)
             (throw (ex-info (str disabled-account-message)
                             {:status-code 401
                              :errors      {:_error disabled-account-snippet}})))))
diff --git a/src/metabase/db/data_migrations.clj b/src/metabase/db/data_migrations.clj
index 4c0943a21ec..014d448adb4 100644
--- a/src/metabase/db/data_migrations.clj
+++ b/src/metabase/db/data_migrations.clj
@@ -1,9 +1,10 @@
 (ns ^:deprecated metabase.db.data-migrations
   "Clojure-land data migration definitions and fns for running them.
-  These migrations are all ran once when Metabase is first launched, except when transferring data from an existing
-  H2 database.  When data is transferred from an H2 database, migrations will already have been run against that data;
-  thus, all of these migrations need to be repeatable, e.g.:
+  Data migrations are run once when Metabase is first launched.
+  Note that there is no locking mechanism for data-migration - thus upon launching Metabase, It's possible
+  for a migration to be run multiple times (e.g: when running multiple Metabase instances).
 
+  That said, these migrations should be idempotent, e.g:
      CREATE TABLE IF NOT EXISTS ... -- Good
      CREATE TABLE ...               -- Bad"
   (:require [cheshire.core :as json]
@@ -11,6 +12,8 @@
             [clojure.walk :as walk]
             [medley.core :as m]
             [metabase.models.dashboard-card :refer [DashboardCard]]
+            [metabase.models.permissions-group :as group]
+            [metabase.models.setting :as setting :refer [Setting]]
             [metabase.util :as u]
             [toucan.db :as db]
             [toucan.models :as models]))
@@ -32,11 +35,12 @@
     (when-not (contains? ran-migrations migration-name)
       (log/info (format "Running data migration '%s'..." migration-name))
       (try
-        (@migration-var)
-        (catch Exception e
-          (if catch?
-            (log/warn (format "Data migration %s failed: %s" migration-name (.getMessage e)))
-            (throw e))))
+       (db/transaction
+        (@migration-var))
+       (catch Exception e
+         (if catch?
+           (log/warn (format "Data migration %s failed: %s" migration-name (.getMessage e)))
+           (throw e))))
       (db/insert! DataMigrations
         :id        migration-name
         :timestamp :%now))))
@@ -172,6 +176,41 @@
                                  [:like
                                   :dashcard.visualization_settings "%\"click_link_template\":%"]]})))
 
+(defn- raw-setting
+  "Get raw setting directly from DB.
+  For some reasons during data-migration [[metabase.models.setting/get]] return the default value defined in
+  [[metabase.models.setting/defsetting]] instead of value from Setting table."
+  [k]
+  (db/select-one-field :value Setting :key (name k)))
+
+(defn- remove-admin-group-from-mappings-by-setting-key!
+  [mapping-setting-key]
+  (let [admin-group-id (:id (group/admin))
+        mapping        (try
+                        (json/parse-string (raw-setting mapping-setting-key))
+                        (catch Exception _e
+                          {}))]
+    (when-not (empty? mapping)
+      (db/update! Setting (name mapping-setting-key)
+                  :value
+                  (->> mapping
+                       (map (fn [[k v]] [k (filter #(not= admin-group-id %) v)]))
+                       (into {})
+                       json/generate-string)))))
+
+(defmigration ^{:author "qnkhuat" :added "0.43.0"} migrate-remove-admin-from-group-mapping-if-needed
+  ;;  In the past we have a setting to disable group sync for admin group when using SSO or LDAP, but it's broken and haven't really worked (see #13820)
+  ;;  In #20991 we remove this option entirely and make sync for admin group just like a regular group.
+  ;;  But on upgrade, to make sure we don't unexpectedly begin adding or removing admin users:
+  ;;  - for LDAP, if the `ldap-sync-admin-group` toggle is disabled, we remove all mapping for the admin group
+  ;;  - for SAML, JWT, we remove all mapping for admin group, because they were previously never being synced
+  (when (= (raw-setting :ldap-sync-admin-group) "false")
+    (remove-admin-group-from-mappings-by-setting-key! :ldap-group-mappings))
+  ;; sso are enterprise feature but we still run this even in OSS in case a customer
+  ;; have switched from enterprise -> SSO and stil have this mapping in Setting table
+  (remove-admin-group-from-mappings-by-setting-key! :jwt-group-mappings)
+  (remove-admin-group-from-mappings-by-setting-key! :saml-group-mappings))
+
 ;; !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 ;; !!                                                                                                               !!
 ;; !!    Please seriously consider whether any new migrations you write here could be written as Liquibase ones     !!
diff --git a/src/metabase/integrations/common.clj b/src/metabase/integrations/common.clj
index 13e65970ffc..71611f70414 100644
--- a/src/metabase/integrations/common.clj
+++ b/src/metabase/integrations/common.clj
@@ -12,21 +12,17 @@
 (defn sync-group-memberships!
   "Update the PermissionsGroups a User belongs to, adding or deleting membership entries as needed so that Users is
   only in `new-groups-or-ids`. Ignores special groups like `all-users`, and only touches groups with mappings set."
-  [user-or-id new-groups-or-ids mapped-groups-or-ids sync-admin-group?]
-  (let [included-group-ids (cond-> (set (map u/the-id mapped-groups-or-ids))
-                             sync-admin-group? (conj (u/the-id (group/admin))))
-        excluded-group-ids (cond-> #{(u/the-id (group/all-users))}
-                             (not sync-admin-group?) (conj (u/the-id (group/admin))))
+  [user-or-id new-groups-or-ids mapped-groups-or-ids]
+  (let [mapped-group-ids   (set (map u/the-id mapped-groups-or-ids))
+        excluded-group-ids #{(u/the-id (group/all-users))}
         user-id            (u/the-id user-or-id)
-        ;; Get a set of mapped Group IDs the user currently belongs to
-        current-group-ids  (when (seq included-group-ids)
+        current-group-ids  (when (seq mapped-group-ids)
                              (db/select-field :group_id PermissionsGroupMembership
                                               :user_id  user-id
-                                              ;; Add nil to included group ids to ensure valid SQL if set is empty
-                                              :group_id [:in included-group-ids]
+                                              :group_id [:in mapped-group-ids]
                                               :group_id [:not-in excluded-group-ids]))
         new-group-ids      (set/intersection (set (map u/the-id new-groups-or-ids))
-                                             included-group-ids)
+                                             mapped-group-ids)
         ;; determine what's different between current mapped groups and new mapped groups
         [to-remove to-add] (data/diff current-group-ids new-group-ids)]
     ;; remove membership from any groups as needed
diff --git a/src/metabase/integrations/ldap/default_implementation.clj b/src/metabase/integrations/ldap/default_implementation.clj
index 99be624a0c6..9636d147d71 100644
--- a/src/metabase/integrations/ldap/default_implementation.clj
+++ b/src/metabase/integrations/ldap/default_implementation.clj
@@ -48,11 +48,11 @@
 
 (s/defn ^:private user-groups :- (s/maybe [su/NonBlankString])
   "Retrieve groups for a supplied DN."
-  [ldap-connection                              :- LDAPConnectionPool
-   dn                                           :- su/NonBlankString
-   uid                                          :- su/NonBlankString
-   {:keys [group-base]}                         :- i/LDAPSettings
-   group-membership-filter                      :- su/NonBlankString]
+  [ldap-connection         :- LDAPConnectionPool
+   dn                      :- su/NonBlankString
+   uid                     :- su/NonBlankString
+   {:keys [group-base]}    :- i/LDAPSettings
+   group-membership-filter :- su/NonBlankString]
   (when group-base
     (let [results (ldap-client/search
                    ldap-connection
@@ -64,7 +64,7 @@
 (s/defn ldap-search-result->user-info :- (s/maybe i/UserInfo)
   "Convert the result "
   [ldap-connection               :- LDAPConnectionPool
-   {:keys [dn uid], :as result} :- su/Map
+   {:keys [dn uid], :as result}  :- su/Map
    {:keys [first-name-attribute
            last-name-attribute
            email-attribute
@@ -125,15 +125,16 @@
 (s/defn ^:private fetch-or-create-user!* :- (class User)
   [{:keys [first-name last-name email groups]} :- i/UserInfo
    {:keys [sync-groups?], :as settings}        :- i/LDAPSettings]
-  (let [user (db/select-one [User :id :last_login :first_name :last_name :is_active] :%lower.email (u/lower-case-en email))
+  (let [user     (db/select-one [User :id :last_login :first_name :last_name :is_active]
+                                :%lower.email (u/lower-case-en email))
         new-user (if user
                    (let [old-first-name (:first_name user)
-                         old-last-name (:last_name user)
+                         old-last-name  (:last_name user)
                          new-first-name (updated-name-part first-name old-first-name)
-                         new-last-name (updated-name-part last-name old-last-name)
-                         user-changes (merge
-                                       (when-not (= new-first-name old-first-name) {:first_name new-first-name})
-                                       (when-not (= new-last-name old-last-name) {:last_name new-last-name}))]
+                         new-last-name  (updated-name-part last-name old-last-name)
+                         user-changes   (merge
+                                          (when-not (= new-first-name old-first-name) {:first_name new-first-name})
+                                          (when-not (= new-last-name old-last-name) {:last_name new-last-name}))]
                      (if (seq user-changes)
                        (do
                          (db/update! User (:id user) user-changes)
@@ -145,9 +146,9 @@
                        (assoc :is_active true)))]
     (u/prog1 new-user
       (when sync-groups?
-        (let [group-ids   (ldap-groups->mb-group-ids groups settings)
+        (let [group-ids            (ldap-groups->mb-group-ids groups settings)
               all-mapped-group-ids (all-mapped-group-ids settings)]
-          (integrations.common/sync-group-memberships! new-user group-ids all-mapped-group-ids false))))))
+          (integrations.common/sync-group-memberships! new-user group-ids all-mapped-group-ids))))))
 
 ;;; ------------------------------------------------------ impl ------------------------------------------------------
 
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index d67a2e8a52b..b06f6881cae 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -133,13 +133,13 @@
   | Data perms? | Coll perms? | Block? | Segmented? | Can run? |
   | ----------- | ----------- | ------ | ---------- | -------- |
   |          no |          no |     no |         no |       â›” |
-  |          no |          no |     no |        yes |       âš  |
+  |          no |          no |     no |        yes |       ⚠️ |
   |          no |          no |    yes |         no |       â›” |
-  |          no |          no |    yes |        yes |       âš  |
+  |          no |          no |    yes |        yes |       ⚠️ |
   |          no |         yes |     no |         no |       ✅ |
-  |          no |         yes |     no |        yes |       âš  |
+  |          no |         yes |     no |        yes |       ⚠️ |
   |          no |         yes |    yes |         no |       â›” |
-  |          no |         yes |    yes |        yes |       âš  |
+  |          no |         yes |    yes |        yes |       ⚠️ |
   |         yes |          no |     no |         no |       ✅ |
   |         yes |          no |     no |        yes |       ✅ |
   |         yes |          no |    yes |         no |       ✅ |
@@ -149,7 +149,7 @@
   |         yes |         yes |    yes |         no |       ✅ |
   |         yes |         yes |    yes |        yes |       ✅ |
 
-  (`âš ` = runs in sandboxed mode)
+  (`⚠️` = runs in sandboxed mode)
 
   ### Known Permissions Paths
 
diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj
index dbb83d177c7..33e344d64da 100644
--- a/src/metabase/models/setting.clj
+++ b/src/metabase/models/setting.clj
@@ -123,7 +123,13 @@
   these names to avoid unintended side-effects if an application database still stores values for these settings."
   #{"-site-url"
     "enable-advanced-humanization"
-    "metabot-enabled"})
+    "metabot-enabled"
+    "ldap-sync-admin-group"})
+
+(def ^:dynamic *allow-retired-setting-names*
+  "A dynamic val that controls whether it's allowed to use retired settings.
+  Primarily used in test to disable retired setting check."
+  false)
 
 (models/defmodel Setting
   "The model that underlies [[defsetting]]."
@@ -741,7 +747,7 @@
                                setting-name (:name same-munge))
                           {:existing-setting (dissoc same-munge :on-change :getter :setter)
                            :new-setting      (dissoc <> :on-change :getter :setter)}))))
-      (when (retired-setting-names (name setting-name))
+      (when (and (retired-setting-names (name setting-name)) (not *allow-retired-setting-names*))
         (throw (ex-info (tru "Setting name ''{0}'' is retired; use a different name instead" (name setting-name))
                         {:retired-setting-name (name setting-name)
                          :new-setting          (dissoc <> :on-change :getter :setter)})))
diff --git a/test/metabase/db/data_migrations_test.clj b/test/metabase/db/data_migrations_test.clj
index f3030070e0d..5a7534fc291 100644
--- a/test/metabase/db/data_migrations_test.clj
+++ b/test/metabase/db/data_migrations_test.clj
@@ -4,11 +4,12 @@
   (:require [cheshire.core :as json]
             [clojure.test :refer :all]
             [metabase.db.data-migrations :as migrations]
-            [metabase.models.card :refer [Card]]
-            [metabase.models.dashboard :refer [Dashboard]]
-            [metabase.models.dashboard-card :refer [DashboardCard]]
+            [metabase.models :refer [Card Dashboard DashboardCard Setting]]
+            [metabase.models.permissions-group :as group]
+            [metabase.models.setting :as setting]
             [metabase.test :as mt]
             [metabase.test.fixtures :as fixtures]
+            [metabase.util :as u]
             [toucan.db :as db]))
 
 (use-fixtures :once (fixtures/initialize :db))
@@ -331,3 +332,63 @@
           (testing "And it is idempotent"
             (#'migrations/migrate-click-through)
             (is (= expected-settings (get-settings!)))))))))
+
+(defn- get-json-setting
+  [setting-k]
+  (json/parse-string (db/select-one-field :value Setting :key (name setting-k))))
+
+(defn- call-with-ldap-and-sso-configured [ldap-group-mappings sso-group-mappings f]
+  (mt/with-temporary-raw-setting-values
+    [ldap-group-mappings    (json/generate-string ldap-group-mappings)
+     saml-group-mappings    (json/generate-string sso-group-mappings)
+     jwt-group-mappings     (json/generate-string sso-group-mappings)
+     ldap-sync-admin-group  "false"
+     saml-enabled           "true"
+     ldap-enabled           "true"
+     jwt-enabled            "true"]
+    (f)))
+
+(defmacro ^:private with-ldap-and-sso-configured
+  "Run body with ldap and SSO configured, in which SSO will only be configured if enterprise is available"
+  [ldap-group-mappings sso-group-mappings & body]
+  (binding [setting/*allow-retired-setting-names* true]
+    `(call-with-ldap-and-sso-configured ~ldap-group-mappings ~sso-group-mappings (fn [] ~@body))))
+
+;; The `remove-admin-from-group-mapping-if-needed` migration is written to run in OSS version
+;; even though it might make changes to some enterprise-only settings.
+;; In order to write tests that runs in both OSS and EE, we can't use
+;; [[metabase.models.setting/get]] and [[metabase.test.util/with-temporary-setting-values]]
+;; because they require all settings are defined.
+;; That's why we use a set of helper functions that get setting directly from DB during tests
+(deftest migrate-remove-admin-from-group-mapping-if-needed-test
+  (let [admin-group-id        (u/the-id (group/admin))
+        sso-group-mappings    {"group-mapping-a" [admin-group-id (+ 1 admin-group-id)]
+                               "group-mapping-b" [admin-group-id (+ 1 admin-group-id) (+ 2 admin-group-id)]}
+        ldap-group-mappings   {"dc=metabase,dc=com" [admin-group-id (+ 1 admin-group-id)]}
+        sso-expected-mapping  {"group-mapping-a" [(+ 1 admin-group-id)]
+                               "group-mapping-b" [(+ 1 admin-group-id) (+ 2 admin-group-id)]}
+        ldap-expected-mapping {"dc=metabase,dc=com" [(+ 1 admin-group-id)]}]
+    (testing "Remove admin from group mapping for LDAP, SAML, JWT if they are enabled"
+      (with-ldap-and-sso-configured ldap-group-mappings sso-group-mappings
+        (#'migrations/migrate-remove-admin-from-group-mapping-if-needed)
+        (is (= ldap-expected-mapping (get-json-setting :ldap-group-mappings)))
+        (is (= sso-expected-mapping (get-json-setting :jwt-group-mappings)))
+        (is (= sso-expected-mapping (get-json-setting :saml-group-mappings)))))
+
+    (testing "remove admin from group mapping for LDAP, SAML, JWT even if they are disabled"
+      (with-ldap-and-sso-configured ldap-group-mappings sso-group-mappings
+        (mt/with-temporary-raw-setting-values
+          [ldap-enabled "false"
+           saml-enabled "false"
+           jwt-enabled  "false"]
+          (#'migrations/migrate-remove-admin-from-group-mapping-if-needed)
+          (is (= ldap-expected-mapping (get-json-setting :ldap-group-mappings)))
+          (is (= sso-expected-mapping (get-json-setting :jwt-group-mappings)))
+          (is (= sso-expected-mapping (get-json-setting :saml-group-mappings))))))
+
+    (testing "Don't remove admin group if `ldap-sync-admin-group` is enabled"
+      (with-ldap-and-sso-configured ldap-group-mappings sso-group-mappings
+        (mt/with-temporary-raw-setting-values
+          [ldap-sync-admin-group "true"]
+          (#'migrations/migrate-remove-admin-from-group-mapping-if-needed)
+          (is (= ldap-group-mappings (get-json-setting :ldap-group-mappings))))))))
diff --git a/test/metabase/integrations/common_test.clj b/test/metabase/integrations/common_test.clj
index 060c4d41198..5ab97c34d0d 100644
--- a/test/metabase/integrations/common_test.clj
+++ b/test/metabase/integrations/common_test.clj
@@ -5,7 +5,6 @@
             [metabase.models.permissions-group-membership :refer [PermissionsGroupMembership]]
             [metabase.test :as mt :refer [with-user-in-groups]]
             [metabase.test.fixtures :as fixtures]
-            [metabase.test.util.log :as tu.log]
             [metabase.util :as u]
             [toucan.db :as db]))
 
@@ -21,7 +20,7 @@
   (testing "does syncing group memberships leave existing memberships in place if nothing has changed?"
     (with-user-in-groups [group {:name (str ::group)}
                           user  [group]]
-      (integrations.common/sync-group-memberships! user #{group} #{group} false)
+      (integrations.common/sync-group-memberships! user #{group} #{group})
       (is (= #{"All Users" ":metabase.integrations.common-test/group"}
              (group-memberships user)))))
 
@@ -32,7 +31,7 @@
                                                       :group_id (u/the-id group)
                                                       :user_id  (u/the-id user))
             original-membership-id (membership-id)]
-        (integrations.common/sync-group-memberships! user #{group} #{group} false)
+        (integrations.common/sync-group-memberships! user #{group} #{group})
         (is (= original-membership-id
                (membership-id))))))
 
@@ -40,7 +39,7 @@
     (with-user-in-groups [group-1 {:name (str ::group-1)}
                           group-2 {:name (str ::group-2)}
                           user    [group-1]]
-      (integrations.common/sync-group-memberships! user #{group-1 group-2} #{group-1 group-2} false)
+      (integrations.common/sync-group-memberships! user #{group-1 group-2} #{group-1 group-2})
       (is (= #{":metabase.integrations.common-test/group-1"
                ":metabase.integrations.common-test/group-2"
                "All Users"}
@@ -50,7 +49,7 @@
     (with-user-in-groups [group-1 {:name (str ::group-1)}
                           group-2 {:name (str ::group-2)}
                           user    [group-1]]
-      (integrations.common/sync-group-memberships! user #{} #{group-1 group-2} false)
+      (integrations.common/sync-group-memberships! user #{} #{group-1 group-2})
       (is (= #{"All Users"}
              (group-memberships user)))))
 
@@ -58,20 +57,20 @@
     (with-user-in-groups [group-1 {:name (str ::group-1)}
                           group-2 {:name (str ::group-2)}
                           user    [group-1]]
-      (integrations.common/sync-group-memberships! user #{group-2} #{group-1 group-2} false)
+      (integrations.common/sync-group-memberships! user #{group-2} #{group-1 group-2})
       (is (= #{":metabase.integrations.common-test/group-2" "All Users"}
              (group-memberships user)))))
 
   (testing "are unmapped groups ignored when adding group memberships?"
     (with-user-in-groups [group-1 {:name (str ::group-1)}
                           user    []]
-      (integrations.common/sync-group-memberships! user #{group-1} #{} false)
+      (integrations.common/sync-group-memberships! user #{group-1} #{})
       (is (= #{"All Users"} (group-memberships user)))))
 
   (testing "are unmapped groups ignored when removing group memberships?"
     (with-user-in-groups [group-1 {:name (str ::group-1)}
                           user    [group-1]]
-      (integrations.common/sync-group-memberships! user #{} #{} false)
+      (integrations.common/sync-group-memberships! user #{} #{})
       (is (= #{":metabase.integrations.common-test/group-1"
                "All Users"}
              (group-memberships user)))))
@@ -79,34 +78,26 @@
   (testing "if we attempt to add a user to a group that doesn't exist, does the group sync complete for the other groups?"
     (with-user-in-groups [group {:name (str ::group)}
                           user    []]
-      (tu.log/suppress-output
-       (integrations.common/sync-group-memberships! user #{Integer/MAX_VALUE group} #{Integer/MAX_VALUE group} false))
+      (integrations.common/sync-group-memberships! user #{Integer/MAX_VALUE group} #{Integer/MAX_VALUE group})
       (is (= #{"All Users" ":metabase.integrations.common-test/group"}
              (group-memberships user)))))
 
   (mt/with-test-user :crowberto
-    (testing "with admin group sync enabled"
-      (testing "are admins synced?"
-        (with-user-in-groups [user]
-          (integrations.common/sync-group-memberships! user [(group/admin)] #{} true)
-          (is (= #{"Administrators" "All Users"}
-                 (group-memberships user)))))
-
-      (testing "are administrators removed appropriately?"
+    (testing "Admin should be synced just like a other groups group."
+      (testing "remove admin role if users are not mapped to it"
         (with-user-in-groups [user [(group/admin)]]
-          (integrations.common/sync-group-memberships! user [] #{} true)
+          (integrations.common/sync-group-memberships! user #{} #{(group/admin)})
           (is (= #{"All Users"}
-                 (group-memberships user)))))))
+                 (group-memberships user)))))
 
-  (testing "with admin group sync disabled"
-    (testing "are admins synced?"
-      (with-user-in-groups [user]
-        (integrations.common/sync-group-memberships! user [(group/admin)] #{} false)
-        (is (= #{"All Users"}
-               (group-memberships user)))))
+      (testing "but it's ignored if admin is not in mappings"
+        (with-user-in-groups [user [(group/admin)]]
+          (integrations.common/sync-group-memberships! user #{} #{})
+          (is (= #{"All Users" "Administrators"}
+                 (group-memberships user)))))
 
-    (testing "are administrators removed appropriately?"
-      (with-user-in-groups [user [(group/admin)]]
-        (integrations.common/sync-group-memberships! user [] #{} false)
-        (is (= #{"Administrators" "All Users"}
-               (group-memberships user)))))))
+      (testing "add admin role if the users are mapped to it"
+        (with-user-in-groups [user []]
+          (integrations.common/sync-group-memberships! user #{(group/admin)} #{(group/admin)})
+          (is (= #{"All Users" "Administrators"}
+                 (group-memberships user))))))))
diff --git a/test/metabase/test.clj b/test/metabase/test.clj
index 5ad9f9afb4e..c4f87a72c39 100644
--- a/test/metabase/test.clj
+++ b/test/metabase/test.clj
@@ -211,6 +211,7 @@
   with-temp-scheduler
   with-temp-vals-in-db
   with-temporary-setting-values
+  with-temporary-raw-setting-values
   with-user-in-groups]
 
  [tu.async
diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj
index aa04e55d9f1..3a710e9870a 100644
--- a/test/metabase/test/util.clj
+++ b/test/metabase/test/util.clj
@@ -13,7 +13,7 @@
             [metabase.driver :as driver]
             [metabase.models :refer [Card Collection Dashboard DashboardCardSeries Database Dimension Field FieldValues
                                      LoginHistory Metric NativeQuerySnippet Permissions PermissionsGroup PermissionsGroupMembership
-                                     Pulse PulseCard PulseChannel Revision Segment Table TaskHistory Timeline TimelineEvent User]]
+                                     Pulse PulseCard PulseChannel Revision Segment Setting Table TaskHistory Timeline TimelineEvent User]]
             [metabase.models.collection :as collection]
             [metabase.models.permissions :as perms]
             [metabase.models.permissions-group :as group]
@@ -342,41 +342,65 @@
       (list `with-temp-env-var-value '[a])
       (list `with-temp-env-var-value '[a b c]))))
 
+(defn- upsert-raw-setting!
+  [original-value setting-k value]
+  (if original-value
+    (db/update! Setting setting-k :value value)
+    (db/insert! Setting :key setting-k :value value))
+  (setting.cache/restore-cache!))
+
+(defn- restore-raw-setting!
+  [original-value setting-k]
+  (if original-value
+    (db/update! Setting setting-k :value original-value)
+    (db/delete! Setting :key setting-k))
+  (setting.cache/restore-cache!))
+
 (defn do-with-temporary-setting-value
   "Temporarily set the value of the Setting named by keyword `setting-k` to `value` and execute `f`, then re-establish
   the original value. This works much the same way as [[binding]].
 
   If an env var value is set for the setting, this acts as a wrapper around [[do-with-temp-env-var-value]].
 
-  Prefer the macro [[with-temporary-setting-values]] over using this function directly."
-  [setting-k value thunk]
-  (test-runner.parallel/assert-test-is-not-parallel "with-temporary-setting-values")
+  If `raw-setting?` is `true`, this works like [[with-temp*]] against the `Setting` table, but it ensures no exception
+  is thrown if the `setting-k` already exists.
+
+  Prefer the macro [[with-temporary-setting-values]] or [[with-temporary-raw-setting-values]] over using this function directly."
+  [setting-k value thunk & {:keys [raw-setting?]}]
   ;; plugins have to be initialized because changing `report-timezone` will call driver methods
   (initialize/initialize-if-needed! :db :plugins)
-  (let [setting       (#'setting/resolve-setting setting-k)
-        env-var-value (#'setting/env-var-value setting)]
-    (if env-var-value
+  (let [setting-k     (name setting-k)
+        setting       (try
+                       (#'setting/resolve-setting setting-k)
+                       (catch Exception e
+                         (when-not raw-setting?
+                           (throw e))))]
+    (if-let [env-var-value (and (not raw-setting?) (#'setting/env-var-value setting-k))]
       (do-with-temp-env-var-value setting env-var-value thunk)
-      (let [original-value (setting/get setting-k)]
+      (let [original-value (db/select-one-field :value Setting :key setting-k)]
         (try
-          (setting/set! setting-k value)
-          (testing (colorize/blue (format "\nSetting %s = %s\n" (keyword setting-k) (pr-str value)))
-            (thunk))
-          (catch Throwable e
-            (throw (ex-info (str "Error in with-temporary-setting-values: " (ex-message e))
-                            {:setting  setting-k
-                             :location (symbol (name (:namespace setting)) (name setting-k))
-                             :value    value}
-                            e)))
-          (finally
-            (try
-              (setting/set! setting-k original-value)
-              (catch Throwable e
-                (throw (ex-info (str "Error restoring original Setting value: " (ex-message e))
-                                {:setting        setting-k
-                                 :location       (symbol (name (:namespace setting)) (name setting-k))
-                                 :original-value original-value}
-                                e))))))))))
+         (if raw-setting?
+           (upsert-raw-setting! original-value setting-k value)
+           (setting/set! setting-k value))
+         (testing (colorize/blue (format "\nSetting %s = %s\n" (keyword setting-k) (pr-str value)))
+           (thunk))
+         (catch Throwable e
+           (throw (ex-info (str "Error in with-temporary-setting-values: " (ex-message e))
+                           {:setting  setting-k
+                            :location (symbol (name (:namespace setting)) (name setting-k))
+                            :value    value}
+                           e)))
+         (finally
+          (try
+           (if raw-setting?
+             (restore-raw-setting! original-value setting-k)
+             (setting/set! setting-k original-value))
+           (catch Throwable e
+             (throw (ex-info (str "Error restoring original Setting value: " (ex-message e))
+                             {:setting        setting-k
+                              :location       (symbol (name (:namespace setting)) setting-k)
+                              :original-value original-value}
+                             e))))))))))
 
 (defmacro with-temporary-setting-values
   "Temporarily bind the site-wide values of one or more `Settings`, execute body, and re-establish the original values.
@@ -389,6 +413,7 @@
   To temporarily override the value of *read-only* env vars, use [[with-temp-env-var-value]]."
   [[setting-k value & more :as bindings] & body]
   (assert (even? (count bindings)) "mismatched setting/value pairs: is each setting name followed by a value?")
+  (test-runner.parallel/assert-test-is-not-parallel "with-temporary-setting-vales")
   (if (empty? bindings)
     `(do ~@body)
     `(do-with-temporary-setting-value ~(keyword setting-k) ~value
@@ -396,6 +421,19 @@
          (with-temporary-setting-values ~more
            ~@body)))))
 
+(defmacro with-temporary-raw-setting-values
+  "Like `with-temporary-setting-values` but works with raw value and it allows settings that are not defined using `defsetting`."
+  [[setting-k value & more :as bindings] & body]
+  (assert (even? (count bindings)) "mismatched setting/value pairs: is each setting name followed by a value?")
+  (test-runner.parallel/assert-test-is-not-parallel "with-temporary-raw-setting-values")
+  (if (empty? bindings)
+    `(do ~@body)
+    `(do-with-temporary-setting-value ~(keyword setting-k) ~value
+       (fn []
+         (with-temporary-raw-setting-values ~more
+           ~@body))
+       :raw-setting? true)))
+
 (defn do-with-discarded-setting-changes [settings thunk]
   (initialize/initialize-if-needed! :db :plugins)
   ((reduce
diff --git a/test_resources/ldap.ldif b/test_resources/ldap.ldif
index 7f176ad995e..843c847be0a 100644
--- a/test_resources/ldap.ldif
+++ b/test_resources/ldap.ldif
@@ -40,6 +40,7 @@ objectClass: person
 objectClass: organizationalPerson
 objectClass: inetOrgPerson
 cn: Fred Taylor
+# intetionally having some attributes not properly cased to test case-insensitive when querying users
 sN: Taylor
 givenname: Fred
 uid: ftaylor300
diff --git a/test_resources/sso/auth0-public-idp.cert b/test_resources/sso/auth0-public-idp.cert
new file mode 100644
index 00000000000..67f541db5a8
--- /dev/null
+++ b/test_resources/sso/auth0-public-idp.cert
@@ -0,0 +1,17 @@
+MIIDEzCCAfugAwIBAgIJYpjQiNMYxf1GMA0GCSqGSIb3DQEBCwUAMCcxJTAjBgNV
+BAMTHHNhbWwtbWV0YWJhc2UtdGVzdC5hdXRoMC5jb20wHhcNMTgwNTI5MjEwMDIz
+WhcNMzIwMjA1MjEwMDIzWjAnMSUwIwYDVQQDExxzYW1sLW1ldGFiYXNlLXRlc3Qu
+YXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzNcrpju4
+sILZQNe1adwg3beXtAMFGB+Buuc414+FDv2OG7X7b9OSYar/nsYfWwiazZRxEGri
+agd0Sj5mJ4Qqx+zmB/r4UgX3q/KgocRLlShvvz5gTD99hR7LonDPSWET1E9PD4XE
+1fRaq+BwftFBl45pKTcCR9QrUAFZJ2R/3g06NPZdhe4bg/lTssY5emCxaZpQEku/
+v+zzpV2nLF4by0vSj7AHsubrsLgsCfV3JvJyTxCyo1aIOlv4Vrx7h9rOgl9eEmoU
+5XJAl3D7DuvSTEOy7MyDnKF17m7l5nOPZCVOSzmCWvxSCyysijgsM5DSgAE8DPJy
+oYezV3gTX2OO2QIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSp
+B3lvrtbSDuXkB6fhbjeUpFmL2DAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQEL
+BQADggEBAAEHGIAhR5GPD2JxgLtpNtZMCYiAM4Gr7hoTQMaKiXgVtdQu4iMFfbpE
+wIr6UVaDU2HKhvSRFIilOjRGmCGrIzvJgR2l+RL1Z3KrZypI1AXKJT5pF5g5FitB
+sZq+kiUpdRILl2hICzw9Q1M2Le+JSUcHcbHTVgF24xuzOZonxeE56Oc26Ju4CorL
+pM3Nb5iYaGOlQ+48/GP82cLxlVyi02va8tp7KP03ePSaZeBEKGpFtBtEN/dC3NKO
+1mmrT9284H0tvete6KLUH+dsS6bDEYGHZM5KGoSLWRr3qYlCB3AmAw+KvuiuSczL
+g9oYBkdxlhK9zZvkjCgaLCen+0aY67A="
-- 
GitLab