From 16599b223e61bf8244cd7694dbd8c71697a15d39 Mon Sep 17 00:00:00 2001
From: Noah Moss <32746338+noahmoss@users.noreply.github.com>
Date: Thu, 13 Jun 2024 14:13:18 -0400
Subject: [PATCH] Relax constraints on GET /api/user/:id endpoint (#44100)

---
 src/metabase/api/user.clj               | 35 ++++++++++++-------------
 test/metabase/api/user_test.clj         |  6 +----
 test/metabase/db/internal_user_test.clj |  4 +--
 3 files changed, 20 insertions(+), 25 deletions(-)

diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj
index 0625f1ffcd9..f03fd0a0995 100644
--- a/src/metabase/api/user.clj
+++ b/src/metabase/api/user.clj
@@ -19,7 +19,7 @@
    [metabase.models.login-history :refer [LoginHistory]]
    [metabase.models.permissions-group :as perms-group]
    [metabase.models.setting :refer [defsetting]]
-   [metabase.models.user :as user :refer [User]]
+   [metabase.models.user :as user]
    [metabase.plugins.classloader :as classloader]
    [metabase.public-settings :as public-settings]
    [metabase.public-settings.premium-features :as premium-features]
@@ -64,7 +64,7 @@
            [400 (tru "Not able to modify the internal user")]))
 
 (defn- fetch-user [& query-criteria]
-  (apply t2/select-one (vec (cons User user/admin-or-self-visible-columns)) :type :personal query-criteria))
+  (apply t2/select-one (vec (cons :model/User user/admin-or-self-visible-columns)) query-criteria))
 
 (defn- maybe-set-user-permissions-groups! [user-or-id new-groups-or-ids]
   (when (and new-groups-or-ids
@@ -198,7 +198,7 @@
         group-id-clause     (when group_id [group_id])
         clauses             (user-clauses status query group-id-clause include_deactivated)]
     {:data (cond-> (t2/select
-                    (vec (cons User (user-visible-columns)))
+                    (vec (cons :model/User (user-visible-columns)))
                     (cond-> clauses
                       (and (some? group_id) group-id-clause) (sql.helpers/order-by [:core_user.is_superuser :desc] [:is_group_manager :desc])
                       true             (sql.helpers/order-by [:%lower.first_name :asc]
@@ -247,7 +247,7 @@
   ;; defining these functions so the branching logic below can be as clear as possible
   (letfn [(all [] (let [clauses (-> (user-clauses nil nil nil nil)
                                     (sql.helpers/order-by [:%lower.last_name :asc] [:%lower.first_name :asc]))]
-                    {:data   (t2/select (vec (cons User (user-visible-columns))) clauses)
+                    {:data   (t2/select (vec (cons :model/User (user-visible-columns))) clauses)
                      :total  (t2/count :model/User (filter-clauses-without-paging clauses))
                      :limit  mw.offset-paging/*limit*
                      :offset mw.offset-paging/*offset*}))
@@ -255,7 +255,7 @@
                                  clauses  (cond-> (user-clauses nil nil nil nil)
                                             (seq user-ids) (sql.helpers/where [:in :core_user.id user-ids])
                                             true           (sql.helpers/order-by [:%lower.last_name :asc] [:%lower.first_name :asc]))]
-                             {:data   (t2/select (vec (cons User (user-visible-columns))) clauses)
+                             {:data   (t2/select (vec (cons :model/User (user-visible-columns))) clauses)
                               :total  (t2/count :model/User (filter-clauses-without-paging clauses))
                               :limit  mw.offset-paging/*limit*
                               :offset mw.offset-paging/*offset*}))
@@ -292,7 +292,7 @@
   "Adds `sso_source` key to the `User`, so FE could determine if the user is logged in via SSO."
   [{:keys [id] :as user}]
   (if (premium-features/enable-any-sso?)
-    (assoc user :sso_source (t2/select-one-fn :sso_source User :id id))
+    (assoc user :sso_source (t2/select-one-fn :sso_source :model/User :id id))
     user))
 
 (defn- add-has-question-and-dashboard
@@ -345,8 +345,7 @@
    (check-self-or-superuser id)
    (catch clojure.lang.ExceptionInfo _e
      (validation/check-group-manager)))
-  (check-not-internal-user id)
-  (-> (api/check-404 (fetch-user :id id, :is_active true))
+  (-> (api/check-404 (fetch-user :id id))
       (t2/hydrate :user_group_memberships)))
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -362,7 +361,7 @@
    user_group_memberships [:maybe [:sequential user/UserGroupMembership]]
    login_attributes       [:maybe user/LoginAttributes]}
   (api/check-superuser)
-  (api/checkp (not (t2/exists? User :%lower.email (u/lower-case-en email)))
+  (api/checkp (not (t2/exists? :model/User :%lower.email (u/lower-case-en email)))
     "email" (tru "Email address already in use."))
   (t2/with-transaction [_conn]
     (let [new-user-id (u/the-id (user/create-and-invite-user!
@@ -434,7 +433,7 @@
       (api/checkp (valid-name-update? user-before-update :last_name last_name)
         "last_name" (tru "Editing last name is not allowed for SSO users.")))
     ;; can't change email if it's already taken BY ANOTHER ACCOUNT
-    (api/checkp (not (t2/exists? User, :%lower.email (if email (u/lower-case-en email) email), :id [:not= id]))
+    (api/checkp (not (t2/exists? :model/User, :%lower.email (if email (u/lower-case-en email) email), :id [:not= id]))
       "email" (tru "Email address already associated to another user."))
     (t2/with-transaction [_conn]
       ;; only superuser or self can update user info
@@ -447,8 +446,8 @@
                                          api/*is-superuser?* (conj :login_attributes))
                               :non-nil (cond-> #{:email}
                                          api/*is-superuser?* (conj :is_superuser))))]
-          (t2/update! User id changes)
-          (events/publish-event! :event/user-update {:object (t2/select-one User :id id)
+          (t2/update! :model/User id changes)
+          (events/publish-event! :event/user-update {:object (t2/select-one :model/User :id id)
                                                      :previous-object user-before-update
                                                      :user-id api/*current-user-id*}))
         (maybe-update-user-personal-collection-name! user-before-update body))
@@ -461,7 +460,7 @@
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
 (defn- reactivate-user! [existing-user]
-  (t2/update! User (u/the-id existing-user)
+  (t2/update! :model/User (u/the-id existing-user)
               {:is_active     true
                :is_superuser  false
                ;; if the user orignally logged in via Google Auth/LDAP and it's no longer enabled, convert them into a regular user
@@ -499,7 +498,7 @@
   {id       ms/PositiveInt
    password ms/ValidPassword}
   (check-self-or-superuser id)
-  (api/let-404 [user (t2/select-one [User :id :last_login :password_salt :password],
+  (api/let-404 [user (t2/select-one [:model/User :id :last_login :password_salt :password],
                                     :id id,
                                     :type :personal,
                                     :is_active true)]
@@ -529,8 +528,8 @@
   ;; don't technically need to because the internal user is already 'deleted' (deactivated), but keeps the warnings consistent
   (check-not-internal-user id)
   (api/check-500
-   (when (pos? (t2/update! User id {:type :personal} {:is_active false}))
-     (events/publish-event! :event/user-deactivated {:object (t2/select-one User :id id) :user-id api/*current-user-id*})))
+   (when (pos? (t2/update! :model/User id {:type :personal} {:is_active false}))
+     (events/publish-event! :event/user-deactivated {:object (t2/select-one :model/User :id id) :user-id api/*current-user-id*})))
   {:success true})
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -550,7 +549,7 @@
               (throw (ex-info (tru "Unrecognized modal: {0}" modal)
                               {:modal modal
                                :allowable-modals #{"qbnewb" "datasetnewb"}})))]
-    (api/check-500 (pos? (t2/update! User id {:type :personal} {k false}))))
+    (api/check-500 (pos? (t2/update! :model/User id {:type :personal} {k false}))))
   {:success true})
 
 (api/defendpoint POST "/:id/send_invite"
@@ -559,7 +558,7 @@
   {id ms/PositiveInt}
   (api/check-superuser)
   (check-not-internal-user id)
-  (when-let [user (t2/select-one User :id id, :is_active true, :type :personal)]
+  (when-let [user (t2/select-one :model/User :id id, :is_active true, :type :personal)]
     (let [reset-token (user/set-password-reset-token! id)
           ;; NOTE: the new user join url is just a password reset with an indicator that this is a first time user
           join-url    (str (user/form-password-reset-url reset-token) "#new")]
diff --git a/test/metabase/api/user_test.clj b/test/metabase/api/user_test.clj
index 0f5093fdba0..74c5343f53d 100644
--- a/test/metabase/api/user_test.clj
+++ b/test/metabase/api/user_test.clj
@@ -515,11 +515,7 @@
                      (dissoc :is_qbnewb :last_login))
                  (-> resp
                      mt/boolean-ids-and-timestamps
-                     (dissoc :is_qbnewb :last_login :user_group_memberships))))))
-
-      (testing "We should get a 404 when trying to access a disabled account"
-        (is (= "Not found."
-               (mt/user-http-request :crowberto :get 404 (str "user/" (mt/user->id :trashbird)))))))))
+                     (dissoc :is_qbnewb :last_login :user_group_memberships)))))))))
 
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
diff --git a/test/metabase/db/internal_user_test.clj b/test/metabase/db/internal_user_test.clj
index 8c61d508b65..0cc926a9bd6 100644
--- a/test/metabase/db/internal_user_test.clj
+++ b/test/metabase/db/internal_user_test.clj
@@ -25,9 +25,9 @@
         (is (not-any? (comp #{internal-user-email} :email) data)))
       (testing "does not count the internal user"
         (is (= total (count data))))))
+
   (testing "User Endpoints with :id"
-    (doseq [[method endpoint status-code] [[:get "user/:id" 400]
-                                           [:put "user/:id" 400]
+    (doseq [[method endpoint status-code] [[:put "user/:id" 400]
                                            [:put "user/:id/reactivate" 400]
                                            [:delete "user/:id" 400]
                                            [:put "user/:id/modal/qbnewb" 400]
-- 
GitLab