From c706ba0dcdb43950e026b86c5d6691e1be8c187d Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Tue, 5 Jan 2021 13:44:13 -0800 Subject: [PATCH] Fix #14114 (#14251) * Fix #14114 * Test fix :wrench: --- .dir-locals.el | 3 +- .../collections/collections.cy.spec.js | 2 +- src/metabase/models/collection.clj | 65 +- test/metabase/api/collection_test.clj | 53 +- test/metabase/models/collection_test.clj | 735 +++++++++--------- 5 files changed, 424 insertions(+), 434 deletions(-) diff --git a/.dir-locals.el b/.dir-locals.el index 59d9bd66e2f..e2c2e9831fc 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -26,7 +26,8 @@ (p.types/def-abstract-type '(1 (:defn))) (p.types/deftype+ '(2 nil nil (:defn))) (p/def-map-type '(2 nil nil (:defn))) - (p.types/defrecord+ '(2 nil nil (:defn)))))) + (p.types/defrecord+ '(2 nil nil (:defn))) + (tools.macro/macrolet '(1 (:defn)))))) ;; if you're using clj-refactor (highly recommended!), prefer prefix notation when cleaning the ns form (cljr-favor-prefix-notation . t) ;; prefer keeping source width about ~118, GitHub seems to cut off stuff at either 119 or 120 and diff --git a/frontend/test/metabase/scenarios/collections/collections.cy.spec.js b/frontend/test/metabase/scenarios/collections/collections.cy.spec.js index bc212ebe19b..8637623fe42 100644 --- a/frontend/test/metabase/scenarios/collections/collections.cy.spec.js +++ b/frontend/test/metabase/scenarios/collections/collections.cy.spec.js @@ -289,7 +289,7 @@ describe("scenarios > collection_defaults", () => { signInAsAdmin(); }); - it.skip("should see a child collection in a sidebar even with revoked access to its parent (metabase#14114)", () => { + it("should see a child collection in a sidebar even with revoked access to its parent (metabase#14114)", () => { // Create Parent collection within `Our analytics` cy.request("POST", "/api/collection", { name: "Parent", diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj index 62b4d6ff336..c051dca0810 100644 --- a/src/metabase/models/collection.clj +++ b/src/metabase/models/collection.clj @@ -1004,32 +1004,39 @@ :children [{:name \"G\"}]}]}]} {:name \"H\"}]" [collections] - (transduce - identity - (fn ->tree - ;; 1. We'll use a map representation to start off with to make building the tree easier. Keyed by Collection ID - ;; e.g. - ;; - ;; {1 {:name "A" - ;; :children {2 {:name "B"}, ...}}} - ([] {}) - ;; 2. For each as we come across it, put it in the correct location in the tree. Convert it's `:location` (e.g. - ;; `/1/`) plus its ID to a key path e.g. `[1 :children 2]` - ([m collection] - (let [path (interpose :children (concat (location-path->ids (:location collection)) - [(:id collection)]))] - (assoc-in m path collection))) - ;; 3. Once we've build the entire tree structure, go in and convert each ID->Collection map into a flat sequence, - ;; sorted by the lowercased Collection name. Do this recursively for the `:children` of each Collection e.g. - ;; - ;; {1 {:name "A" - ;; :children {2 {:name "B"}, ...}}} - ;; -> - ;; [{:name "A" - ;; :children [{:name "B"}, ...]}] - ([m] - (let [vs (for [v (vals m)] - (cond-> v - (:children v) (update :children ->tree)))] - (sort-by (comp (fnil u/lower-case-en "") :name) vs)))) - collections)) + (let [all-visible-ids (set (map :id collections))] + (transduce + identity + (fn ->tree + ;; 1. We'll use a map representation to start off with to make building the tree easier. Keyed by Collection ID + ;; e.g. + ;; + ;; {1 {:name "A" + ;; :children {2 {:name "B"}, ...}}} + ([] {}) + ;; 2. For each as we come across it, put it in the correct location in the tree. Convert it's `:location` (e.g. + ;; `/1/`) plus its ID to a key path e.g. `[1 :children 2]` + ;; + ;; If any ancestor Collections are not present in `collections`, just remove their IDs from the path, + ;; effectively "pulling" a Collection up to a higher level. e.g. if we have A > B > C and we can't see B then + ;; the tree should come back as A > C. + ([m collection] + (let [path (as-> (location-path->ids (:location collection)) ids + (filter all-visible-ids ids) + (concat ids [(:id collection)]) + (interpose :children ids))] + (assoc-in m path collection))) + ;; 3. Once we've build the entire tree structure, go in and convert each ID->Collection map into a flat sequence, + ;; sorted by the lowercased Collection name. Do this recursively for the `:children` of each Collection e.g. + ;; + ;; {1 {:name "A" + ;; :children {2 {:name "B"}, ...}}} + ;; -> + ;; [{:name "A" + ;; :children [{:name "B"}, ...]}] + ([m] + (let [vs (for [v (vals m)] + (cond-> v + (:children v) (update :children ->tree)))] + (sort-by (comp (fnil u/lower-case-en "") :name) vs)))) + collections))) diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 28f167f5853..d41dd8736aa 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -4,7 +4,7 @@ [string :as str] [test :refer :all]] [metabase - [models :refer [Card Collection Dashboard DashboardCard NativeQuerySnippet Permissions PermissionsGroup + [models :refer [Card Collection Dashboard DashboardCard NativeQuerySnippet PermissionsGroup PermissionsGroupMembership Pulse PulseCard PulseChannel PulseChannelRecipient]] [test :as mt] [util :as u]] @@ -119,6 +119,15 @@ ;;; | GET /collection/tree | ;;; +----------------------------------------------------------------------------------------------------------------+ +(defn- collection-tree-names-only + "Keep just the names of Collections in `collection-ids-to-keep` in the response returned by the Collection tree + endpoint." + [collection-ids-to-keep collections] + (for [collection collections + :when (contains? (set collection-ids-to-keep) (:id collection))] + (cond-> (select-keys collection [:name :children]) + (:children collection) (update :children (partial collection-tree-names-only collection-ids-to-keep))))) + (deftest collection-tree-test (testing "GET /api/collection/tree" (with-collection-hierarchy [a b c d e f g] @@ -126,21 +135,15 @@ [a b c d e f g]))) response (mt/user-http-request :rasta :get 200 "collection/tree")] (testing "Make sure overall tree shape of the response is as is expected" - (letfn [(collection-names [collections] - (for [collection collections - :when (contains? ids (:id collection))] - (cond-> (select-keys collection [:name :children]) - (:children collection) (update :children collection-names))))] - (testing "GET /api/collection/tree" - (is (= [{:name "A" - :children [{:name "B"} - {:name "C" - :children [{:name "D" - :children [{:name "E"}]} - {:name "F" - :children [{:name "G"}]}]}]} - {:name "Rasta Toucan's Personal Collection"}] - (collection-names response)))))) + (is (= [{:name "A" + :children [{:name "B"} + {:name "C" + :children [{:name "D" + :children [{:name "E"}]} + {:name "F" + :children [{:name "G"}]}]}]} + {:name "Rasta Toucan's Personal Collection"}] + (collection-tree-names-only ids response)))) (testing "Make sure each Collection comes back with the expected keys" (is (= {:description nil :archived false @@ -155,6 +158,24 @@ %) response)))))))) +(deftest collection-tree-child-permissions-test + (testing "GET /api/collection/tree" + (testing "Tree endpoint should still return Collections if we don't have perms for the parent Collection (#14114)" + ;; Create a hierarchy like: + ;; + ;; + Our analytics (Revoke permissions to All Users) + ;; +--+ Parent collection (Revoke permissions to All Users) + ;; +--+ Child collection (Give All Users group Curate access) + (mt/with-non-admin-groups-no-root-collection-perms + (mt/with-temp* [Collection [parent-collection {:name "Parent"}] + Collection [child-collection {:name "Child", :location (format "/%d/" (:id parent-collection))}]] + (perms/revoke-collection-permissions! (group/all-users) parent-collection) + (perms/grant-collection-readwrite-permissions! (group/all-users) child-collection) + (is (= [{:name "Child"}] + (collection-tree-names-only (map :id [parent-collection child-collection]) + (mt/user-http-request :rasta :get 200 "collection/tree"))))))))) + + ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | GET /collection/:id | ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/test/metabase/models/collection_test.clj b/test/metabase/models/collection_test.clj index 0846a630c89..33c1072dfa4 100644 --- a/test/metabase/models/collection_test.clj +++ b/test/metabase/models/collection_test.clj @@ -3,7 +3,6 @@ (:require [clojure [string :as str] [test :refer :all]] - [expectations :refer [expect]] [metabase [models :refer [Card Collection Dashboard NativeQuerySnippet Permissions PermissionsGroup Pulse User]] [test :as mt] @@ -567,101 +566,84 @@ (defn- effective-children [collection] (set (map :name (collection/effective-children collection)))) -;; If we *have* perms for everything we should just see B and C. -(expect - #{"B" "C"} +(deftest effective-children-test (with-collection-hierarchy [{:keys [a b c d e f g]}] - (with-current-user-perms-for-collections [a b c d e f g] - (effective-children a)))) + (testing "If we *have* perms for everything we should just see B and C." + (with-current-user-perms-for-collections [a b c d e f g] + (is (= #{"B" "C"} + (effective-children a))))) -;; make sure that `effective-children` isn't returning children or location of children! Those should get discarded. -(expect - #{:name :id :description} - (with-collection-hierarchy [{:keys [a b c d e f g]}] - (with-current-user-perms-for-collections [a b c d e f g] - (set (keys (first (collection/effective-children a))))))) + (testing "make sure that `effective-children` isn't returning children or location of children! Those should get discarded." + (with-current-user-perms-for-collections [a b c d e f g] + (is (= #{:name :id :description} + (set (keys (first (collection/effective-children a)))))))) -;; If we don't have permissions for C, C's children (D and F) should be moved up one level -;; -;; +-> B +-> B -;; | | -;; A -+-> x -+-> D -> E ===> A -+-> D -> E -;; | | -;; +-> F -> G +-> F -> G -(expect - #{"B" "D" "F"} - (with-collection-hierarchy [{:keys [a b d e f g]}] - (with-current-user-perms-for-collections [a b d e f g] - (effective-children a)))) - -;; If we also remove D, its child (F) should get moved up, for a total of 2 levels. -;; -;; +-> B +-> B -;; | | -;; A -+-> x -+-> x -> E ===> A -+-> E -;; | | -;; +-> F -> G +-> F -> G -(expect - #{"B" "E" "F"} - (with-collection-hierarchy [{:keys [a b e f g]}] - (with-current-user-perms-for-collections [a b e f g] - (effective-children a)))) - -;; If we remove C and both its children, both grandchildren should get get moved up -;; -;; +-> B +-> B -;; | | -;; A -+-> x -+-> x -> E ===> A -+-> E -;; | | -;; +-> x -> G +-> G -(expect - #{"B" "E" "G"} - (with-collection-hierarchy [{:keys [a b e g]}] - (with-current-user-perms-for-collections [a b e g] - (effective-children a)))) - -;; Now try with one of the Children. `effective-children` for C should be D & F -;; -;; C -+-> D -> E C -+-> D -> E -;; | ===> | -;; +-> F -> G +-> F -> G -(expect - #{"D" "F"} - (with-collection-hierarchy [{:keys [b c d e f g]}] - (with-current-user-perms-for-collections [b c d e f g] - (effective-children c)))) - -;; If we remove perms for D & F their respective children should get moved up -;; -;; C -+-> x -> E C -+-> E -;; | ===> | -;; +-> x -> G +-> G -(expect - #{"E" "G"} - (with-collection-hierarchy [{:keys [b c e g]}] - (with-current-user-perms-for-collections [b c e g] - (effective-children c)))) - -;; For the Root Collection: can we fetch its effective children? -(expect - #{"A"} - (with-collection-hierarchy [{:keys [a b c d e f g]}] - (with-current-user-perms-for-collections [a b c d e f g] - (effective-children collection/root-collection)))) - -;; For the Root Collection: if we don't have perms for A, we should get B and C as effective children -(expect - #{"B" "C"} - (with-collection-hierarchy [{:keys [b c d e f g]}] - (with-current-user-perms-for-collections [b c d e f g] - (effective-children collection/root-collection)))) - -;; For the Root Collection: if we remove A and C we should get B, D and F -(expect - #{"B" "D" "F"} - (with-collection-hierarchy [{:keys [b d e f g]}] - (with-current-user-perms-for-collections [b d e f g] - (effective-children collection/root-collection)))) + (testing "If we don't have permissions for C, C's children (D and F) should be moved up one level" + ;; + ;; +-> B +-> B + ;; | | + ;; A -+-> x -+-> D -> E ===> A -+-> D -> E + ;; | | + ;; +-> F -> G +-> F -> G + (with-current-user-perms-for-collections [a b d e f g] + (is (= #{"B" "D" "F"} + (effective-children a))))) + + (testing "If we also remove D, its child (F) should get moved up, for a total of 2 levels." + ;; + ;; +-> B +-> B + ;; | | + ;; A -+-> x -+-> x -> E ===> A -+-> E + ;; | | + ;; +-> F -> G +-> F -> G + (with-current-user-perms-for-collections [a b e f g] + (is (= #{"B" "E" "F"} + (effective-children a))))) + + (testing "If we remove C and both its children, both grandchildren should get get moved up" + ;; + ;; +-> B +-> B + ;; | | + ;; A -+-> x -+-> x -> E ===> A -+-> E + ;; | | + ;; +-> x -> G +-> G + (with-current-user-perms-for-collections [a b e g] + (is (= #{"B" "E" "G"} + (effective-children a))))) + + (testing "Now try with one of the Children. `effective-children` for C should be D & F" + ;; + ;; C -+-> D -> E C -+-> D -> E + ;; | ===> | + ;; +-> F -> G +-> F -> G + (with-current-user-perms-for-collections [b c d e f g] + (is (= #{"D" "F"} + (effective-children c))))) + + (testing "If we remove perms for D & F their respective children should get moved up" + ;; + ;; C -+-> x -> E C -+-> E + ;; | ===> | + ;; +-> x -> G +-> G + (with-current-user-perms-for-collections [b c e g] + (is (= #{"E" "G"} + (effective-children c))))) + + (testing "For the Root Collection: can we fetch its effective children?" + (with-current-user-perms-for-collections [a b c d e f g] + (is (= #{"A"} + (effective-children collection/root-collection))))) + + (testing "For the Root Collection: if we don't have perms for A, we should get B and C as effective children" + (with-current-user-perms-for-collections [b c d e f g] + (is (= #{"B" "C"} + (effective-children collection/root-collection))))) + + (testing "For the Root Collection: if we remove A and C we should get B, D and F" + (with-collection-hierarchy [{:keys [b d e f g]}] + (is (= #{"B" "D" "F"} + (with-current-user-perms-for-collections [b d e f g] + (effective-children collection/root-collection)))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Nested Collections: Perms for Moving & Archiving | @@ -881,95 +863,89 @@ [(:name collection)]) {})))) -;; Make sure the util functions above actually work correctly -;; -;; +-> B -;; | -;; A -+-> C -+-> D -> E -;; | -;; +-> F -> G -(expect - {"A" {"B" {} - "C" {"D" {"E" {}} - "F" {"G" {}}}}} - (with-collection-hierarchy [collections] - (collection-locations (vals collections)))) - -;; Test that we can move a Collection -;; -;; +-> B +-> B ---> E -;; | | -;; A -+-> C -+-> D -> E ===> A -+-> C -+-> D -;; | | -;; +-> F -> G +-> F -> G -(expect - {"A" {"B" {"E" {}} - "C" {"D" {} - "F" {"G" {}}}}} - (with-collection-hierarchy [{:keys [b e], :as collections}] - (collection/move-collection! e (collection/children-location b)) - (collection-locations (vals collections)))) - -;; Test that we can move a Collection and its descendants get moved as well -;; -;; +-> B +-> B ---> D -> E -;; | | -;; A -+-> C -+-> D -> E ===> A -+-> C -+ -;; | | -;; +-> F -> G +-> F -> G -(expect - {"A" {"B" {"D" {"E" {}}} - "C" {"F" {"G" {}}}}} - (with-collection-hierarchy [{:keys [b d], :as collections}] - (collection/move-collection! d (collection/children-location b)) - (collection-locations (vals collections)))) - - -;; Test that we can move a Collection into the Root Collection -;; -;; +-> B +-> B -;; | | -;; A -+-> C -+-> D -> E ===> A -+-> C -> D -> E -;; | -;; +-> F -> G F -> G -(expect - {"A" {"B" {} - "C" {"D" {"E" {}}}} - "F" {"G" {}}} - (with-collection-hierarchy [{:keys [f], :as collections}] - (collection/move-collection! f (collection/children-location collection/root-collection)) - (collection-locations (vals collections)))) - -;; Test that we can move a Collection out of the Root Collection -;; -;; +-> B +-> B -;; | | -;; A -+-> C -+-> D -> E ===> F -+-> A -+-> C -+-> D -> E -;; | | -;; +-> F -> G +-> G -(expect - {"F" {"A" {"B" {} - "C" {"D" {"E" {}}}} - "G" {}}} - (with-collection-hierarchy [{:keys [a f], :as collections}] - (collection/move-collection! f (collection/children-location collection/root-collection)) - (collection/move-collection! a (collection/children-location (Collection (u/get-id f)))) - (collection-locations (vals collections)))) +(deftest move-nested-collections-test + (testing "Make sure the util functions above actually work correctly" + ;; + ;; +-> B + ;; | + ;; A -+-> C -+-> D -> E + ;; | + ;; +-> F -> G + (with-collection-hierarchy [collections] + (is (= {"A" {"B" {} + "C" {"D" {"E" {}} + "F" {"G" {}}}}} + (collection-locations (vals collections)))))) + + (testing "Test that we can move a Collection" + ;; + ;; +-> B +-> B ---> E + ;; | | + ;; A -+-> C -+-> D -> E ===> A -+-> C -+-> D + ;; | | + ;; +-> F -> G +-> F -> G + (with-collection-hierarchy [{:keys [b e], :as collections}] + (collection/move-collection! e (collection/children-location b)) + (is (= {"A" {"B" {"E" {}} + "C" {"D" {} + "F" {"G" {}}}}} + (collection-locations (vals collections)))))) + + (testing "Test that we can move a Collection and its descendants get moved as well" + ;; + ;; +-> B +-> B ---> D -> E + ;; | | + ;; A -+-> C -+-> D -> E ===> A -+-> C -+ + ;; | | + ;; +-> F -> G +-> F -> G + (with-collection-hierarchy [{:keys [b d], :as collections}] + (collection/move-collection! d (collection/children-location b)) + (is (= {"A" {"B" {"D" {"E" {}}} + "C" {"F" {"G" {}}}}} + (collection-locations (vals collections)))))) + + (testing "Test that we can move a Collection into the Root Collection" + ;; + ;; +-> B +-> B + ;; | | + ;; A -+-> C -+-> D -> E ===> A -+-> C -> D -> E + ;; | + ;; +-> F -> G F -> G + (with-collection-hierarchy [{:keys [f], :as collections}] + (collection/move-collection! f (collection/children-location collection/root-collection)) + (is (= {"A" {"B" {} + "C" {"D" {"E" {}}}} + "F" {"G" {}}} + (collection-locations (vals collections)))))) + + (testing "Test that we can move a Collection out of the Root Collection" + ;; + ;; +-> B +-> B + ;; | | + ;; A -+-> C -+-> D -> E ===> F -+-> A -+-> C -+-> D -> E + ;; | | + ;; +-> F -> G +-> G + (with-collection-hierarchy [{:keys [a f], :as collections}] + (collection/move-collection! f (collection/children-location collection/root-collection)) + (collection/move-collection! a (collection/children-location (Collection (u/get-id f)))) + (is (= {"F" {"A" {"B" {} + "C" {"D" {"E" {}}}} + "G" {}}} + (collection-locations (vals collections))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Nested Collections: Archiving/Unarchiving | ;;; +----------------------------------------------------------------------------------------------------------------+ -;; Make sure the 'additional-conditions' for collection-locations is working normally -(expect - {"A" {"B" {} - "C" {"D" {"E" {}} - "F" {"G" {}}}}} - (with-collection-hierarchy [collections] - (collection-locations (vals collections) :archived false))) - (deftest nested-collections-archiving-test + (testing "Make sure the 'additional-conditions' for collection-locations is working normally" + (with-collection-hierarchy [collections] + (is (= {"A" {"B" {} + "C" {"D" {"E" {}} + "F" {"G" {}}}}} + (collection-locations (vals collections) :archived false))))) + (testing "Test that we can archive a Collection with no descendants!" ;; +-> B +-> B ;; | | @@ -1153,63 +1129,56 @@ (is (= #{(perms/collection-readwrite-path root-collection)} (group->perms [collection] group)))))))))) -;; Make sure that when creating a new Collection as a child of another, we copy the group permissions for its parent -(expect - #{"/collection/{parent}/" - "/collection/{child}/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [parent {:name "{parent}"}]] - (perms/grant-collection-readwrite-permissions! group parent) - (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] - (group->perms [parent child] group)))) - -(expect - #{"/collection/{parent}/read/" - "/collection/{child}/read/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [parent {:name "{parent}"}]] - (perms/grant-collection-read-permissions! group parent) - (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] - (group->perms [parent child] group)))) - -(expect - #{} - (mt/with-temp* [PermissionsGroup [group] - Collection [parent {:name "{parent}"}] - Collection [child {:name "{child}", :location (collection/children-location parent)}]] - (group->perms [parent child] group))) - -(expect - #{"/collection/{parent}/read/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [parent {:name "{parent}"}] - Collection [child {:name "{child}", :location (collection/children-location parent)}]] - (perms/grant-collection-read-permissions! group parent) - (group->perms [parent child] group))) - -;; If we have Root Collection perms they shouldn't be copied for a Child -(expect - #{"/collection/root/read/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [parent {:name "{parent}"}]] - (perms/grant-collection-read-permissions! group collection/root-collection) - (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] - (group->perms [parent child] group)))) - -;; Make sure that when creating a new Collection as child of a Personal Collection, no group permissions are created -(expect - false - (mt/with-temp Collection [child {:name "{child}", :location (lucky-collection-children-location)}] - (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))]))) - -;; Make sure that when creating a new Collection as grandchild of a Personal Collection, no group permissions are -;; created -(expect - false - (mt/with-temp* [Collection [child {:location (lucky-collection-children-location)}] - Collection [grandchild {:location (collection/children-location child)}]] - (or (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))]) - (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id grandchild))])))) +(deftest copy-parent-permissions-test + (testing "Make sure that when creating a new child Collection, we copy the group permissions for its parent" + (mt/with-temp PermissionsGroup [group] + (testing "parent has readwrite permissions" + (mt/with-temp Collection [parent {:name "{parent}"}] + (perms/grant-collection-readwrite-permissions! group parent) + (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] + (is (= #{"/collection/{parent}/" + "/collection/{child}/"} + (group->perms [parent child] group)))))) + + (testing "parent has read permissions" + (mt/with-temp Collection [parent {:name "{parent}"}] + (perms/grant-collection-read-permissions! group parent) + (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] + (is (= #{"/collection/{parent}/read/" + "/collection/{child}/read/"} + (group->perms [parent child] group)))))) + + (testing "parent has no permissions" + (mt/with-temp* [Collection [parent {:name "{parent}"}] + Collection [child {:name "{child}", :location (collection/children-location parent)}]] + (is (= #{} + (group->perms [parent child] group))))) + + (testing "parent given read permissions after the fact -- should not update existing children" + (mt/with-temp* [Collection [parent {:name "{parent}"}] + Collection [child {:name "{child}", :location (collection/children-location parent)}]] + (perms/grant-collection-read-permissions! group parent) + (is (= #{"/collection/{parent}/read/"} + (group->perms [parent child] group))))) + + (testing "If we have Root Collection perms they shouldn't be copied for a Child" + (mt/with-temp Collection [parent {:name "{parent}"}] + (perms/grant-collection-read-permissions! group collection/root-collection) + (mt/with-temp Collection [child {:name "{child}", :location (collection/children-location parent)}] + (is (= #{"/collection/root/read/"} + (group->perms [parent child] group))))))) + + (testing (str "Make sure that when creating a new Collection as child of a Personal Collection, no group " + "permissions are created") + (mt/with-temp Collection [child {:name "{child}", :location (lucky-collection-children-location)}] + (is (not (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))]))))) + + (testing (str "Make sure that when creating a new Collection as grandchild of a Personal Collection, no group " + "permissions are created") + (mt/with-temp* [Collection [child {:location (lucky-collection-children-location)}] + Collection [grandchild {:location (collection/children-location child)}]] + (is (not (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id child))]))) + (is (not (db/exists? Permissions :object [:like (format "/collection/%d/%%" (u/get-id grandchild))]))))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -1256,165 +1225,145 @@ ;;; --------------------------------------------- Personal -> Impersonal --------------------------------------------- -;; When moving a Collection from a Personal Collection to the Root Collection, we should create perms entries that -;; match the Root Collection's entries for any groups that have Root Collection perms. -;; -;; Personal Collection > A Personal Collection -;; ===> -;; Root Collection Root Collection > A -(expect - #{"/collection/root/read/" - "/collection/A/read/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}]] - (perms/grant-collection-read-permissions! group collection/root-collection) - (db/update! Collection (u/get-id a) :location (collection/children-location collection/root-collection)) - (group->perms [a] group))) - -;; When moving a Collection from a *descendant* of a Personal Collection to the Root Collection, we should create -;; perms entries that match the Root Collection's entries for any groups that have Root Collection perms. -;; -;; Personal Collection > A > B Personal Collection > A -;; ===> -;; Root Collection Root Collection > B -(expect - #{"/collection/root/" - "/collection/B/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location a)}]] - (perms/grant-collection-readwrite-permissions! group collection/root-collection) - (db/update! Collection (u/get-id b) :location (collection/children-location collection/root-collection)) - (group->perms [a b] group))) - -;; When moving a Collection from a Personal Collection to a non-personal Collection, we should create perms entries -;; that match the Root Collection's entries for any groups that have Root Collection perms. -;; -;; Personal Collection > A Personal Collection -;; ===> -;; Root Collection > B Root Collection > B > A -(expect - #{"/collection/A/read/" - "/collection/B/read/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location collection/root-collection)}]] - (perms/grant-collection-read-permissions! group b) - (db/update! Collection (u/get-id a) :location (collection/children-location b)) - (group->perms [a b] group))) - -;; When moving a Collection from a *descendant* of a Personal Collection to a non-personal Collection, we should -;; create perms entries that match the Root Collection's entries for any groups that have Root Collection perms. -;; -;; Personal Collection > A > B Personal Collection > A -;; ===> -;; Root Collection > C Root Collection > C > B -(expect - #{"/collection/B/" - "/collection/C/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location a)}] - Collection [c {:name "C", :location (collection/children-location collection/root-collection)}]] - (perms/grant-collection-readwrite-permissions! group c) - (db/update! Collection (u/get-id b) :location (collection/children-location c)) - (group->perms [a b c] group))) - -;; Perms should apply recursively as well... -;; -;; Personal Collection > A > B Personal Collection -;; ===> -;; Root Collection > C Root Collection > C > A > B -(expect - #{"/collection/A/" - "/collection/B/" - "/collection/C/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location a)}] - Collection [c {:name "C", :location (collection/children-location collection/root-collection)}]] - (perms/grant-collection-readwrite-permissions! group c) - (db/update! Collection (u/get-id a) :location (collection/children-location c)) - (group->perms [a b c] group))) +(defmacro ^:private with-collection-hierarchy-in [parent-location [collection-symb & more] & body] + (if-not collection-symb + `(do ~@body) + `(mt/with-temp Collection [~collection-symb {:name ~(str/upper-case (name collection-symb)) + :location ~parent-location}] + (println "CREATE" ~(name collection-symb) "IN" (pr-str ~parent-location)) ; NOCOMMIT + (with-collection-hierarchy-in (collection/children-location ~collection-symb) ~more ~@body)))) + +(defmacro ^:private with-personal-and-impersonal-collections {:style/indent 1} + [[group-binding personal-and-root-collection-bindings] & body] + (let [collections (zipmap (vals personal-and-root-collection-bindings) + (keys personal-and-root-collection-bindings))] + `(mt/with-temp PermissionsGroup [~group-binding] + (with-collection-hierarchy-in (lucky-collection-children-location) ~(:personal collections) + (with-collection-hierarchy-in (collection/children-location collection/root-collection) ~(:root collections) + ~@body))))) + +(deftest move-from-personal-to-impersonal-test + (testing "Moving a Collection" + (testing "from a Personal Collection" + (testing (str "to the Root Collection, we should create perms entries that match the Root Collection's entries " + "for any groups that have Root Collection perms.") + ;; Personal Collection > A Personal Collection + ;; ===> + ;; Root Collection Root Collection > A + (with-personal-and-impersonal-collections [group {[a] :personal}] + (perms/grant-collection-read-permissions! group collection/root-collection) + (db/update! Collection (u/get-id a) :location (collection/children-location collection/root-collection)) + (is (= #{"/collection/root/read/" + "/collection/A/read/"} + (group->perms [a] group))))) + + (testing (str "to a non-personal Collection, we should create perms entries that match the Root Collection's " + "entries for any groups that have Root Collection perms.") + ;; Personal Collection > A Personal Collection + ;; ===> + ;; Root Collection > B Root Collection > B > A + (with-personal-and-impersonal-collections [group {[a] :personal, [b] :root}] + (perms/grant-collection-read-permissions! group b) + (db/update! Collection (u/get-id a) :location (collection/children-location b)) + (is (= #{"/collection/A/read/" + "/collection/B/read/"} + (group->perms [a b] group)))))) + + (testing "from a descendant of a Personal Collection" + (testing (str "to the Root Collection, we should create perms entries that match the Root Collection's entries " + "for any groups that have Root Collection perms.") + ;; Personal Collection > A > B Personal Collection > A + ;; ===> + ;; Root Collection Root Collection > B + (with-personal-and-impersonal-collections [group {[a b] :personal}] + (perms/grant-collection-readwrite-permissions! group collection/root-collection) + (db/update! Collection (u/get-id b) :location (collection/children-location collection/root-collection)) + (is (= #{"/collection/root/" + "/collection/B/"} + (group->perms [a b] group))))) + + (testing (str "to a non-personal Collection, we should create perms entries that match the Root Collection's " + "entries for any groups that have Root Collection perms.") + ;; Personal Collection > A > B Personal Collection > A + ;; ===> + ;; Root Collection > C Root Collection > C > B + (with-personal-and-impersonal-collections [group {[a b] :personal, [c] :root}] + (perms/grant-collection-readwrite-permissions! group c) + (db/update! Collection (u/get-id b) :location (collection/children-location c)) + (is (= #{"/collection/B/" + "/collection/C/"} + (group->perms [a b c] group))))))) + + (testing "Perms should apply recursively as well..." + ;; Personal Collection > A > B Personal Collection + ;; ===> + ;; Root Collection > C Root Collection > C > A > B + (with-personal-and-impersonal-collections [group {[a b] :personal, [c] :root}] + (perms/grant-collection-readwrite-permissions! group c) + (db/update! Collection (u/get-id a) :location (collection/children-location c)) + (is (= #{"/collection/A/" + "/collection/B/" + "/collection/C/"} + (group->perms [a b c] group)))))) ;;; --------------------------------------------- Impersonal -> Personal --------------------------------------------- -;; When moving a Collection from Root to a Personal Collection, we should *delete* perms entries for it -;; -;; Personal Collection Personal Collection > A -;; ===> -;; Root Collection > A Root Collection -(expect - #{} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (collection/children-location collection/root-collection)}]] - (perms/grant-collection-readwrite-permissions! group a) - (db/update! Collection (u/get-id a) :location (lucky-collection-children-location)) - (group->perms [a] group))) - -;; When moving a Collection from a non-Personal Collection to a Personal Collection, we should *delete* perms entries -;; for it -;; -;; Personal Collection Personal Collection > B -;; ===> -;; Root Collection > A > B Root Collection > A -(expect - #{"/collection/A/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (collection/children-location collection/root-collection)}] - Collection [b {:name "B", :location (collection/children-location a)}]] - (perms/grant-collection-readwrite-permissions! group a) - (perms/grant-collection-readwrite-permissions! group b) - (db/update! Collection (u/get-id b) :location (lucky-collection-children-location)) - (group->perms [a b] group))) - -;; When moving a Collection from Root to a descendant of a Personal Collection, we should *delete* perms entries for it -;; -;; Personal Collection > A Personal Collection > A > B -;; ===> -;; Root Collection > B Root Collection -(expect - #{} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location collection/root-collection)}]] - (perms/grant-collection-readwrite-permissions! group b) - (db/update! Collection (u/get-id b) :location (collection/children-location a)) - (group->perms [a b] group))) - -;; When moving a Collection from a non-Personal Collection to a descendant of a Personal Collection, we should -;; *delete* perms entries for it -;; -;; Personal Collection > A Personal Collection > A > C -;; ===> -;; Root Collection > B > C Root Collection > B -(expect - #{"/collection/B/"} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location collection/root-collection)}] - Collection [c {:name "C", :location (collection/children-location b)}]] - (perms/grant-collection-readwrite-permissions! group b) - (perms/grant-collection-readwrite-permissions! group c) - (db/update! Collection (u/get-id c) :location (collection/children-location a)) - (group->perms [a b c] group))) - -;; Deleting perms should apply recursively as well... -;; -;; Personal Collection > A Personal Collection > A > B > C -;; ===> -;; Root Collection > B > C Root Collection -(expect - #{} - (mt/with-temp* [PermissionsGroup [group] - Collection [a {:name "A", :location (lucky-collection-children-location)}] - Collection [b {:name "B", :location (collection/children-location collection/root-collection)}] - Collection [c {:name "C", :location (collection/children-location b)}]] - (perms/grant-collection-readwrite-permissions! group b) - (perms/grant-collection-readwrite-permissions! group c) - (db/update! Collection (u/get-id b) :location (collection/children-location a)) - (group->perms [a b c] group))) +(deftest move-from-impersonal-to-personal-test + (testing "Moving a Collection" + (testing "from Root" + (testing "to a Personal Collection, we should *delete* perms entries for it" + ;; Personal Collection Personal Collection > A + ;; ===> + ;; Root Collection > A Root Collection + (with-personal-and-impersonal-collections [group {[a] :root}] + (perms/grant-collection-readwrite-permissions! group a) + (db/update! Collection (u/get-id a) :location (lucky-collection-children-location)) + (is (= #{} + (group->perms [a] group))))) + (testing "to a descendant of a Personal Collection, we should *delete* perms entries for it" + ;; Personal Collection > A Personal Collection > A > B + ;; ===> + ;; Root Collection > B Root Collection + (with-personal-and-impersonal-collections [group {[a] :personal, [b] :root}] + (perms/grant-collection-readwrite-permissions! group b) + (db/update! Collection (u/get-id b) :location (collection/children-location a)) + (is (= #{} + (group->perms [a b] group)))))) + + (testing "from a non-Personal Collection" + (testing "to a Personal Collection, we should *delete* perms entries for it" + ;; Personal Collection Personal Collection > B + ;; ===> + ;; Root Collection > A > B Root Collection > A + (with-personal-and-impersonal-collections [group {[a b] :root}] + (perms/grant-collection-readwrite-permissions! group a) + (perms/grant-collection-readwrite-permissions! group b) + (db/update! Collection (u/get-id b) :location (lucky-collection-children-location)) + (is (= #{"/collection/A/"} + (group->perms [a b] group))))) + + (testing "to a descendant of a Personal Collection, we should *delete* perms entries for it" + ;; Personal Collection > A Personal Collection > A > C + ;; ===> + ;; Root Collection > B > C Root Collection > B + (with-personal-and-impersonal-collections [group {[a] :personal, [b c] :root}] + (perms/grant-collection-readwrite-permissions! group b) + (perms/grant-collection-readwrite-permissions! group c) + (db/update! Collection (u/get-id c) :location (collection/children-location a)) + (is (= #{"/collection/B/"} + (group->perms [a b c] group))))))) + + (testing "Deleting perms should apply recursively as well..." + ;; Personal Collection > A Personal Collection > A > B > C + ;; ===> + ;; Root Collection > B > C Root Collection + (with-personal-and-impersonal-collections [group {[a] :personal, [b c] :root}] + (perms/grant-collection-readwrite-permissions! group b) + (perms/grant-collection-readwrite-permissions! group c) + (db/update! Collection (u/get-id b) :location (collection/children-location a)) + (is (= #{} + (group->perms [a b c] group)))))) (deftest valid-location-path?-test (doseq [[path expected] {nil false @@ -1557,3 +1506,15 @@ {:name "a", :location "/", :id 2}] (collection/collections->tree [{:name nil, :location "/", :id 1} {:name "a", :location "/", :id 2}]))))) + +(deftest collections->tree-missing-parents-test + (testing "collections->tree should 'pull' Collections up to a higher level if their parent isn't present (#14114)" + ;; Imagine a hierarchy like: + ;; + ;; + Our analytics (All Users group has no perms) + ;; +--+ [1] Parent Collection (All Users group has no perms) + ;; +--+ [2] Child Collection (All Users group has readwrite perms) + ;; +--+ [3] Grandchild collection (All Users group has readwrite perms) + (is (= [{:name "Child", :location "/1/", :id 2, :children [{:name "Grandchild", :location "/1/2/", :id 3}]}] + (collection/collections->tree [{:name "Child", :location "/1/", :id 2} + {:name "Grandchild", :location "/1/2/", :id 3} ]))))) -- GitLab