From d19be7f5978a5e5c93a54c8111cc584888a56965 Mon Sep 17 00:00:00 2001 From: Noah Moss <32746338+noahmoss@users.noreply.github.com> Date: Tue, 8 Jun 2021 14:22:19 -0700 Subject: [PATCH] Ignore unmapped groups during SSO group syncs (#16314) * changes to sync-group-memberships! and unit tests * update jwt and saml code * fix admin sync tests * fix misplaced docstrings * address review comment --- .../enhancements/integrations/ldap.clj | 8 +++-- .../sso/integrations/jwt.clj | 21 +++++++++-- .../sso/integrations/saml.clj | 21 +++++++++-- src/metabase/integrations/common.clj | 34 ++++++++++-------- .../ldap/default_implementation.clj | 16 ++++++--- test/metabase/integrations/common_test.clj | 36 +++++++++++++------ 6 files changed, 98 insertions(+), 38 deletions(-) diff --git a/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj b/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj index 9af9f71abc8..3838351d91d 100644 --- a/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj +++ b/enterprise/backend/src/metabase_enterprise/enhancements/integrations/ldap.clj @@ -68,8 +68,12 @@ :login_attributes attributes}))] (u/prog1 user (when sync-groups? - (let [group-ids (default-impl/ldap-groups->mb-group-ids groups settings)] - (integrations.common/sync-group-memberships! user group-ids (ldap-sync-admin-group))))))) + (let [group-ids (default-impl/ldap-groups->mb-group-ids groups settings) + 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))))))) (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 4db2f877176..b05adebff27 100644 --- a/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj +++ b/enterprise/backend/src/metabase_enterprise/sso/integrations/jwt.clj @@ -42,15 +42,30 @@ ;; used by Zendesk for their JWT SSO, so it seemed like a good place for us to start (def ^:private ^:const three-minutes-in-seconds 180) -(defn- group-names->ids [group-names] +(defn- group-names->ids + "Translate a user's group names to a set of MB group IDs using the configured mappings" + [group-names] (set (mapcat (sso-settings/jwt-group-mappings) (map keyword group-names)))) -(defn- sync-groups! [user jwt-data] +(defn- all-mapped-group-ids + "Returns the set of all MB group IDs that have configured mappings" + [] + (-> (sso-settings/jwt-group-mappings) + vals + flatten + set)) + +(defn- sync-groups! + "Sync a user's groups based on mappings configured in the JWT settings" + [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))] - (integrations.common/sync-group-memberships! user (group-names->ids group-names) false))))) + (integrations.common/sync-group-memberships! user + (group-names->ids group-names) + (all-mapped-group-ids) + false))))) (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 8ab452d0adf..d9fbd0935c2 100644 --- a/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj +++ b/enterprise/backend/src/metabase_enterprise/sso/integrations/saml.clj @@ -37,16 +37,31 @@ [schema.core :as s]) (:import java.util.UUID)) -(defn- group-names->ids [group-names] +(defn- group-names->ids + "Translate a user's group names to a set of MB group IDs using the configured mappings" + [group-names] (->> (cond-> group-names (string? group-names) vector) (map keyword) (mapcat (sso-settings/saml-group-mappings)) set)) -(defn- sync-groups! [user group-names] +(defn- all-mapped-group-ids + "Returns the set of all MB group IDs that have configured mappings" + [] + (-> (sso-settings/saml-group-mappings) + vals + flatten + set)) + +(defn- sync-groups! + "Sync a user's groups based on mappings configured in the SAML settings" + [user group-names] (when (sso-settings/saml-group-sync) (when group-names - (integrations.common/sync-group-memberships! user (group-names->ids group-names) false)))) + (integrations.common/sync-group-memberships! user + (group-names->ids group-names) + (all-mapped-group-ids) + false)))) (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/src/metabase/integrations/common.clj b/src/metabase/integrations/common.clj index e8ca1358e1d..13e65970ffc 100644 --- a/src/metabase/integrations/common.clj +++ b/src/metabase/integrations/common.clj @@ -1,6 +1,7 @@ (ns metabase.integrations.common "Shared functionality used by different integrations." (:require [clojure.data :as data] + [clojure.set :as set] [clojure.tools.logging :as log] [metabase.models.permissions-group :as group] [metabase.models.permissions-group-membership :refer [PermissionsGroupMembership]] @@ -9,21 +10,24 @@ [toucan.db :as db])) (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`." - [user-or-id new-groups-or-ids sync-admin-group?] - ;; if you AREN'T syncing the admin group, it's a "special group", which means it's excluded from being added - ;; or removed from a user - (let [special-group-ids (if (false? sync-admin-group?) - #{(u/the-id (group/admin)) (u/the-id (group/all-users))} - #{(u/the-id (group/all-users))}) + "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-id (u/the-id user-or-id) - ;; Get a set of Group IDs the user currently belongs to - current-group-ids (db/select-field :group_id PermissionsGroupMembership - :user_id user-id - :group_id [:not-in special-group-ids]) - new-group-ids (set (map u/the-id new-groups-or-ids)) - ;; determine what's different between current groups and new groups + ;; Get a set of mapped Group IDs the user currently belongs to + current-group-ids (when (seq included-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 [:not-in excluded-group-ids])) + new-group-ids (set/intersection (set (map u/the-id new-groups-or-ids)) + included-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 (when (seq to-remove) @@ -31,7 +35,7 @@ (db/delete! PermissionsGroupMembership :group_id [:in to-remove], :user_id user-id)) ;; add new memberships for any groups as needed (doseq [id to-add - :when (not (special-group-ids id))] + :when (not (excluded-group-ids id))] (log/debugf "Adding user %s to group %s" user-id id) ;; if adding membership fails for one reason or another (i.e. if the group doesn't exist) log the error add the ;; user to the other groups rather than failing entirely diff --git a/src/metabase/integrations/ldap/default_implementation.clj b/src/metabase/integrations/ldap/default_implementation.clj index 0e43088938f..5bbc4ac55e1 100644 --- a/src/metabase/integrations/ldap/default_implementation.clj +++ b/src/metabase/integrations/ldap/default_implementation.clj @@ -82,7 +82,7 @@ ;;; --------------------------------------------- fetch-or-create-user! ---------------------------------------------- (s/defn ldap-groups->mb-group-ids :- #{su/IntGreaterThanZero} - "Translate a set of DNs to a set of MB group IDs using the configured mappings." + "Translate a set of a user's group DNs to a set of MB group IDs using the configured mappings." [ldap-groups :- (s/maybe [su/NonBlankString]) {:keys [group-mappings]} :- (select-keys i/LDAPSettings [:group-mappings s/Keyword])] (-> group-mappings @@ -91,6 +91,14 @@ flatten set)) +(s/defn all-mapped-group-ids :- #{su/IntGreaterThanZero} + "Returns the set of all MB group IDs that have configured mappings." + [{:keys [group-mappings]} :- (select-keys i/LDAPSettings [:group-mappings s/Keyword])] + (-> group-mappings + vals + flatten + set)) + (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] @@ -101,9 +109,9 @@ :email email}))] (u/prog1 user (when sync-groups? - (let [group-ids (ldap-groups->mb-group-ids groups settings)] - (integrations.common/sync-group-memberships! user group-ids false)))))) - + (let [group-ids (ldap-groups->mb-group-ids groups settings) + all-mapped-group-ids (all-mapped-group-ids settings)] + (integrations.common/sync-group-memberships! user group-ids all-mapped-group-ids false)))))) ;;; ------------------------------------------------------ impl ------------------------------------------------------ diff --git a/test/metabase/integrations/common_test.clj b/test/metabase/integrations/common_test.clj index bbe9bd8ca36..4d2c8f232d3 100644 --- a/test/metabase/integrations/common_test.clj +++ b/test/metabase/integrations/common_test.clj @@ -53,7 +53,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} false) + (integrations.common/sync-group-memberships! user #{group} #{group} false) (is (= #{"All Users" ":metabase.integrations.common-test/group"} (group-memberships user))))) @@ -64,7 +64,7 @@ :group_id (u/get-id group) :user_id (u/get-id user)) original-membership-id (membership-id)] - (integrations.common/sync-group-memberships! user #{group} false) + (integrations.common/sync-group-memberships! user #{group} #{group} false) (is (= original-membership-id (membership-id)))))) @@ -72,7 +72,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} #{group-1 group-2} false) (is (= #{":metabase.integrations.common-test/group-1" ":metabase.integrations.common-test/group-2" "All Users"} @@ -82,7 +82,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 #{} false) + (integrations.common/sync-group-memberships! user #{} #{group-1 group-2} false) (is (= #{"All Users"} (group-memberships user))))) @@ -90,15 +90,29 @@ (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} false) + (integrations.common/sync-group-memberships! user #{group-2} #{group-1 group-2} false) (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) + (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) + (is (= #{":metabase.integrations.common-test/group-1" + "All Users"} + (group-memberships user))))) + (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 [group]] + user []] (tu.log/suppress-output - (integrations.common/sync-group-memberships! user [Integer/MAX_VALUE group] false)) + (integrations.common/sync-group-memberships! user #{Integer/MAX_VALUE group} #{Integer/MAX_VALUE group} false)) (is (= #{"All Users" ":metabase.integrations.common-test/group"} (group-memberships user))))) @@ -106,25 +120,25 @@ (testing "with admin group sync enabled" (testing "are admins synced?" (with-user-in-groups [user] - (integrations.common/sync-group-memberships! user [(group/admin)] true) + (integrations.common/sync-group-memberships! user [(group/admin)] #{} true) (is (= #{"Administrators" "All Users"} (group-memberships user))))) (testing "are administrators removed appropriately?" (with-user-in-groups [user [(group/admin)]] - (integrations.common/sync-group-memberships! user [] true) + (integrations.common/sync-group-memberships! user [] #{} true) (is (= #{"All Users"} (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) + (integrations.common/sync-group-memberships! user [(group/admin)] #{} false) (is (= #{"All Users"} (group-memberships user))))) (testing "are administrators removed appropriately?" (with-user-in-groups [user [(group/admin)]] - (integrations.common/sync-group-memberships! user [] false) + (integrations.common/sync-group-memberships! user [] #{} false) (is (= #{"Administrators" "All Users"} (group-memberships user))))))) -- GitLab