diff --git a/enterprise/backend/src/metabase_enterprise/audit_db.clj b/enterprise/backend/src/metabase_enterprise/audit_db.clj index 6741748329453524ac5fd9fb04a68a24aab0fb0e..2d34ba0a7ed42ece087fbb04c72ad619ac94d08f 100644 --- a/enterprise/backend/src/metabase_enterprise/audit_db.clj +++ b/enterprise/backend/src/metabase_enterprise/audit_db.clj @@ -3,7 +3,6 @@ [babashka.fs :as fs] [clojure.java.io :as io] [clojure.string :as str] - [metabase-enterprise.internal-user :as ee.internal-user] [metabase-enterprise.serialization.cmd :as serialization.cmd] [metabase.db :as mdb] [metabase.models.database :refer [Database]] @@ -251,7 +250,6 @@ (defn- maybe-load-analytics-content! [audit-db] (when analytics-dir-resource - (ee.internal-user/ensure-internal-user-exists!) (adjust-audit-db-to-source! audit-db) (ia-content->plugins (plugins/plugins-dir)) (let [[last-checksum current-checksum] (get-last-and-current-checksum)] diff --git a/enterprise/backend/src/metabase_enterprise/internal_user.clj b/enterprise/backend/src/metabase_enterprise/internal_user.clj deleted file mode 100644 index a7fdfb3207a65025cf384cda9add8cc1892f3536..0000000000000000000000000000000000000000 --- a/enterprise/backend/src/metabase_enterprise/internal_user.clj +++ /dev/null @@ -1,28 +0,0 @@ -(ns metabase-enterprise.internal-user - (:require [metabase.config :as config] - [metabase.models :refer [User]] - [metabase.util.log :as log] - [toucan2.core :as t2])) - -(defn- install-internal-user! [] - (t2/insert-returning-instances! - User - {:id config/internal-mb-user-id - :first_name "Metabase" - :last_name "Internal" - :email "internal@metabase.com" - :password (str (random-uuid)) - :is_active false - :is_superuser false - :login_attributes nil - :sso_source nil - :type :internal})) - -(defn ensure-internal-user-exists! - "Creates the internal user" - [] - (if-not (t2/exists? User :id config/internal-mb-user-id) - (do (log/info "No internal user found, creating now...") - (install-internal-user!) - ::installed) - ::no-op)) diff --git a/enterprise/backend/test/metabase_enterprise/internal_user_test.clj b/enterprise/backend/test/metabase_enterprise/internal_user_test.clj deleted file mode 100644 index da1c03c69c0bce681f746bc2d13a477568f5e5a5..0000000000000000000000000000000000000000 --- a/enterprise/backend/test/metabase_enterprise/internal_user_test.clj +++ /dev/null @@ -1,77 +0,0 @@ -(ns metabase-enterprise.internal-user-test - (:require - [clojure.string :as str] - [clojure.test :refer :all] - [metabase-enterprise.internal-user :as ee.internal-user] - [metabase.config :as config] - [metabase.models :refer [User]] - [metabase.setup :as setup] - [metabase.test :as mt] - [metabase.util :as u] - [toucan2.core :as t2])) - -(defmacro with-internal-user-restoration [& body] - `(let [original-audit-db# (t2/select-one User :id config/internal-mb-user-id)] - (try - (t2/delete! User :id config/internal-mb-user-id) - ~@body - (finally - (t2/delete! User :id config/internal-mb-user-id) - (when original-audit-db# - (#'ee.internal-user/install-internal-user!)))))) - -(deftest installation-of-internal-user-test - (with-internal-user-restoration - (is (= nil (t2/select-one User :id config/internal-mb-user-id))) - (ee.internal-user/ensure-internal-user-exists!) - (is (map? (t2/select-one User :id config/internal-mb-user-id))))) - -(deftest internal-user-installed-one-time-test - (with-internal-user-restoration - (is (= nil (t2/select-one User :id config/internal-mb-user-id))) - (ee.internal-user/ensure-internal-user-exists!) - (is (map? (t2/select-one User :id config/internal-mb-user-id))) - (ee.internal-user/ensure-internal-user-exists!) - (is (map? (t2/select-one User :id config/internal-mb-user-id))))) - -(deftest has-user-setup-ignores-internal-user - (with-internal-user-restoration - (if config/ee-available? - ;; ee: - (let [user-ids-minus-internal (t2/select-fn-set :id User {:where [:not= :id config/internal-mb-user-id]})] - (is (true? (setup/has-user-setup))) - (is (false? (contains? user-ids-minus-internal config/internal-mb-user-id)) - "Selecting Users with a clause to ignore internal users does not return the internal user.")) - ;; oss: - (is (= (t2/select-fn-set :id User {:where [:not= :id config/internal-mb-user-id]}) - (t2/select-fn-set :id User)) - "Ignore internal user where clause does nothing in ee mode."))) - (is (= (setup/has-user-setup) - (with-internal-user-restoration - ;; there's no internal-user in this block - (setup/has-user-setup))))) - -(deftest internal-user-is-unmodifiable-via-api-test - ;; ensure the internal user exists for these test assertions - (ee.internal-user/ensure-internal-user-exists!) - (testing "GET /api/user" - ;; since the Internal user is deactivated, we only need to check in the `:include_deactivated` `true` - (let [{:keys [data total]} (mt/user-http-request :crowberto :get 200 "user", :include_deactivated true)] - (testing "does not return the internal user" - (is (not (some #{"internal@metabase.com"} (map :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] - [:put "user/:id/reactivate" 400] - [:delete "user/:id" 400] - [:put "user/:id/modal/qbnewb" 400] - [:post "user/:id/send_invite" 400]]] - (let [endpoint (str/replace endpoint #":id" (str config/internal-mb-user-id)) - testing-details-string (str/join " " [(u/upper-case-en (name :get)) - endpoint - "does not allow modifying the internal user"])] - (testing testing-details-string - (is (= "Not able to modify the internal user" - (mt/user-http-request :crowberto method status-code endpoint)))))))) diff --git a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj index 26e3934d2594d318551bf46828fc2886074bc359..1a07b555c1da53b604254bc9cbff7994af72dbd3 100644 --- a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj +++ b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj @@ -9,9 +9,11 @@ [metabase-enterprise.serialization.v2.storage :as v2.storage] [metabase.analytics.snowplow-test :as snowplow-test] [metabase.cmd :as cmd] - [metabase.models :refer [Card Collection Dashboard DashboardCard Database User]] + [metabase.models :refer [Card Collection Dashboard DashboardCard Database + User]] [metabase.test :as mt] [metabase.test.fixtures :as fixtures] + [metabase.test.initialize.test-users :as test-users] [metabase.util :as u] [metabase.util.log :as log] [metabase.util.yaml :as yaml] @@ -66,17 +68,21 @@ (deftest blank-target-db-test (testing "Loading a dump into an empty app DB still works (#16639)" (mt/with-premium-features #{:serialization} - (let [dump-dir (ts/random-dump-dir "serdes-") - user-pre-insert-called? (atom false)] - (log/infof "Dumping to %s" dump-dir) - (cmd/dump dump-dir "--user" "crowberto@metabase.com") - (mt/with-empty-h2-app-db - (with-redefs [load/pre-insert-user (fn [user] - (reset! user-pre-insert-called? true) - (assoc user :password "test-password"))] - (cmd/load dump-dir "--mode" "update" - "--on-error" "abort") - (is (true? @user-pre-insert-called?)))))))) + (ts/with-dbs [source-db dest-db] + (let [dump-dir (ts/random-dump-dir "serdes-") + user-pre-insert-called? (atom false)] + (log/infof "Dumping to %s" dump-dir) + (ts/with-db source-db + (test-users/init!) + (ts/create! :model/Collection :name "My_Collection") + (cmd/dump dump-dir "--user" "crowberto@metabase.com")) + (ts/with-db dest-db + (with-redefs [load/pre-insert-user (fn [user] + (reset! user-pre-insert-called? true) + (assoc user :password "test-password"))] + (cmd/load dump-dir "--mode" "update" + "--on-error" "abort") + (is (true? @user-pre-insert-called?))))))))) (deftest mode-update-remove-cards-test (testing "--mode update should remove Cards in a Dashboard if they're gone from the serialized YAML (#20786)" diff --git a/enterprise/backend/test/metabase_enterprise/serialization/v2/e2e_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/v2/e2e_test.clj index 1d5f22deb664a4b6e4c73d500338e547a23b3046..024801f36e9d073c93673c925e1c27d7dbee769c 100644 --- a/enterprise/backend/test/metabase_enterprise/serialization/v2/e2e_test.clj +++ b/enterprise/backend/test/metabase_enterprise/serialization/v2/e2e_test.clj @@ -222,7 +222,7 @@ :pulse-channel-recipient (many-random-fks 40 {} {:pulse_channel_id [:pulse-channel 30] :user_id [:u 100]})})) - (is (= 100 (count (t2/select-fn-set :email 'User)))) + (is (= 101 (count (t2/select-fn-set :email 'User)))) ; +1 for the internal user (testing "extraction" (reset! extraction (serdes/with-cache (into [] (extract/extract {})))) diff --git a/resources/migrations/001_update_migrations.yaml b/resources/migrations/001_update_migrations.yaml index 58d7a515c9a7d349d30965ac065e8ddd144c8691..c7baf0a8849debeb2bac4e1ab9ae7baf53c0d4cb 100644 --- a/resources/migrations/001_update_migrations.yaml +++ b/resources/migrations/001_update_migrations.yaml @@ -6106,6 +6106,14 @@ databaseChangeLog: nullable: false defaultValue: true + - changeSet: + id: v50.2024-03-28T16:30:35 + author: calherries + comment: Create internal user + changes: + - customChange: + class: "metabase.db.custom_migrations.CreateInternalUser" + # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<< ######################################################################################################################## diff --git a/src/metabase/cmd/copy.clj b/src/metabase/cmd/copy.clj index 1d8e71eca4cedb98ca03b02baa3f7e79c9df744b..19acc2a5c83bf8a557d8a486fe528a901b5853ae 100644 --- a/src/metabase/cmd/copy.clj +++ b/src/metabase/cmd/copy.clj @@ -199,7 +199,8 @@ "Make sure [target] application DB is empty before we start copying data." [data-source] ;; check that there are no Users yet - (let [[{:keys [cnt]}] (jdbc/query {:datasource data-source} "SELECT count(*) AS cnt FROM core_user;")] + (let [[{:keys [cnt]}] (jdbc/query {:datasource data-source} ["SELECT count(*) AS cnt FROM core_user where not id = ?;" + config/internal-mb-user-id])] (assert (integer? cnt)) (when (pos? cnt) (throw (ex-info (trs "Target DB is already populated!") diff --git a/src/metabase/db/custom_migrations.clj b/src/metabase/db/custom_migrations.clj index 2f25ff887c6b8e0d4524475ad526006fba2120a2..0d78d1f4cc876c37d3c3ba82091f1351d2986b1b 100644 --- a/src/metabase/db/custom_migrations.clj +++ b/src/metabase/db/custom_migrations.clj @@ -18,6 +18,7 @@ [clojurewerkz.quartzite.scheduler :as qs] [clojurewerkz.quartzite.triggers :as triggers] [medley.core :as m] + [metabase.config :as config] [metabase.db.connection :as mdb.connection] [metabase.models.interface :as mi] [metabase.plugins.classloader :as classloader] @@ -31,7 +32,8 @@ (liquibase.change Change) (liquibase.change.custom CustomTaskChange CustomTaskRollback) (liquibase.exception ValidationErrors) - (liquibase.util BooleanUtil))) + (liquibase.util BooleanUtil) + (org.mindrot.jbcrypt BCrypt))) (set! *warn-on-reflection* true) @@ -1068,6 +1070,49 @@ :from [:revision] :where [:= :model "Card"]}))))) +(defn- hash-bcrypt + "Hashes a given plaintext password using bcrypt. Should be used to hash + passwords included in stored user credentials that are to be later verified + using `bcrypt-credential-fn`." + [password] + (BCrypt/hashpw password (BCrypt/gensalt))) + +(defn- internal-user-exists? [] + (pos? (first (vals (t2/query-one {:select [:%count.*] :from :core_user :where [:= :id config/internal-mb-user-id]}))))) + +(define-migration CreateInternalUser + ;; the internal user may have been created in a previous version for Metabase Analytics, so don't add it again if it + ;; exists already. + (when (not (internal-user-exists?)) + (let [salt (str (random-uuid)) + password (hash-bcrypt (str salt (random-uuid))) + user {;; we insert the internal user ID directly because it's + ;; deliberately high enough to not conflict with any other + :id config/internal-mb-user-id + :password_salt salt + :password password + :email "internal@metabase.com" + :first_name "Metabase" + :locale nil + :last_login nil + :is_active false + :settings nil + :type "internal" + :is_qbnewb true + :updated_at nil + :reset_triggered nil + :is_superuser false + :login_attributes nil + :reset_token nil + :last_name "Internal" + :date_joined :%now + :sso_source nil + :is_datasetnewb true}] + (t2/query {:insert-into :core_user :values [user]})) + (let [all-users-id (first (vals (t2/query-one {:select [:id] :from :permissions_group :where [:= :name "All Users"]}))) + perms-group {:user_id config/internal-mb-user-id :group_id all-users-id}] + (t2/query {:insert-into :permissions_group_membership :values [perms-group]})))) + ;; This was renamed to TruncateAuditTables, so we need to delete the old job & trigger (define-migration DeleteTruncateAuditLogTask (classloader/the-classloader) diff --git a/test/metabase/db/internal_user_test.clj b/test/metabase/db/internal_user_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..8c61d508b65ae814b1b50feb41a4685e7decc3ee --- /dev/null +++ b/test/metabase/db/internal_user_test.clj @@ -0,0 +1,41 @@ +(ns ^:mb/once metabase.db.internal-user-test + (:require + [clojure.string :as str] + [clojure.test :refer :all] + [metabase.config :as config] + [metabase.test :as mt] + [metabase.util :as u] + [toucan2.core :as t2])) + +(deftest internal-user-is-in-all-users-test + (testing "The internal user should be in the All Users group, just like any other user." + (let [memberships (-> (t2/select-one :model/User config/internal-mb-user-id) + (t2/hydrate :user_group_memberships) + :user_group_memberships + (->> (map :id))) + all-users-id (t2/select-one-pk :model/PermissionsGroup :name "All Users")] + (is [all-users-id] memberships)))) + +(deftest internal-user-is-unmodifiable-via-api-test + (testing "GET /api/user" + ;; since the Internal user is inactive, we only need to check in the `:include_deactivated` `true` + (let [{:keys [data total]} (mt/user-http-request :crowberto :get 200 "user", :include_deactivated true) + internal-user-email (t2/select-one-fn :email :model/User config/internal-mb-user-id)] + (testing "does not return the internal user" + (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] + [:put "user/:id/reactivate" 400] + [:delete "user/:id" 400] + [:put "user/:id/modal/qbnewb" 400] + [:post "user/:id/send_invite" 400]]] + (let [endpoint (str/replace endpoint #":id" (str config/internal-mb-user-id)) + testing-details-string (str/join " " [(u/upper-case-en (name method)) + endpoint + "does not allow modifying the internal user"])] + (testing testing-details-string + (is (= "Not able to modify the internal user" + (mt/user-http-request :crowberto method status-code endpoint)))))))) diff --git a/test/metabase/db/schema_migrations_test.clj b/test/metabase/db/schema_migrations_test.clj index 7f00d03639235921f0e8cecfc4bd4bdd4d1ab4ac..447ba552c5bc228a57b0ef20044ddecd270b7298 100644 --- a/test/metabase/db/schema_migrations_test.clj +++ b/test/metabase/db/schema_migrations_test.clj @@ -12,6 +12,7 @@ [clojure.java.jdbc :as jdbc] [clojure.test :refer :all] [java-time.api :as t] + [metabase.config :as config] [metabase.db :as mdb] [metabase.db.custom-migrations-test :as custom-migrations-test] [metabase.db.query :as mdb.query] @@ -1180,6 +1181,47 @@ (t2/table-name :model/DataPermissions) :db_id db-id :group_id group-id :perm_type "perms/manage-database")))))))) +(deftest create-internal-user-test + (testing "The internal user is created if it doesn't already exist" + (impl/test-migrations "v50.2024-03-28T16:30:35" [migrate!] + (let [get-users #(t2/query "SELECT * FROM core_user")] + (is (= [] (get-users))) + (migrate!) + (is (=? [{:id config/internal-mb-user-id + :first_name "Metabase" + :last_name "Internal" + :email "internal@metabase.com" + :password some? + :password_salt some? + :is_active false + :is_superuser false + :login_attributes nil + :sso_source nil + :type "internal" + :date_joined some?}] + (get-users)))))) + (testing "The internal user isn't created again if it already exists" + (impl/test-migrations "v50.2024-03-28T16:30:35" [migrate!] + (t2/insert-returning-pks! + :core_user + ;; Copied from the old `metabase-enterprise.internal-user` namespace + {:id config/internal-mb-user-id + :first_name "Metabase" + :last_name "Internal" + :email "internal@metabase.com" + :password (str (random-uuid)) + :password_salt (str (random-uuid)) + :is_active false + :is_superuser false + :login_attributes nil + :sso_source nil + :type "internal" + :date_joined :%now}) + (let [get-users #(t2/query "SELECT * FROM core_user") + users-before (get-users)] + (migrate!) + (is (= users-before (get-users))))))) + (deftest data-permissions-migration-rollback-test (testing "Data permissions are correctly rolled back from `data_permissions` to `permissions`" (impl/test-migrations ["v50.2024-01-04T13:52:51" "v50.2024-02-19T21:32:04"] [migrate!] diff --git a/test/metabase/query_processor/middleware/metrics_test.clj b/test/metabase/query_processor/middleware/metrics_test.clj index 46ce637a00ae014b3837724c296d5a7aaa247214..803936372d254e01f499f491c9029302606d95da 100644 --- a/test/metabase/query_processor/middleware/metrics_test.clj +++ b/test/metabase/query_processor/middleware/metrics_test.clj @@ -107,7 +107,6 @@ (lib/aggregate (lib/avg (lib.metadata/field mp (mt/id :products :rating)))))] (mt/with-temp [:model/Card source-metric {:dataset_query (lib.convert/->legacy-MBQL source-query) :database_id (mt/id) - :creator_id 1 :type :metric}] (let [query {:lib/type :mbql/query :database (mt/id) diff --git a/test/metabase/setup_test.clj b/test/metabase/setup_test.clj index ea2f3f7b84cb5e154d82bb616d98ff403eaaa685..4a130b32ca5dc30e13dc3cc18b3ce3b3b711b18e 100644 --- a/test/metabase/setup_test.clj +++ b/test/metabase/setup_test.clj @@ -1,23 +1,21 @@ (ns ^:mb/once metabase.setup-test (:require [clojure.test :refer :all] - [metabase.core :as mbc] + [metabase.config :as config] [metabase.db :as mdb] - [metabase.db.schema-migrations-test.impl :as schema-migrations-test.impl] [metabase.setup :as setup] [metabase.test :as mt] [toucan2.core :as t2])) -(deftest has-user-setup-test - (testing "The has-user-setup getter should return falsey for an empty instance with only an internal user" - ;; create a new completely empty database. - (schema-migrations-test.impl/with-temp-empty-app-db [_conn :h2] - ;; make sure the DB is set up - (mdb/setup-db!) - ;; install audit DB, which creates an internal user as a side effect (on EE instances)) - (mbc/ensure-audit-db-installed!) - (is (= false - (setup/has-user-setup)))))) +(deftest has-user-setup-ignores-internal-user-test + (mt/with-empty-h2-app-db + (is (t2/exists? :model/User :id config/internal-mb-user-id) + "Sense check the internal user exists") + (testing "`has-user-setup` should return false for an empty instance with only an internal user" + (is (false? (setup/has-user-setup)))) + (testing "`has-user-setup` should return true as soon as a user is created" + (mt/with-temp [:model/User _ {}] + (is (true? (setup/has-user-setup))))))) (deftest has-user-setup-cached-test (testing "The has-user-setup getter should cache truthy results since it can never become falsey"