From c4fd58f0d905aeb47f06df67dc25851f5241b0b9 Mon Sep 17 00:00:00 2001 From: Nemanja Glumac <31325167+nemanjaglumac@users.noreply.github.com> Date: Fri, 3 Feb 2023 02:37:34 +0100 Subject: [PATCH] Revert "Added logic to correctly return collections for only the current user (#27962)" (#28046) This reverts commit dca5f4414e4caacc76c75ad1975e137c490fc74a. --- src/metabase/api/collection.clj | 270 ++++++++++++-------------- test/metabase/api/collection_test.clj | 28 +-- 2 files changed, 124 insertions(+), 174 deletions(-) diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj index 6fa57812f85..57d3e7b5b8e 100644 --- a/src/metabase/api/collection.clj +++ b/src/metabase/api/collection.clj @@ -37,42 +37,13 @@ [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db] - [toucan.hydrate :refer [hydrate]] - [toucan.models :as models])) + [toucan.hydrate :refer [hydrate]])) ;;; when alias defined for namespaced keywords is run through kondo macro, ns should be regarded as used (comment collection.root/keep-me) (declare root-collection) -(defn- coll-query - "Generate a query to return collections viewable by the current user. - If provided, additional and clauses will be added to the final where clause. - Note that and clauses should be aliased as c (for collection). Note also that - MySQL 5.7 does not support CTEs so this was all inlined. A much more readable - solution existed with CTEs, but we needed to inline for backwards compatibility." - [owner-id & and-clauses] - {:select [:c.*] - :from [[:collection :c]] - :left-join [[{:select [[:cwo.id :id] - [true :visible]] - :from [[{:select [[:c.id :id] - [[:coalesce - :c.personal_owner_id - :pr.personal_owner_id] :owner]] - :from [[:collection :c]] - :left-join [[{:select [:c.personal_owner_id - [[:concat "/" :c.id "/%"] :pc_prefix]] - :from [[:collection :c]] - :where [:not= :c.personal_owner_id nil]} :pr] - [:like :c.location :pr.pc_prefix]]} :cwo]] - :where [:or - [:= :cwo.owner owner-id] - [:= :cwo.owner nil]]} :a] - [:= :a.id :c.id]] - :where (into [:and [:= :a.visible true]] and-clauses) - :order-by [[:%lower.name :asc]]}) - #_{:clj-kondo/ignore [:deprecated-var]} (api/defendpoint-schema GET "/" "Fetch a list of all Collections that the current user has read permissions for (`:can_write` is returned as an @@ -83,30 +54,29 @@ [archived namespace] {archived (s/maybe su/BooleanString) namespace (s/maybe su/NonBlankString)} - (let [archived? (Boolean/parseBoolean archived) - q (coll-query - api/*current-user-id* - [:= :c.archived archived?] - [:= :c.namespace namespace] - (collection/visible-collection-ids->honeysql-filter-clause - :c.id - (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*)))] - (as-> (->> (mdb.query/query q) - (map (partial models/do-post-select Collection))) collections - ;; include Root Collection at beginning or results if archived isn't `true` - (if archived? - collections - (let [root (root-collection namespace)] - (cond->> collections - (mi/can-read? root) - (cons root)))) - (hydrate collections :can_write) - ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it - ;; and for personal collections we translate the name to user's locale - (for [collection collections] - (-> collection - (dissoc ::collection.root/is-root?) - collection/personal-collection-with-ui-details))))) + (let [archived? (Boolean/parseBoolean archived)] + (as-> (db/select Collection + {:where [:and + [:= :archived archived?] + [:= :namespace namespace] + (collection/visible-collection-ids->honeysql-filter-clause + :id + (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*))] + :order-by [[:%lower.name :asc]]}) collections + ;; include Root Collection at beginning or results if archived isn't `true` + (if archived? + collections + (let [root (root-collection namespace)] + (cond->> collections + (mi/can-read? root) + (cons root)))) + (hydrate collections :can_write) + ;; remove the :metabase.models.collection.root/is-root? tag since FE doesn't need it + ;; and for personal collections we translate the name to user's locale + (for [collection collections] + (-> collection + (dissoc ::collection.root/is-root?) + collection/personal-collection-with-ui-details))))) #_{:clj-kondo/ignore [:deprecated-var]} (api/defendpoint-schema GET "/tree" @@ -140,16 +110,14 @@ (mdb.query/reducible-query {:select-distinct [:collection_id :dataset] :from [:report_card] :where [:= :archived false]})) - q (coll-query - api/*current-user-id* - (when exclude-archived - [:= :c.archived false]) - [:= :c.namespace namespace] - (collection/visible-collection-ids->honeysql-filter-clause - :c.id - (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*))) - colls (->> (mdb.query/query q) - (map (partial models/do-post-select Collection))) + colls (db/select Collection + {:where [:and + (when exclude-archived + [:= :archived false]) + [:= :namespace namespace] + (collection/visible-collection-ids->honeysql-filter-clause + :id + (collection/permissions-set->visible-collection-ids @api/*current-user-permissions-set*))]}) colls (map collection/personal-collection-with-ui-details colls)] (collection/collections->tree coll-type-ids colls))) @@ -217,8 +185,8 @@ (pinned-state->clause pinned-state :collection_position)) ([pinned-state col] (case pinned-state - :all always-true-hsql-expr - :is_pinned [:<> col nil] + :all always-true-hsql-expr + :is_pinned [:<> col nil] :is_not_pinned [:= col nil] always-true-hsql-expr))) @@ -249,10 +217,10 @@ :from [[:pulse :p]] :left-join [[:pulse_card :pc] [:= :p.id :pc.pulse_id]] :where [:and - [:= :p.collection_id (:id collection)] - [:= :p.archived (boolean archived?)] + [:= :p.collection_id (:id collection)] + [:= :p.archived (boolean archived?)] ;; exclude alerts - [:= :p.alert_condition nil] + [:= :p.alert_condition nil] ;; exclude dashboard subscriptions [:= :p.dashboard_id nil]]} (sql.helpers/where (pinned-state->clause pinned-state :p.collection_position)))) @@ -439,15 +407,15 @@ collection [:= :archived archived?] [:= :namespace (u/qualified-name collection-namespace)]) - ;; We get from the effective-children-query a normal set of columns selected: - ;; want to make it fit the others to make UNION ALL work - :select [:id - :name - :description - :entity_id - :personal_owner_id - [(h2x/literal "collection") :model] - :authority_level]) + ;; We get from the effective-children-query a normal set of columns selected: + ;; want to make it fit the others to make UNION ALL work + :select [:id + :name + :description + :entity_id + :personal_owner_id + [(h2x/literal "collection") :model] + :authority_level]) ;; the nil indicates that collections are never pinned. (sql.helpers/where (pinned-state->clause pinned-state nil)))) @@ -463,11 +431,11 @@ ;; Previous examination with logging to DB says that there's no N+1 query for this. ;; However, this was only tested on H2 and Postgres (cond-> row - ;; when fetching root collection, we might have personal collection - (:personal_owner_id row) (assoc :name (collection/user->personal-collection-name (:personal_owner_id row) :user)) - true (assoc :can_write (mi/can-write? Collection (:id row))) - true (dissoc :collection_position :display :moderated_status :icon :personal_owner_id - :collection_preview :dataset_query)))) + ;; when fetching root collection, we might have personal collection + (:personal_owner_id row) (assoc :name (collection/user->personal-collection-name (:personal_owner_id row) :user)) + true (assoc :can_write (mi/can-write? Collection (:id row))) + true (dissoc :collection_position :display :moderated_status :icon :personal_owner_id + :collection_preview :dataset_query)))) (s/defn ^:private coalesce-edit-info :- last-edit/MaybeAnnotated "Hoist all of the last edit information into a map under the key :last-edit-info. Considers this information present @@ -483,8 +451,8 @@ :last_edit_email :email :last_edit_timestamp :timestamp}] (cond-> (apply dissoc row :model_ranking (keys mapping)) - ;; don't use contains as they all have the key, we care about a value present - (:last_edit_user row) (assoc :last-edit-info (select-as row mapping)))))) + ;; don't use contains as they all have the key, we care about a value present + (:last_edit_user row) (assoc :last-edit-info (select-as row mapping)))))) (defn- post-process-rows "Post process any data. Have a chance to process all of the same type at once using @@ -502,12 +470,12 @@ (defn- model-name->toucan-model [model-name] (case (keyword model-name) :collection Collection - :card Card - :dataset Card - :dashboard Dashboard - :pulse Pulse - :snippet NativeQuerySnippet - :timeline Timeline)) + :card Card + :dataset Card + :dashboard Dashboard + :pulse Pulse + :snippet NativeQuerySnippet + :timeline Timeline)) (defn- select-name "Takes a honeysql select column and returns a keyword of which column it is. @@ -566,56 +534,56 @@ treatment of nulls in the different app db types." [sort-info db-type] (case sort-info - nil [[:%lower.name :asc]] - [:name :asc] [[:%lower.name :asc]] - [:name :desc] [[:%lower.name :desc]] - [:last-edited-at :asc] [(if (= db-type :mysql) - [:%isnull.last_edit_timestamp] - [:last_edit_timestamp :nulls-last]) - [:last_edit_timestamp :asc] - [:%lower.name :asc]] + nil [[:%lower.name :asc]] + [:name :asc] [[:%lower.name :asc]] + [:name :desc] [[:%lower.name :desc]] + [:last-edited-at :asc] [(if (= db-type :mysql) + [:%isnull.last_edit_timestamp] + [:last_edit_timestamp :nulls-last]) + [:last_edit_timestamp :asc] + [:%lower.name :asc]] [:last-edited-at :desc] (remove nil? [(case db-type - :mysql [:%isnull.last_edit_timestamp] + :mysql [:%isnull.last_edit_timestamp] :postgres [:last_edit_timestamp :desc-nulls-last] - :h2 nil) + :h2 nil) [:last_edit_timestamp :desc] [:%lower.name :asc]]) - [:last-edited-by :asc] [(if (= db-type :mysql) - [:%isnull.last_edit_last_name] - [:last_edit_last_name :nulls-last]) - [:last_edit_last_name :asc] - (if (= db-type :mysql) - [:%isnull.last_edit_first_name] - [:last_edit_first_name :nulls-last]) - [:last_edit_first_name :asc] - [:%lower.name :asc]] + [:last-edited-by :asc] [(if (= db-type :mysql) + [:%isnull.last_edit_last_name] + [:last_edit_last_name :nulls-last]) + [:last_edit_last_name :asc] + (if (= db-type :mysql) + [:%isnull.last_edit_first_name] + [:last_edit_first_name :nulls-last]) + [:last_edit_first_name :asc] + [:%lower.name :asc]] [:last-edited-by :desc] (remove nil? [(case db-type - :mysql [:%isnull.last_edit_last_name] + :mysql [:%isnull.last_edit_last_name] :postgres [:last_edit_last_name :desc-nulls-last] - :h2 nil) + :h2 nil) [:last_edit_last_name :desc] (case db-type - :mysql [:%isnull.last_edit_first_name] + :mysql [:%isnull.last_edit_first_name] :postgres [:last_edit_last_name :desc-nulls-last] - :h2 nil) + :h2 nil) [:last_edit_first_name :desc] [:%lower.name :asc]]) - [:model :asc] [[:model_ranking :asc] [:%lower.name :asc]] - [:model :desc] [[:model_ranking :desc] [:%lower.name :asc]])) + [:model :asc] [[:model_ranking :asc] [:%lower.name :asc]] + [:model :desc] [[:model_ranking :desc] [:%lower.name :asc]])) (defn- collection-children* [collection models {:keys [sort-info] :as options}] (let [sql-order (children-sort-clause sort-info (mdb/db-type)) models (sort (map keyword models)) queries (for [model models - :let [query (collection-children-query model collection options) - select-clause-type (some - (fn [k] - (when (get query k) - k)) - [:select :select-distinct])]] + :let [query (collection-children-query model collection options) + select-clause-type (some + (fn [k] + (when (get query k) + k)) + [:select :select-distinct])]] (-> query (update select-clause-type add-missing-columns all-select-columns) (update select-clause-type add-model-ranking model))) @@ -632,14 +600,14 @@ (= (:collection-namespace options) "snippets")) rows-query (assoc rows-query - :limit mw.offset-paging/*limit* - :offset mw.offset-paging/*offset*)) + :limit mw.offset-paging/*limit* + :offset mw.offset-paging/*offset*)) res {:total (->> (mdb.query/query total-query) first :count) :data (->> (mdb.query/query limit-query) post-process-rows) :models models} limit-res (assoc res - :limit mw.offset-paging/*limit* - :offset mw.offset-paging/*offset*)] + :limit mw.offset-paging/*limit* + :offset mw.offset-paging/*offset*)] (if (= (:collection-namespace options) "snippets") res limit-res))) @@ -647,14 +615,14 @@ (s/defn ^:private collection-children "Fetch a sequence of 'child' objects belonging to a Collection, filtered using `options`." [{collection-namespace :namespace, :as collection} :- collection/CollectionWithLocationAndIDOrRoot - {:keys [models], :as options} :- CollectionChildrenOptions] + {:keys [models], :as options} :- CollectionChildrenOptions] (let [valid-models (for [model-kw [:collection :dataset :card :dashboard :pulse :snippet :timeline] ;; only fetch models that are specified by the `model` param; or everything if it's empty - :when (or (empty? models) (contains? models model-kw)) - :let [toucan-model (model-name->toucan-model model-kw) - allowed-namespaces (collection/allowed-namespaces toucan-model)] - :when (or (= model-kw :collection) - (contains? allowed-namespaces (keyword collection-namespace)))] + :when (or (empty? models) (contains? models model-kw)) + :let [toucan-model (model-name->toucan-model model-kw) + allowed-namespaces (collection/allowed-namespaces toucan-model)] + :when (or (= model-kw :collection) + (contains? allowed-namespaces (keyword collection-namespace)))] model-kw)] (if (seq valid-models) (collection-children* collection valid-models (assoc options :collection-namespace collection-namespace)) @@ -776,12 +744,12 @@ model-set (set (map keyword (u/one-or-many models))) model-kwds (visible-model-kwds root-collection model-set)] (collection-children - root-collection - {:models model-kwds - :archived? (Boolean/parseBoolean archived) - :pinned-state (keyword pinned_state) - :sort-info [(or (some-> sort_column normalize-sort-choice) :name) - (or (some-> sort_direction normalize-sort-choice) :asc)]}))) + root-collection + {:models model-kwds + :archived? (Boolean/parseBoolean archived) + :pinned-state (keyword pinned_state) + :sort-info [(or (some-> sort_column normalize-sort-choice) :name) + (or (some-> sort_direction normalize-sort-choice) :asc)]}))) ;;; ----------------------------------------- Creating/Editing a Collection ------------------------------------------ @@ -793,7 +761,7 @@ (api/write-check (if collection-id (db/select-one Collection :id collection-id) (cond-> collection/root-collection - collection-namespace (assoc :namespace collection-namespace))))) + collection-namespace (assoc :namespace collection-namespace))))) (defn create-collection! "Create a new collection." @@ -805,11 +773,11 @@ (and api/*is-superuser?* authority_level))) (db/insert! Collection (merge - {:name name - :color color - :description description + {:name name + :color color + :description description :authority_level authority_level - :namespace namespace} + :namespace namespace} (when parent_id {:location (collection/children-location (db/select-one [Collection :location :id] :id parent_id))})))) @@ -846,7 +814,7 @@ ;; ok, make sure we have perms to do this operation (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* - (collection/perms-for-moving collection-before-update new-parent))) + (collection/perms-for-moving collection-before-update new-parent))) ;; ok, we're good to move! (collection/move-collection! collection-before-update new-location))))) @@ -860,7 +828,7 @@ ;; Check that we have approprate perms (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set* - (collection/perms-for-archiving collection-before-update))))) + (collection/perms-for-archiving collection-before-update))))) (defn- maybe-send-archived-notificaitons! "When a collection is archived, all of it's cards are also marked as archived, but this is down in the model layer @@ -876,12 +844,12 @@ (api/defendpoint-schema PUT "/:id" "Modify an existing Collection, including archiving or unarchiving it, or moving it." [id, :as {{:keys [name color description archived parent_id authority_level], :as collection-updates} :body}] - {name (s/maybe su/NonBlankString) - color (s/maybe collection/hex-color-regex) - description (s/maybe su/NonBlankString) - archived (s/maybe s/Bool) - parent_id (s/maybe su/IntGreaterThanZero) - authority_level collection/AuthorityLevel} + {name (s/maybe su/NonBlankString) + color (s/maybe collection/hex-color-regex) + description (s/maybe su/NonBlankString) + archived (s/maybe s/Bool) + parent_id (s/maybe su/IntGreaterThanZero) + authority_level collection/AuthorityLevel} ;; do we have perms to edit this Collection? (let [collection-before-update (api/write-check Collection id)] ;; if we're trying to *archive* the Collection, make sure we're allowed to do that diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 9d689a05ed1..ff98db02236 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -29,7 +29,6 @@ [metabase.models.permissions-group :as perms-group] [metabase.models.revision :as revision] [metabase.test :as mt] - [metabase.test.data.users :as test.users] [metabase.test.fixtures :as fixtures] [metabase.util :as u] [metabase.util.schema :as su] @@ -95,33 +94,16 @@ (filter :personal_owner_id) (map :name)))) - (testing "...even if we are *admins*" - (is (= ["Crowberto Corv's Personal Collection"] + (testing "...unless we are *admins*" + (is (= ["Crowberto Corv's Personal Collection" + "Lucky Pigeon's Personal Collection" + "Rasta Toucan's Personal Collection" + "Trash Bird's Personal Collection"] (->> (mt/user-http-request :crowberto :get 200 "collection") (filter #((set (map mt/user->id [:crowberto :lucky :rasta :trashbird])) (:personal_owner_id %))) (map :name) sort))))) - (testing "You should only see your collection and public collections" - (let [user-id (u/the-id (test.users/fetch-user :crowberto)) - crowberto-root (db/select-one Collection :personal_owner_id user-id)] - (mt/with-temp* [Collection [collection] - Collection [{collection-id :id} {:name "Collection with Items"}] - Collection [_ {:name "subcollection" - :location (format "/%d/" collection-id) - :authority_level "official"}] - Collection [_ {:name "Crowberto's Child Collection" - :location (collection/location-path crowberto-root)}]] - (let [public-collections #{"Our analytics" (:name collection) "Collection with Items" "subcollection"} - crowbertos (set (map :name (mt/user-http-request :crowberto :get 200 "collection"))) - luckys (set (map :name (mt/user-http-request :lucky :get 200 "collection")))] - (is (= (into public-collections #{"Crowberto Corv's Personal Collection" "Crowberto's Child Collection"}) - crowbertos)) - (is (false? (contains? crowbertos "Lucky Pigeon's Personal Collection"))) - (is (= (conj public-collections (:name collection) "Lucky Pigeon's Personal Collection") - luckys)) - (is (false? (contains? luckys "Crowberto Corv's Personal Collection"))))))) - (testing "Personal Collection's name and slug should be returned in user's locale" (with-french-user-and-personal-collection user _collection (is (= [{:name "Collection personnelle de Taco Bell" -- GitLab