diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index a3c19a00c5ca1b8fcb15aefc9688d5462dc0fd5a..29a174a44356f115446220f51324ef2a18eb9c86 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -668,7 +668,7 @@ metabase.search.config search.config metabase.search.filter search.filter metabase.search.in-place.filter search.in-place.filter - metabase.search.util search.util + metabase.search.in-place.util search.util metabase.search.in-place.scoring scoring metabase.server.middleware.auth mw.auth metabase.server.middleware.browser-cookie mw.browser-cookie diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj index ea0844e1c079c3bf4c812e7efb7fe52568438b02..e11b9c9e635ce0a0e62c99b6d731b8cb89365fdb 100644 --- a/src/metabase/models/table.clj +++ b/src/metabase/models/table.clj @@ -311,7 +311,7 @@ :view-count true :created-at true :updated-at true} - :search-terms [:name :description :display_name] + :search-terms [:name :display_name :description] :render-terms {:initial-sync-status true :table-id :id :table-description :description diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 4a967a0001ee9a23b01e22d1f37d25d0dfbb9c96..316f37a4a3a98d16c5afd79a07ec2352fccc2850 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -1052,7 +1052,6 @@ See [fonts](../configuring-metabase/fonts.md).") (defsetting experimental-search-weight-overrides (deferred-tru "Used to override weights used for search ranking") :visibility :internal - :cache? false :encryption :no :export? false :default nil diff --git a/src/metabase/search/filter.clj b/src/metabase/search/filter.clj index a0bdc477d174904765a88d2d001d12e9063a5c90..cc9c731cad13fe64a9fb9a2d9e94598e03d9ae6f 100644 --- a/src/metabase/search/filter.clj +++ b/src/metabase/search/filter.clj @@ -8,7 +8,8 @@ [metabase.search.permissions :as search.permissions] [metabase.search.spec :as search.spec] [metabase.util.date-2 :as u.date] - [metabase.util.i18n :refer [tru]]) + [metabase.util.i18n :refer [tru]] + [toucan2.core :as t2]) (:import (java.time LocalDate))) @@ -77,6 +78,31 @@ (defmethod where-clause* ::list [_ k v] [:in k v]) +(defn personal-collections-where-clause + "Build a clause limiting the entries to those (not) within or within personal collections, if relevant. + WARNING: this method queries the appdb, and its approach will get very slow when there are many users!" + [{filter-type :filter-items-in-personal-collection} collection-id-col] + (when filter-type + (let [parent-ids (t2/select-pks-vec :model/Collection :personal_owner_id [:not= nil]) + child-patterns (for [id parent-ids] (format "/%d/%%" id))] + (case filter-type + "only" + `[:or + ;; top level personal collections + [:and [:not= :collection.personal_owner_id nil] [:= :collection.location "/"]] + ;; their sub-collections + ~@(for [p child-patterns] [:like :collection.location p])] + + "exclude" + `[:or + ;; not in a collection + [:= ~collection-id-col nil] + [:and + ;; neither in a top-level personal collection + [:= :collection.personal_owner_id nil] + ;; nor within one of their sub-collections + ~@(for [p child-patterns] [:not-like :collection.location p])]])))) + (defn with-filters "Return a HoneySQL clause corresponding to all the optional search filters." [search-context qry] diff --git a/src/metabase/search/in_place/filter.clj b/src/metabase/search/in_place/filter.clj index 8ad689f5695b6d37e84fd4a15e2746fd4e3ddc8b..3ad8b997f5033420212f2591d2ee64a36a3294d4 100644 --- a/src/metabase/search/in_place/filter.clj +++ b/src/metabase/search/in_place/filter.clj @@ -19,8 +19,8 @@ [metabase.search.config :as search.config :refer [SearchableModel SearchContext]] + [metabase.search.in-place.util :as search.util] [metabase.search.permissions :as search.permissions] - [metabase.search.util :as search.util] [metabase.util.date-2 :as u.date] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu]) diff --git a/src/metabase/search/in_place/scoring.clj b/src/metabase/search/in_place/scoring.clj index 934c15becb1023b70953edba4f831590233b90cf..27d93e252734fdd756ded39f88ba52f4f0f11a69 100644 --- a/src/metabase/search/in_place/scoring.clj +++ b/src/metabase/search/in_place/scoring.clj @@ -115,7 +115,7 @@ [java-time.api :as t] [metabase.public-settings.premium-features :refer [defenterprise]] [metabase.search.config :as search.config] - [metabase.search.util :as search.util] + [metabase.search.in-place.util :as search.util] [metabase.util :as u])) (defn- matches? diff --git a/src/metabase/search/util.clj b/src/metabase/search/in_place/util.clj similarity index 98% rename from src/metabase/search/util.clj rename to src/metabase/search/in_place/util.clj index a1954f0d8b8e721137ad278b7990ce8d95b447fe..9358160dda879fa2cdf86c1187224f03a94c0516 100644 --- a/src/metabase/search/util.clj +++ b/src/metabase/search/in_place/util.clj @@ -1,4 +1,4 @@ -(ns metabase.search.util +(ns metabase.search.in-place.util (:require [clojure.core.memoize :as memoize] [clojure.string :as str] diff --git a/src/metabase/search/legacy.clj b/src/metabase/search/legacy.clj index 85736dca42ffdb2bd0ece9df11cce7ee64fd1e5f..20c1d3380113ae94c4706a2926dc65e755496967 100644 --- a/src/metabase/search/legacy.clj +++ b/src/metabase/search/legacy.clj @@ -11,10 +11,11 @@ [metabase.search.config :as search.config :refer [SearchContext SearchableModel]] + [metabase.search.filter :as search.filter] [metabase.search.in-place.filter :as search.in-place.filter] [metabase.search.in-place.scoring :as scoring] + [metabase.search.in-place.util :as search.util] [metabase.search.permissions :as search.permissions] - [metabase.search.util :as search.util] [metabase.util :as u] [metabase.util.honey-sql-2 :as h2x] [metabase.util.log :as log] @@ -151,6 +152,24 @@ (sql.helpers/where query [:= id :database_id]) query)) +(mu/defn add-collection-join-and-where-clauses + "Add a `WHERE` clause to the query to only return Collections the Current User has access to; join against Collection, + so we can return its `:name`." + [honeysql-query :- :map + model :- [:maybe :string] + search-ctx :- SearchContext] + (let [collection-id-col (case model + "collection" :collection.id + "search-index" :search_index.collection_id + :collection_id) + permitted-clause (search.permissions/permitted-collections-clause search-ctx collection-id-col) + personal-clause (search.filter/personal-collections-where-clause search-ctx collection-id-col)] + (cond-> honeysql-query + ;; add a JOIN against Collection *unless* the source table is already Collection + (not= model "collection") (sql.helpers/left-join [:collection :collection] [:= collection-id-col :collection.id]) + true (sql.helpers/where permitted-clause) + personal-clause (sql.helpers/where personal-clause)))) + (mu/defn- replace-select :- :map "Replace a select from query that has alias is `target-alias` with [`with` `target-alias`] column, throw an error if can't find the target select. @@ -418,7 +437,7 @@ [:and [:= :bookmark.card_id :card.id] [:= :bookmark.user_id (:current-user-id search-ctx)]]) - (search.permissions/add-collection-join-and-where-clauses "card" search-ctx) + (add-collection-join-and-where-clauses "card" search-ctx) (add-card-db-id-clause (:table-db-id search-ctx)) (with-last-editing-info "card") (with-moderated-status "card"))) @@ -430,7 +449,7 @@ [:= :model.id :action.model_id]) (sql.helpers/left-join :query_action [:= :query_action.action_id :action.id]) - (search.permissions/add-collection-join-and-where-clauses model search-ctx))) + (add-collection-join-and-where-clauses model search-ctx))) (defmethod search-query-for-model "card" [_model search-ctx] @@ -455,7 +474,7 @@ [:and [:= :bookmark.collection_id :collection.id] [:= :bookmark.user_id (:current-user-id search-ctx)]]) - (search.permissions/add-collection-join-and-where-clauses model search-ctx))) + (add-collection-join-and-where-clauses model search-ctx))) (defmethod search-query-for-model "database" [model search-ctx] @@ -469,7 +488,7 @@ [:= :bookmark.dashboard_id :dashboard.id] [:= :bookmark.user_id (:current-user-id search-ctx)]]) (with-moderated-status "dashboard") - (search.permissions/add-collection-join-and-where-clauses model search-ctx) + (add-collection-join-and-where-clauses model search-ctx) (with-last-editing-info "dashboard"))) (defn- add-model-index-permissions-clause diff --git a/src/metabase/search/permissions.clj b/src/metabase/search/permissions.clj index f2408e8802a66f48d8117057a749a09d23117216..23b11134b825a86fab3ef08de05484c971c32755 100644 --- a/src/metabase/search/permissions.clj +++ b/src/metabase/search/permissions.clj @@ -1,12 +1,10 @@ (ns metabase.search.permissions (:require - [honey.sql.helpers :as sql.helpers] [metabase.models.collection :as collection] [metabase.models.permissions :as perms] [metabase.public-settings.premium-features :as premium-features] [metabase.search.config :refer [SearchContext]] - [metabase.util.malli :as mu] - [toucan2.core :as t2])) + [metabase.util.malli :as mu])) (defn- assert-current-user! [missing-param] (assert @@(requiring-resolve 'metabase.api.common/*current-user*) @@ -31,54 +29,15 @@ [search-ctx] (or (impersonated-user? search-ctx) (sandboxed-user? search-ctx))) -(mu/defn add-collection-join-and-where-clauses - "Add a `WHERE` clause to the query to only return Collections the Current User has access to; join against Collection, - so we can return its `:name`." - [honeysql-query :- :map - model :- [:maybe :string] - {:keys [filter-items-in-personal-collection - archived - current-user-id - is-superuser?]} :- SearchContext] - (let [collection-id-col (case model - "collection" :collection.id - "search-index" :search_index.collection_id - :collection_id) - collection-filter-clause (collection/visible-collection-filter-clause - collection-id-col - {:include-archived-items :all - :include-trash-collection? true - :permission-level (if archived - :write - :read)} - {:current-user-id current-user-id - :is-superuser? is-superuser?})] - (cond-> honeysql-query - true - (sql.helpers/where collection-filter-clause (perms/audit-namespace-clause :collection.namespace nil)) - ;; add a JOIN against Collection *unless* the source table is already Collection - (not= model "collection") - (sql.helpers/left-join [:collection :collection] - [:= collection-id-col :collection.id]) - - ;; TODO This is not really about permissions, it should really be handled in search.filter - (some? filter-items-in-personal-collection) - (sql.helpers/where - (case filter-items-in-personal-collection - "only" - (concat [:or] - ;; sub personal collections - (for [id (t2/select-pks-set :model/Collection :personal_owner_id [:not= nil])] - [:like :collection.location (format "/%d/%%" id)]) - ;; top level personal collections - [[:and - [:= :collection.location "/"] - [:not= :collection.personal_owner_id nil]]]) - - "exclude" - (conj [:or] - (into - [:and [:= :collection.personal_owner_id nil]] - (for [id (t2/select-pks-set :model/Collection :personal_owner_id [:not= nil])] - [:not-like :collection.location (format "/%d/%%" id)])) - [:= collection-id-col nil])))))) +(mu/defn permitted-collections-clause + "Build the WHERE clause corresponding to which collections the given user has access to." + [{:keys [archived current-user-id is-superuser?]} :- SearchContext collection-id-col :- :keyword] + [:and + (collection/visible-collection-filter-clause + collection-id-col + {:include-archived-items :all + :include-trash-collection? true + :permission-level (if archived :write :read)} + {:current-user-id current-user-id + :is-superuser? is-superuser?}) + (perms/audit-namespace-clause :collection.namespace nil)]) diff --git a/src/metabase/search/postgres/core.clj b/src/metabase/search/postgres/core.clj index fc9e65d6ac267465ce0176fb37b77a8dd5e7bdb9..5e188656019df764ed28fcd59a1179ca60e8858b 100644 --- a/src/metabase/search/postgres/core.clj +++ b/src/metabase/search/postgres/core.clj @@ -81,14 +81,26 @@ (update :updated_at parse-datetime) (update :last_edited_at parse-datetime))) +(defn add-collection-join-and-where-clauses + "Add a `WHERE` clause to the query to only return Collections the Current User has access to; join against Collection, + so we can return its `:name`." + [search-ctx qry] + (let [collection-id-col :search_index.collection_id + permitted-clause (search.permissions/permitted-collections-clause search-ctx collection-id-col) + personal-clause (search.filter/personal-collections-where-clause search-ctx collection-id-col)] + (cond-> qry + true (sql.helpers/left-join [:collection :collection] [:= collection-id-col :collection.id]) + true (sql.helpers/where permitted-clause) + personal-clause (sql.helpers/where personal-clause)))) + (defn- fulltext "Search purely using the index." [search-term & {:as search-ctx}] (when-not @#'search.index/initialized? (throw (ex-info "Search index is not initialized. Use [[init!]] to ensure it exists." {:search-engine :postgres}))) - (->> (let [base-query (search.index/search-query search-term search-ctx [:legacy_input])] - (search.permissions/add-collection-join-and-where-clauses base-query "search-index" search-ctx)) + (->> (search.index/search-query search-term search-ctx [:legacy_input]) + (add-collection-join-and-where-clauses search-ctx) (search.scoring/with-scores search-ctx) (search.filter/with-filters search-ctx) (t2/query) @@ -114,8 +126,8 @@ [search-ctx] ;; We ignore any current models filter (let [search-ctx (assoc search-ctx :models search.config/all-models)] - (->> (-> (search.index/search-query (:search-string search-ctx) search-ctx [[[:distinct :model] :model]]) - (search.permissions/add-collection-join-and-where-clauses "search-index" search-ctx)) + (->> (search.index/search-query (:search-string search-ctx) search-ctx [[[:distinct :model] :model]]) + (add-collection-join-and-where-clauses search-ctx) (search.filter/with-filters search-ctx) t2/query (into #{} (map :model))))) diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj index d469ef73a25d3c24bae37ebc43ca777efa93234c..90c62acde4590f022339ddba0959f4db131219a7 100644 --- a/test/metabase/api/search_test.clj +++ b/test/metabase/api/search_test.clj @@ -313,7 +313,7 @@ (when (search/supports-index?) (testing "It can use an alternate search engine" (with-search-items-in-root-collection "test" - (let [resp (search-request :crowberto :q "test" :search_engine "fulltext")] + (let [resp (search-request :crowberto :q "test" :search_engine "fulltext" :limit 1)] ;; The index is not populated here, so there's not much interesting to assert. (is (= "search.engine/fulltext" (:engine resp)))))))) diff --git a/test/metabase/search/util_test.clj b/test/metabase/search/in_place/util_test.clj similarity index 95% rename from test/metabase/search/util_test.clj rename to test/metabase/search/in_place/util_test.clj index 980f14354ae5e9e404851e3b2712f4bfc01d06c7..5f4f1fe8b7cf5c7f52ee4652764f6168f5108039 100644 --- a/test/metabase/search/util_test.clj +++ b/test/metabase/search/in_place/util_test.clj @@ -1,7 +1,7 @@ -(ns metabase.search.util-test +(ns metabase.search.in-place.util-test (:require [clojure.test :refer :all] - [metabase.search.util :as search.util])) + [metabase.search.in-place.util :as search.util])) (deftest ^:parallel tokenize-test (testing "basic tokenization"