diff --git a/enterprise/backend/src/metabase_enterprise/audit_db.clj b/enterprise/backend/src/metabase_enterprise/audit_db.clj new file mode 100644 index 0000000000000000000000000000000000000000..a195c996eae8048a72b491da47c250fbe9a5d747 --- /dev/null +++ b/enterprise/backend/src/metabase_enterprise/audit_db.clj @@ -0,0 +1,58 @@ +(ns metabase-enterprise.audit-db + (:require [metabase.db.env :as mdb.env] + [metabase.models.database :refer [Database]] + [metabase.public-settings.premium-features :refer [defenterprise]] + [metabase.util :as u] + [metabase.util.log :as log] + [toucan2.core :as t2])) + +(def ^:private default-admin-db-id 13371337) + +(defn- install-database! + "Creates the audit db, a clone of the app db used for auditing purposes. + + - This uses a weird ID because some tests are hardcoded to look for database with ID = 2, and inserting an extra db + throws that off since the IDs are sequential... + + - In the unlikely case that a user has many many databases in Metabase, and ensure there can Never be a collision, we + do a quick check here and pick a new ID if it would have collided. Similar to finding an open port number." + ([engine] (install-database! engine default-admin-db-id)) + ([engine id] + (if (t2/select-one Database :id id) + (install-database! engine (inc id)) + (t2/insert! Database {:is_audit true + :id default-admin-db-id + :name "Audit Database" + :description "Internal Audit DB used to power metabase analytics." + :engine engine + :is_full_sync true + :is_on_demand false + :creator_id nil + :auto_run_queries true})))) + +(defn ensure-db-installed! + "Called on app startup to ensure the existance of the audit db in enterprise apps. + + The return values indicate what action was taken." + [] + (let [audit-db (t2/select-one Database :is_audit true)] + (cond + (nil? audit-db) + (u/prog1 ::installed + (log/info "Audit DB does not exist, Installing...") + (install-database! mdb.env/db-type)) + + (not= mdb.env/db-type (:engine audit-db)) + (u/prog1 ::updated + (log/infof "Updating the Audit DB engine to %s." (name mdb.env/db-type)) + (t2/update! Database :is_audit true {:engine mdb.env/db-type}) + (ensure-db-installed!)) + + :else + ::no-op))) + +(defenterprise ensure-audit-db-installed! + "EE implementation of `ensure-db-installed!`." + :feature :any + [] + (ensure-db-installed!)) diff --git a/enterprise/backend/test/metabase_enterprise/audit_db_test.clj b/enterprise/backend/test/metabase_enterprise/audit_db_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..86c3103e7817bd40ccf3d18c1d4fcf43d860e319 --- /dev/null +++ b/enterprise/backend/test/metabase_enterprise/audit_db_test.clj @@ -0,0 +1,23 @@ +(ns metabase-enterprise.audit-db-test + (:require [clojure.test :refer [deftest is]] + [metabase-enterprise.audit-db :as audit-db] + [metabase.models.database :refer [Database]] + [metabase.util.log :as log] + [toucan2.core :as t2])) + +(deftest audit-db-is-installed-then-left-alone + (let [original-audit-db (t2/select-one Database :is_audit true)] + (try + (t2/delete! Database :is_audit true) + (is (= :metabase-enterprise.audit-db/installed (audit-db/ensure-db-installed!))) + (is (= :metabase-enterprise.audit-db/no-op (audit-db/ensure-db-installed!))) + + (t2/update! Database :is_audit true {:engine "datomic"}) + (is (= :metabase-enterprise.audit-db/updated (audit-db/ensure-db-installed!))) + (is (= :metabase-enterprise.audit-db/no-op (audit-db/ensure-db-installed!))) + + (finally + (t2/delete! Database :is_audit true) + (when original-audit-db + (log/fatal (str "Original Audit DB: " (pr-str original-audit-db))) + (#'t2/insert! Database original-audit-db)))))) diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index c43b56e9f56c276d8117f2f4a0090ae7cfe8679b..0c3ead6e63c31f8fb3006b50468438d0dd8f67df 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -14591,6 +14591,22 @@ databaseChangeLog: constraints: nullable: false + - changeSet: + id: v47.00-015 + author: escherize + comment: added 0.47.0 - Add is_audit to metabase_database + changes: + - addColumn: + tableName: metabase_database + columns: + - column: + name: is_audit + type: boolean + defaultValueBoolean: false + remarks: 'Only the app db, visible to admins via auditing should have this set true.' + constraints: + nullable: false + - changeSet: id: v47.00-016 author: calherres diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj index c75119cdfab86d837a6e3db87d136ae789445265..e079fca4631f01715b88868bef2e2ea1bb84f67e 100644 --- a/src/metabase/api/database.clj +++ b/src/metabase/api/database.clj @@ -43,6 +43,7 @@ [metabase.util.honey-sql-2 :as h2x] [metabase.util.i18n :refer [deferred-tru trs tru]] [metabase.util.log :as log] + [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [metabase.util.schema :as su] [schema.core :as s] @@ -231,7 +232,8 @@ include-saved-questions-tables? include-editable-data-model? exclude-uneditable-details?]}] - (let [dbs (t2/select Database {:order-by [:%lower.name :%lower.engine]}) + (let [dbs (t2/select Database {:where [:= :is_audit false] + :order-by [:%lower.name :%lower.engine]}) filter-by-data-access? (not (or include-editable-data-model? exclude-uneditable-details?))] (cond-> (add-native-perms-info dbs) include-tables? add-tables @@ -240,14 +242,7 @@ filter-by-data-access? (#(filter mi/can-read? %)) include-saved-questions-db? (add-saved-questions-virtual-database :include-tables? include-saved-questions-tables?)))) -(def FetchAllIncludeValues - "Schema for matching the include parameter of the GET / endpoint" - (su/with-api-error-message - (s/maybe (s/eq "tables")) - (deferred-tru "include must be either empty or the value 'tables'"))) - -#_{:clj-kondo/ignore [:deprecated-var]} -(api/defendpoint-schema GET "/" +(api/defendpoint GET "/" "Fetch all `Databases`. * `include=tables` means we should hydrate the Tables belonging to each DB. Default: `false`. @@ -268,32 +263,28 @@ * `exclude_uneditable_details` will only include DBs for which the current user can edit the DB details. Has no effect unless Enterprise Edition code is available and the advanced-permissions feature is enabled." [include_tables include_cards include saved include_editable_data_model exclude_uneditable_details] - {include_tables (s/maybe su/BooleanString) - include_cards (s/maybe su/BooleanString) - include FetchAllIncludeValues - saved (s/maybe su/BooleanString) - include_editable_data_model (s/maybe su/BooleanString) - exclude_uneditable_details (s/maybe su/BooleanString)} + {include_tables [:maybe :boolean] + include_cards [:maybe :boolean] + include (mu/with-api-error-message + [:maybe [:= "tables"]] + (deferred-tru "include must be either empty or the value 'tables'")) + saved [:maybe :boolean] + include_editable_data_model [:maybe :boolean] + exclude_uneditable_details [:maybe :boolean]} (when (and config/is-dev? (or include_tables include_cards)) ;; don't need to i18n since this is dev-facing only (log/warn "GET /api/database?include_tables and ?include_cards are deprecated." "Prefer using ?include=tables and ?saved=true instead.")) - (let [include-tables? (cond - (seq include) (= include "tables") - (seq include_tables) (Boolean/parseBoolean include_tables)) - include-saved-questions-db? (cond - (seq saved) (Boolean/parseBoolean saved) - (seq include_cards) (Boolean/parseBoolean include_cards)) + (let [include-tables? (or (= include "tables") include_tables) + include-saved-questions-db? (or saved include_cards) include-saved-questions-tables? (when include-saved-questions-db? - (if (seq include_cards) - true - include-tables?)) + (if include_cards true include-tables?)) db-list-res (or (dbs-list :include-tables? include-tables? :include-saved-questions-db? include-saved-questions-db? :include-saved-questions-tables? include-saved-questions-tables? - :include-editable-data-model? (Boolean/parseBoolean include_editable_data_model) - :exclude-uneditable-details? (Boolean/parseBoolean exclude_uneditable_details)) + :include-editable-data-model? include_editable_data_model + :exclude-uneditable-details? exclude_uneditable_details) [])] {:data db-list-res :total (count db-list-res)})) @@ -759,21 +750,21 @@ ;; no error, proceed with creation. If record is inserted successfuly, publish a `:database-create` event. ;; Throw a 500 if nothing is inserted (u/prog1 (api/check-500 (first (t2/insert-returning-instances! - Database - (merge - {:name name - :engine engine - :details details-or-error - :is_full_sync is-full-sync? - :is_on_demand (boolean is_on_demand) - :cache_ttl cache_ttl - :creator_id api/*current-user-id*} - (sync.schedules/schedule-map->cron-strings - (if (:let-user-control-scheduling details) - (sync.schedules/scheduling schedules) - (sync.schedules/default-randomized-schedule))) - (when (some? auto_run_queries) - {:auto_run_queries auto_run_queries}))))) + Database + (merge + {:name name + :engine engine + :details details-or-error + :is_full_sync is-full-sync? + :is_on_demand (boolean is_on_demand) + :cache_ttl cache_ttl + :creator_id api/*current-user-id*} + (sync.schedules/schedule-map->cron-strings + (if (:let-user-control-scheduling details) + (sync.schedules/scheduling schedules) + (sync.schedules/default-randomized-schedule))) + (when (some? auto_run_queries) + {:auto_run_queries auto_run_queries}))))) (events/publish-event! :database-create <>) (snowplow/track-event! ::snowplow/database-connection-successful api/*current-user-id* diff --git a/src/metabase/core.clj b/src/metabase/core.clj index 441e2cf6cf07ed1f8ffda3a84a53803f6c854cc8..c07cee10f244bdc657280470f16de0442837feca 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -17,6 +17,7 @@ [metabase.plugins :as plugins] [metabase.plugins.classloader :as classloader] [metabase.public-settings :as public-settings] + [metabase.public-settings.premium-features :refer [defenterprise]] [metabase.sample-data :as sample-data] [metabase.server :as server] [metabase.server.handler :as handler] @@ -89,6 +90,12 @@ (prometheus/shutdown!) (log/info (trs "Metabase Shutdown COMPLETE"))) +(defenterprise ensure-audit-db-installed! + "OSS implementation of `audit-db/ensure-db-installed!`, which is an enterprise feature, so does nothing in the OSS + version." + metabase-enterprise.audit-db + []) + (defn- init!* "General application initialization function which should be run once at application startup." [] @@ -133,6 +140,7 @@ ;; otherwise update if appropriate (sample-data/update-sample-database-if-needed!)) (init-status/set-progress! 0.9)) + (ensure-audit-db-installed!) ;; start scheduler at end of init! (task/start-scheduler!) (init-status/set-complete!) diff --git a/src/metabase/db/env.clj b/src/metabase/db/env.clj index f08d521d23914881b152de8bcc8b588793a81c51..c328884f3b5e002efdbedf9cfd25ddb245f8c191 100644 --- a/src/metabase/db/env.clj +++ b/src/metabase/db/env.clj @@ -26,7 +26,8 @@ [metabase.config :as config] [metabase.db.data-source :as mdb.data-source] [metabase.util :as u] - [metabase.util.log :as log])) + [metabase.util.log :as log] + [metabase.util.malli :as mu])) (set! *warn-on-reflection* true) @@ -39,13 +40,11 @@ "postgresql" :postgres (keyword subprotocol))))) -(defn- env->db-type +(mu/defn ^:private env->db-type :- [:enum :postgres :mysql :h2] [{:keys [mb-db-connection-uri mb-db-type]}] - {:post [(#{:postgres :mysql :h2} %)]} (or (some-> mb-db-connection-uri raw-connection-string->type) mb-db-type)) - ;;;; [[env->DataSource]] (defn- get-db-file @@ -132,7 +131,8 @@ :mb-db-pass (config/config-str :mb-db-pass)} (env-defaults db-type))) -(def ^:private env +(def env + "Metabase Datatbase environment. Used to setup *application-db* and audit-db for enterprise users." (env* (config/config-kw :mb-db-type))) (def db-type diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj index 6598ebf8d7b373074c17f42ad0a184035a26be66..082600692401139e53aaebb68771389e1aa6b0d7 100644 --- a/src/metabase/driver/sql_jdbc/connection.clj +++ b/src/metabase/driver/sql_jdbc/connection.clj @@ -5,6 +5,7 @@ [clojure.java.jdbc :as jdbc] [metabase.config :as config] [metabase.connection-pool :as connection-pool] + [metabase.db.connection :as mdb.connection] [metabase.driver :as driver] [metabase.models.database :refer [Database]] [metabase.models.interface :as mi] @@ -211,6 +212,7 @@ "Return a JDBC connection spec that includes a cp30 `ComboPooledDataSource`. These connection pools are cached so we don't create multiple ones for the same DB." [db-or-id-or-spec] + (cond ;; db-or-id-or-spec is a Database instance or an integer ID (u/id db-or-id-or-spec) @@ -218,14 +220,18 @@ ;; we need the Database instance no matter what (in order to compare details hash with cached value) db (or (when (mi/instance-of? Database db-or-id-or-spec) db-or-id-or-spec) ; passed in - (t2/select-one [Database :id :engine :details] :id database-id) ; look up by ID + (t2/select-one [Database :id :engine :details :is_audit] :id database-id) ; look up by ID (throw (ex-info (tru "Database {0} does not exist." database-id) - {:status-code 404 - :type qp.error-type/invalid-query - :database-id database-id}))) + {:status-code 404 + :type qp.error-type/invalid-query + :database-id database-id}))) get-fn (fn [db-id log-invalidation?] (when-let [details (get @database-id->connection-pool db-id)] (cond + ;; For the audit db, we pass the datasource for the app-db. This lets us use fewer db + ;; connections with the *application-db*, and 1 less connection pool. + (:is_audit db) + {:datasource (mdb.connection/data-source)} ;; details hash changed from what is cached; invalid (let [curr-hash (get @database-id->jdbc-spec-hash db-id) new-hash (jdbc-spec-hash db)] diff --git a/src/metabase/query_processor/store.clj b/src/metabase/query_processor/store.clj index 8ee4217ec9994f283c3b2fa314c063c77324d871..7a37c66e61448461ea184b32534144da888f0f37 100644 --- a/src/metabase/query_processor/store.clj +++ b/src/metabase/query_processor/store.clj @@ -59,7 +59,8 @@ :name :dbms_version :details - :settings]) + :settings + :is_audit]) (def ^:private DatabaseInstanceWithRequiredStoreKeys (s/both diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index 2dcc00d5b2841603aaffec6c793e1cde686ab8ac..cb5d88e697c55306e5c626eb2104c99ce2661a66 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -49,7 +49,8 @@ :refingerprint nil :auto_run_queries true :settings nil - :cache_ttl nil})) + :cache_ttl nil + :is_audit false})) (defn- table-defaults [] (merge