From a1894202ed1d5a2e80221e15e797f435ae2eab22 Mon Sep 17 00:00:00 2001 From: metamben <103100869+metamben@users.noreply.github.com> Date: Wed, 19 Oct 2022 08:39:48 +0300 Subject: [PATCH] Use "apps" collection namespace for app collections (#25963) * Create app collections in the "apps" collection namespace * Add test macro for setting global app permissions for "All Users" * Introduce app permission audit table --- .clj-kondo/config.edn | 1 + .../models/permissions/app_permissions.clj | 118 ++++------ .../permissions/app_permissions_test.clj | 134 ++++++++--- .../models/entity_id_test.clj | 1 + resources/migrations/000_migrations.yaml | 49 ++++ src/metabase/api/app.clj | 23 +- src/metabase/api/collection.clj | 7 +- src/metabase/api/search.clj | 4 +- src/metabase/cmd/copy.clj | 2 + src/metabase/models.clj | 3 + src/metabase/models/app.clj | 14 +- src/metabase/models/app/graph.clj | 65 ++---- .../models/app_permission_graph_revision.clj | 22 ++ src/metabase/models/collection.clj | 24 +- src/metabase/models/collection/graph.clj | 33 ++- src/metabase/models/permissions.clj | 11 +- test/metabase/api/activity_test.clj | 4 +- test/metabase/api/app_test.clj | 218 ++++++++++-------- test/metabase/api/bookmark_test.clj | 93 ++++---- test/metabase/api/collection_test.clj | 146 +++++++----- test/metabase/api/search_test.clj | 36 +-- test/metabase/models/card_test.clj | 4 +- .../metabase/models/collection/graph_test.clj | 5 +- test/metabase/models/collection_test.clj | 2 +- test/metabase/models/dashboard_test.clj | 4 +- test/metabase/models/pulse_test.clj | 4 +- test/metabase/test.clj | 1 + test/metabase/test/util.clj | 19 ++ 28 files changed, 624 insertions(+), 423 deletions(-) create mode 100644 src/metabase/models/app_permission_graph_revision.clj diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 07a753e529a..8645d909489 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -455,6 +455,7 @@ metabase.api.public-test/with-temp-public-dashboard hooks.common/let-one-with-optional-value metabase.api.public-test/with-temp-public-dashboard-and-card hooks.common/with-three-bindings metabase.api.search-test/with-search-items-in-collection hooks.metabase.api.search-test/with-search-items-in-collection + metabase.api.search-test/with-search-items-in-app-collection hooks.metabase.api.search-test/with-search-items-in-collection metabase.api.search-test/with-search-items-in-root-collection hooks.common/do* metabase.api.user-test/with-temp-user-email hooks.common/with-one-binding metabase.async.streaming-response-test/with-start-execution-chan hooks.common/with-one-binding diff --git a/enterprise/backend/src/metabase_enterprise/advanced_permissions/models/permissions/app_permissions.clj b/enterprise/backend/src/metabase_enterprise/advanced_permissions/models/permissions/app_permissions.clj index f590bb204e6..f24d3288a3e 100644 --- a/enterprise/backend/src/metabase_enterprise/advanced_permissions/models/permissions/app_permissions.clj +++ b/enterprise/backend/src/metabase_enterprise/advanced_permissions/models/permissions/app_permissions.clj @@ -3,100 +3,74 @@ on the app collections." (:require [clojure.data :as data] [clojure.set :as set] - [clojure.tools.logging :as log] - [metabase.models.app.graph :as app.graph] - [metabase.models.collection-permission-graph-revision :as c-perm-revision - :refer [CollectionPermissionGraphRevision]] + [metabase.models.app-permission-graph-revision :as app-perm-revision + :refer [AppPermissionGraphRevision]] [metabase.models.collection.graph :as graph] [metabase.models.permissions :as perms] [metabase.public-settings.premium-features :as premium-features] [metabase.util.i18n :as i18n] - [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db])) -(def ^:private GroupPermissionsGraph - "collection-id -> status" - {su/IntGreaterThanZero app.graph/AppPermissions}) ; be present, which is why it's *optional* - -(def ^:private PermissionsGraph - {:revision s/Int - :groups {su/IntGreaterThanZero GroupPermissionsGraph}}) - (defn- check-advanced-permissions [] - (log/fatal "checking advanced permsissions") (when-not (premium-features/has-feature? :advanced-permissions) (throw (ex-info (i18n/tru "The granular app permission functionality is only enabled if you have a premium token with the advanced-permissions feature.") {:status-code 402})))) (defn- replace-collection-ids - "Convert the collection group permission graph `g` into an app group - permission graph or vice versa by removing the root collection and replacing - the collection IDs by app IDs according to `mapping`. - - Note that the app permission graph never contains the root collection. So it's - removed when converting a collection graph to an app graph and plays no role - when converting an app graph to a collection graph." - [g mapping] - (update-vals g (fn [group-permissions] - (-> group-permissions - (dissoc :root) - (update-keys mapping))))) - -(defn- merge-graphs - [base override] - (merge-with merge base override)) + "Convert the collection group permission graph `permission-graph` into an + app group permission graph or vice versa by replacing the collection IDs + by app IDs according to `mapping`. + Stray collections in the :apps namespace are ignored." + [group-permissions mapping] + (let [full-mapping (assoc mapping :root :root)] + (update-vals group-permissions (fn [coll-perms] + (into {} + (for [[coll-id perm] coll-perms + :let [app-id (full-mapping coll-id)] + :when app-id] + [app-id perm])))))) -(s/defn graph :- PermissionsGraph +(s/defn graph :- graph/PermissionsGraph "Returns the app permission graph. Throws an exception if the - advanced-permissions feature is not enabled." + advanced-permissions feature is not enabled. + + This works by reading the permissions for app collections and replacing + the IDs of the collections with the corresponding app IDs." [] (check-advanced-permissions) (db/transaction - (let [collection-id->app-id (set/map-invert (db/select-id->field :collection_id 'App))] - (-> collection-id->app-id - keys - graph/collection-permission-graph - (update :groups replace-collection-ids collection-id->app-id))))) - -(defn update-collection-graph! - "Update the Collections permissions graph for Collections of `collection-namespace` (default `nil`, the 'default' - namespace). This works just like [[metabase.models.permission/update-data-perms-graph!]], but for Collections; - refer to that function's extensive documentation to get a sense for how this works." - [old-graph new-graph] - (let [old-perms (:groups old-graph) - new-perms (:groups new-graph) - ;; filter out any groups not in the old graph - new-perms (select-keys new-perms (keys old-perms)) - ;; filter out any collections not in the old graph - new-perms (into {} (for [[group-id collection-id->perms] new-perms] - [group-id (select-keys collection-id->perms (keys (get old-perms group-id)))])) - [diff-old changes] (data/diff old-perms new-perms)] - (perms/log-permissions-changes diff-old changes) - (perms/check-revision-numbers old-graph new-graph) - (when (seq changes) - (db/transaction - (doseq [[group-id new-group-perms] changes - [collection-id new-perms] new-group-perms] - (graph/update-collection-permissions! nil group-id collection-id new-perms)) - (perms/save-perms-revision! CollectionPermissionGraphRevision (:revision old-graph) - (assoc old-graph :namespace nil) changes))))) + (let [collection-id->app-id (set/map-invert (db/select-id->field :collection_id 'App))] + (-> (graph/graph :apps) + (assoc :revision (app-perm-revision/latest-id)) + (update :groups replace-collection-ids collection-id->app-id))))) -(s/defn update-graph! :- PermissionsGraph +(s/defn update-graph! :- graph/PermissionsGraph "Updates the app permissions according to `new-graph` and returns the resulting graph as read from the database. Throws an exception if the advanced-permissions feature is not enabled." - [new-graph :- PermissionsGraph] + [new-graph :- graph/PermissionsGraph] (check-advanced-permissions) (db/transaction - (let [{:keys [revision groups]} new-graph - app-id->collection-id (db/select-id->field :collection_id 'App) - collection-ids (vals app-id->collection-id) - old-graph (graph/graph) - new-graph (-> old-graph - (assoc :revision revision) - (update :groups update-vals (fn [group-permissions] - (apply dissoc group-permissions collection-ids))) - (update :groups merge-graphs (replace-collection-ids groups app-id->collection-id)))] - (update-collection-graph! old-graph new-graph)) + (let [old-graph (graph) + old-perms (:groups old-graph) + new-perms (:groups new-graph) + ;; filter out any groups not in the old graph + new-perms (select-keys new-perms (keys old-perms)) + ;; filter out any collections not in the old graph + new-perms (into {} (for [[group-id collection-id->perms] new-perms] + [group-id (select-keys collection-id->perms + (keys (get old-perms group-id)))])) + [diff-old changes] (data/diff old-perms new-perms)] + (perms/log-permissions-changes diff-old changes) + (perms/check-revision-numbers old-graph new-graph) + (when (seq changes) + (let [app-id->collection-id + (assoc (db/select-id->field :collection_id 'App) :root :root)] + (doseq [[group-id new-group-perms] changes + [app-id new-perms] new-group-perms + :let [collection-id (app-id->collection-id app-id)]] + (graph/update-collection-permissions! :apps group-id collection-id new-perms)) + (perms/save-perms-revision! AppPermissionGraphRevision (:revision old-graph) + old-graph changes)))) (graph))) diff --git a/enterprise/backend/test/metabase_enterprise/advanced_permissions/models/permissions/app_permissions_test.clj b/enterprise/backend/test/metabase_enterprise/advanced_permissions/models/permissions/app_permissions_test.clj index 10413ff5fda..80d47aa0a4a 100644 --- a/enterprise/backend/test/metabase_enterprise/advanced_permissions/models/permissions/app_permissions_test.clj +++ b/enterprise/backend/test/metabase_enterprise/advanced_permissions/models/permissions/app_permissions_test.clj @@ -1,10 +1,37 @@ (ns metabase-enterprise.advanced-permissions.models.permissions.app-permissions-test "Tests for /api/collection endpoints." (:require [clojure.test :refer :all] - [metabase.models :refer [App Collection PermissionsGroup]] + [metabase.models :refer [App Card Collection Dashboard + Permissions PermissionsGroup PermissionsGroupMembership]] + [metabase.models.interface :as mi] [metabase.models.permissions-group :as perms-group] [metabase.public-settings.premium-features-test :as premium-features-test] - [metabase.test :as mt])) + [metabase.test :as mt] + [metabase.test.data :as data] + [toucan.db :as db])) + +(deftest permission-set-test + (premium-features-test/with-premium-features #{} + (testing "App permission set without advanced permission feature" + (is (= #{"/collection/namespace/apps/root/"} + (mi/perms-objects-set (mi/instance App) :write))) + (is (= #{"/collection/namespace/apps/root/read/"} + (mi/perms-objects-set (mi/instance App) :read))) + (is (= #{"/collection/namespace/apps/root/read/"} + (mi/perms-objects-set (mi/instance App {:collection_id 1}) :read))) + (is (= #{"/collection/namespace/apps/root/"} + (mi/perms-objects-set (mi/instance App {:collection_id 1}) :write))))) + + (premium-features-test/with-premium-features #{:advanced-permissions} + (testing "App permission set with advanced permission feature" + (is (= #{"/collection/namespace/apps/root/"} + (mi/perms-objects-set (mi/instance App) :write))) + (is (= #{"/collection/namespace/apps/root/read/"} + (mi/perms-objects-set (mi/instance App) :read))) + (is (= #{"/collection/1/read/"} + (mi/perms-objects-set (mi/instance App {:collection_id 1}) :read))) + (is (= #{"/collection/1/"} + (mi/perms-objects-set (mi/instance App {:collection_id 1}) :write)))))) (deftest graph-test (testing "GET /api/app/graph works only with advanced permissions" @@ -13,13 +40,13 @@ (mt/user-http-request :crowberto :get 402 "app/graph"))))) (premium-features-test/with-premium-features #{:advanced-permissions} - (mt/with-temp* [Collection [{app-coll-id :id} {:location "/"}] + (mt/with-temp* [Collection [{app-coll-id :id} {:location "/", :namespace :apps}] App [{app-id :id} {:collection_id app-coll-id}] PermissionsGroup [{group-id :id}]] (testing "GET /api/app/graph\n" (testing "Should be able to fetch the permissions graph for apps" (is (partial= {(:id (perms-group/admin)) {app-id "write"} - (:id (perms-group/all-users)) {app-id "write"} + (:id (perms-group/all-users)) {app-id "none"} group-id {app-id "none"}} (:groups (mt/user-http-request :crowberto :get 200 "app/graph"))))) @@ -27,18 +54,19 @@ (is (= "You don't have permissions to do that." (mt/user-http-request :rasta :get 403 "app/graph")))))) - (mt/with-non-admin-groups-no-root-collection-perms - (mt/with-temp* [Collection [_ {:location "/"}] - Collection [{app-coll-id :id} {:location "/"}] - App [{app-id :id} {:collection_id app-coll-id}] - PermissionsGroup [{group-id :id}]] - (testing "All users' right to root collection is respected" - (let [group-perms (:groups (mt/user-http-request :crowberto :get 200 "app/graph"))] - (is (partial= {(:id (perms-group/admin)) {app-id "write"} - (:id (perms-group/all-users)) {app-id "none"} - group-id {app-id "none"}} - group-perms)) - (is (= #{app-id} (into #{} (mapcat keys) (vals group-perms))) + (mt/with-temp* [Collection [_ {:location "/"}] + Collection [{app-coll-id :id} {:location "/", :namespace :apps}] + App [{app-id :id} {:collection_id app-coll-id}] + PermissionsGroup [{group-id :id}]] + (testing "All users' right to root collection is respected" + (let [group-perms (:groups (mt/user-http-request :crowberto :get 200 "app/graph"))] + (is (partial= {(:id (perms-group/admin)) {app-id "write"} + (:id (perms-group/all-users)) {app-id "none"} + group-id {app-id "none"}} + group-perms)) + (let [app-ids (into #{} (mapcat keys) (vals group-perms))] + (is (every? #(contains? app-ids %) [:root app-id])) + (is (not (contains? app-ids app-coll-id)) "Shouldn't confuse collection IDs and app IDs"))))))) (deftest graph-update-test @@ -48,24 +76,60 @@ (mt/user-http-request :crowberto :put 402 "app/graph" {:revision 0 :groups {1 {1 "write"}}}))))) - (premium-features-test/with-premium-features #{:advanced-permissions} - (mt/with-temp* [Collection [{app-coll-id :id} {:location "/"}] - App [{app-id :id} {:collection_id app-coll-id}] - PermissionsGroup [{group-id :id}]] - (testing "PUT /api/app/graph\n" - (testing "Should be able to update the permissions graph for apps" - (let [initial-graph (mt/user-http-request :crowberto :get 200 "app/graph") - updated-graph (assoc-in initial-graph [:groups group-id app-id] "read")] - (is (partial= {(:id (perms-group/admin)) {app-id "write"} - (:id (perms-group/all-users)) {app-id "write"} - group-id {app-id "none"}} - (:groups initial-graph)) - "Unexpected initial state") + (mt/with-model-cleanup [Card Dashboard Collection Permissions] + (premium-features-test/with-premium-features #{:advanced-permissions} + (let [max-coll-id (:id (db/select-one [Collection [:%max.id :id]])) + max-app-id (:id (db/select-one [App [:%max.id :id]]))] + (mt/with-temp* [Collection [{coll0-id :id} {:location "/", :namespace :apps}] + Collection [{coll1-id :id} {:location "/", :namespace :apps}] + ;; make sure that the :id and :collection_id of the app are different + App [{app-id :id :as app} {:collection_id (if (= max-app-id max-coll-id) + coll1-id + coll0-id)}] + PermissionsGroup [{group-id :id}] + PermissionsGroupMembership [_ {:user_id (mt/user->id :rasta) + :group_id group-id}]] + (testing "PUT /api/app/graph\n" + (testing "Initial assumptions should hold" + (let [initial-graph (mt/user-http-request :crowberto :get 200 "app/graph")] + (is (not= app-id (:collection_id app)) + "The IDs of the app and its collection should be different. Fix the test!") + (is (partial= {(:id (perms-group/admin)) {app-id "write"} + (:id (perms-group/all-users)) {app-id "none"} + group-id {app-id "none"}} + (:groups initial-graph)) + "Unexpected initial state"))) + + (testing "Should be able to update the permissions graph for a specific app\n" + (let [initial-graph (mt/user-http-request :crowberto :get 200 "app/graph") + updated-graph (assoc-in initial-graph [:groups group-id app-id] "write")] + (testing "Have to be a superuser" + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :put 403 "app/graph" updated-graph)))) + + (testing "Cannot scaffold an app without write permission" + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :post 403 (format "app/%s/scaffold" app-id) + {:table-ids [(data/id :venues)]})))) + + (testing "Superuser can update permissions for an app" + (is (= (:groups updated-graph) + (:groups (mt/user-http-request :crowberto :put 200 "app/graph" + updated-graph))))) + + (testing "Normal user can scaffold an app with write permission" + (is (partial= (dissoc app :updated_at :nav_items) + (mt/user-http-request :rasta :post 200 (format "app/%s/scaffold" app-id) + {:table-ids [(data/id :venues)]})))))) - (testing "have to be a superuser" - (is (= "You don't have permissions to do that." - (mt/user-http-request :rasta :put 403 "app/graph" updated-graph)))) + (testing "Should be able to update the permissions graph for the root\n" + (let [initial-graph (mt/user-http-request :crowberto :get 200 "app/graph") + updated-graph (assoc-in initial-graph [:groups group-id :root] "read")] + (testing "Have to be a superuser" + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :put 403 "app/graph" updated-graph)))) - (testing "superuser can update" - (is (= (:groups updated-graph) - (:groups (mt/user-http-request :crowberto :put 200 "app/graph" updated-graph))))))))))) + (testing "Superuser can update permissions for the root" + (is (= (:groups updated-graph) + (:groups (mt/user-http-request :crowberto :put 200 "app/graph" + updated-graph))))))))))))) diff --git a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj index d4e0de1ad0b..63f7c65ef11 100644 --- a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj +++ b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj @@ -35,6 +35,7 @@ metabase.models.action.HTTPActionInstance metabase.models.action.QueryActionInstance metabase.models.activity.ActivityInstance + metabase.models.app_permission_graph_revision.AppPermissionGraphRevisionInstance metabase.models.application_permissions_revision.ApplicationPermissionsRevisionInstance metabase.models.bookmark.BookmarkOrderingInstance metabase.models.bookmark.CardBookmarkInstance diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index d230622e796..0433bc62283 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -12993,6 +12993,55 @@ databaseChangeLog: tableName: metabase_database columnName: details + - changeSet: + id: v45.00-044 + author: metamben + comment: Added 0.45.0 -- create app permission graph revision table + changes: + - createTable: + tableName: app_permission_graph_revision + remarks: 'Used to keep track of changes made to app permissions.' + columns: + - column: + name: id + type: int + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: before + type: ${text.type} + remarks: 'Serialized JSON of the apps graph before the changes.' + constraints: + nullable: false + - column: + name: after + type: ${text.type} + remarks: 'Serialized JSON of the apps graph after the changes.' + constraints: + nullable: false + - column: + name: user_id + type: int + remarks: 'The ID of the admin who made this set of changes.' + constraints: + nullable: false + referencedTableName: core_user + referencedColumnNames: id + foreignKeyName: fk_app_permission_graph_revision_user_id + - column: + name: created_at + type: ${timestamp_type} + remarks: 'The timestamp of when these changes were made.' + defaultValueComputed: current_timestamp + constraints: + nullable: false + - column: + name: remark + type: ${text.type} + remarks: 'Optional remarks explaining why these changes were made.' + # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<< ######################################################################################################################## diff --git a/src/metabase/api/app.clj b/src/metabase/api/app.clj index a9fa964d32b..8cf52be81b3 100644 --- a/src/metabase/api/app.clj +++ b/src/metabase/api/app.clj @@ -8,7 +8,7 @@ [metabase.api.collection :as api.collection] [metabase.api.common :as api] [metabase.mbql.schema :as mbql.s] - [metabase.models :refer [App Collection Dashboard ModelAction Table]] + [metabase.models :refer [App Dashboard ModelAction Table]] [metabase.models.app.graph :as app.graph] [metabase.models.collection :as collection] [metabase.models.dashboard :as dashboard] @@ -26,13 +26,13 @@ (defn- create-app! [{:keys [collection] :as app}] (db/transaction - (let [coll-params (select-keys collection [:name :color :description :namespace :authority_level]) - collection-instance (api.collection/create-collection! coll-params) + (let [coll-params (select-keys collection [:name :color :description :authority_level]) + collection-instance (api.collection/create-collection! + (assoc coll-params :namespace :apps)) app-params (-> app (select-keys [:dashboard_id :options :nav_items]) (assoc :collection_id (:id collection-instance))) app (db/insert! App app-params)] - (app.graph/set-default-permissions! app) (hydrate-details app)))) (api/defendpoint POST "/" @@ -57,7 +57,7 @@ dashboard_id (s/maybe su/IntGreaterThanOrEqualToZero) options (s/maybe su/Map) nav_items (s/maybe [(s/maybe su/Map)])} - (api/write-check Collection (db/select-one-field :collection_id App :id app-id)) + (api/write-check App app-id) (db/update! App app-id (select-keys body [:dashboard_id :options :nav_items])) (hydrate-details (db/select-one App :id app-id))) @@ -287,6 +287,7 @@ (api/defendpoint POST "/:app-id/scaffold" "Endpoint to scaffold a new table onto an existing data-app" [app-id :as {{:keys [table-ids]} :body}] + (api/write-check App app-id) (db/transaction (let [{app-id :id app-name :name nav-items :nav_items {collection-id :id} :collection} (hydrate-details (db/select-one App :id app-id)) ;; We can scaffold this as a new app, but use the existing collection-id and nav-items to merge into the existing app @@ -328,12 +329,12 @@ (let [graph (resolve-advanced-app-permission-function 'graph)] (graph))) -(defn- ->int [id] (Integer/parseInt (name id))) +(defn- ->int [id] (parse-long (name id))) (defn- dejsonify-with [f m] (into {} (map (fn [[k v]] - [(->int k) (f v)])) + [(or (->int k) k) (f v)])) m)) (defn- dejsonify-id->permission-map [m] @@ -342,12 +343,6 @@ (defn- dejsonify-groups-map [m] (dejsonify-with dejsonify-id->permission-map m)) -(defn- dejsonify-global-graph - "Fix the types in the graph when it comes in from the API, e.g. converting things like `\"none\"` to `:none` and - parsing object keys as integers." - [graph] - (update graph :groups dejsonify-id->permission-map)) - (defn- dejsonify-graph "Fix the types in the graph when it comes in from the API, e.g. converting things like `\"none\"` to `:none` and parsing object keys as integers." @@ -360,7 +355,7 @@ {body su/Map} (api/check-superuser) (-> body - dejsonify-global-graph + dejsonify-graph app.graph/update-global-graph!)) (api/defendpoint PUT "/graph" diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj index 40df45f6f9b..f5456e69e1f 100644 --- a/src/metabase/api/collection.clj +++ b/src/metabase/api/collection.clj @@ -760,16 +760,17 @@ (defn- write-check-collection-or-root-collection "Check that you're allowed to write Collection with `collection-id`; if `collection-id` is `nil`, check that you have Root Collection perms." - [collection-id] + [collection-id collection-namespace] (api/write-check (if collection-id (db/select-one Collection :id collection-id) - collection/root-collection))) + (cond-> collection/root-collection + collection-namespace (assoc :namespace collection-namespace))))) (defn create-collection! "Create a new collection." [{:keys [name color description parent_id namespace authority_level]}] ;; To create a new collection, you need write perms for the location you are going to be putting it in... - (write-check-collection-or-root-collection parent_id) + (write-check-collection-or-root-collection parent_id namespace) ;; Now create the new Collection :) (api/check-403 (or (nil? authority_level) (and api/*is-superuser?* authority_level))) diff --git a/src/metabase/api/search.clj b/src/metabase/api/search.clj index 2b1fa4a9502..4f5e536d632 100644 --- a/src/metabase/api/search.clj +++ b/src/metabase/api/search.clj @@ -230,7 +230,9 @@ visible-collections) honeysql-query (-> honeysql-query (hh/merge-where collection-filter-clause) - (hh/merge-where [:= :collection.namespace nil]))] + (hh/merge-where [:or + [:= :collection.namespace nil] + [:= :collection.namespace "apps"]]))] ;; add a JOIN against Collection *unless* the source table is already Collection (cond-> honeysql-query (not= collection-id-column :collection.id) diff --git a/src/metabase/cmd/copy.clj b/src/metabase/cmd/copy.clj index 2e06bc50a2f..d92e523152b 100644 --- a/src/metabase/cmd/copy.clj +++ b/src/metabase/cmd/copy.clj @@ -11,6 +11,7 @@ [metabase.db.setup :as mdb.setup] [metabase.models :refer [Activity App + AppPermissionGraphRevision ApplicationPermissionsRevision BookmarkOrdering Card @@ -107,6 +108,7 @@ DashboardCardSeries Activity App + AppPermissionGraphRevision Pulse PulseCard PulseChannel diff --git a/src/metabase/models.clj b/src/metabase/models.clj index 12e0ad9dd55..bc8857cfcba 100644 --- a/src/metabase/models.clj +++ b/src/metabase/models.clj @@ -2,6 +2,7 @@ (:require [metabase.models.action :as action] [metabase.models.activity :as activity] [metabase.models.app :as app] + [metabase.models.app-permission-graph-revision :as app-perm-revision] [metabase.models.application-permissions-revision :as a-perm-revision] [metabase.models.bookmark :as bookmark] [metabase.models.card :as card] @@ -48,6 +49,7 @@ (comment action/keep-me activity/keep-me app/keep-me + app-perm-revision/keep-me card/keep-me bookmark/keep-me collection/keep-me @@ -93,6 +95,7 @@ [action Action HTTPAction ModelAction QueryAction] [activity Activity] [app App] + [app-perm-revision AppPermissionGraphRevision] [bookmark CardBookmark] [bookmark DashboardBookmark] [bookmark CollectionBookmark] diff --git a/src/metabase/models/app.clj b/src/metabase/models/app.clj index 52bb6fa2408..e49f55c78ba 100644 --- a/src/metabase/models/app.clj +++ b/src/metabase/models/app.clj @@ -1,5 +1,6 @@ (ns metabase.models.app - (:require [metabase.models.permissions :as perms] + (:require [metabase.models.collection :as collection :refer [Collection]] + [metabase.models.interface :as mi] [metabase.models.query :as query] [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] @@ -9,7 +10,16 @@ (models/defmodel App :app) ;;; You can read/write an App if you can read/write its Collection -(derive App ::perms/use-parent-collection-perms) +(doto App + (derive ::mi/read-policy.full-perms-for-perms-set) + (derive ::mi/write-policy.full-perms-for-perms-set)) + +(defmethod mi/perms-objects-set App + [instance read-or-write] + (let [collection (if-let [collection-id (:collection_id instance)] + (mi/instance Collection {:id collection-id, :namespace :apps}) + (assoc collection/root-collection :namespace :apps))] + (mi/perms-objects-set collection read-or-write))) (u/strict-extend #_{:clj-kondo/ignore [:metabase/disallow-class-or-type-on-model]} (class App) models/IModel diff --git a/src/metabase/models/app/graph.clj b/src/metabase/models/app/graph.clj index 0cabeeccc96..c4d2cb9d666 100644 --- a/src/metabase/models/app/graph.clj +++ b/src/metabase/models/app/graph.clj @@ -2,54 +2,38 @@ "Code for generating and updating the App permissions graph. App permissions are based on the permissions of the app's collection." (:require [clojure.data :as data] - [metabase.models.collection-permission-graph-revision :as c-perm-revision - :refer [CollectionPermissionGraphRevision]] + [metabase.models.app-permission-graph-revision :as app-perm-revision + :refer [AppPermissionGraphRevision]] [metabase.models.collection.graph :as graph] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] - [metabase.models.setting :as setting :refer [defsetting]] - [metabase.public-settings.premium-features :as premium-features] [metabase.util.i18n :as i18n :refer [tru]] [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db])) -(def AppPermissions +(def RootPermissions "The valid app permissions. Currently corresponds 1:1 to `CollectionPermissions` since app permissions are implemented in terms of collection permissions." - graph/CollectionPermissions) + {:root graph/CollectionPermissions}) (def ^:private GlobalPermissionsGraph {:revision s/Int - :groups {su/IntGreaterThanZero AppPermissions}}) - -(s/defn ^:private set-all-users-app-permission! - [permission :- AppPermissions] - (setting/set-value-of-type! :keyword :all-users-app-permission permission)) - -(defsetting all-users-app-permission - "App permission of the All Users group" - :type :keyword - :visibility :internal - :default :none - :setter (fn [v] - (set-all-users-app-permission! (cond-> v (string? v) keyword)))) - -(defn set-default-permissions! - "Sets the default permissions for the ''All Users'' group on`app` as specified - by `all-users-app-permission` if advanced permissions are not available." - [app] - (when-not (premium-features/has-feature? :advanced-permissions) - (graph/update-collection-permissions! (:id (perms-group/all-users)) - (:collection_id app) - (all-users-app-permission)))) + :groups {su/IntGreaterThanZero RootPermissions}}) (s/defn global-graph :- GlobalPermissionsGraph - "Fetch the global app permission graph." + "Fetch the global app permission graph. + + This works by reading the permissions for app collections, restricting + the groups to admin and ''All Users'' and the collections to the root." [] - {:revision (c-perm-revision/latest-id) - :groups {(:id (perms-group/admin)) :write - (:id (perms-group/all-users)) (all-users-app-permission)}}) + (-> (graph/graph :apps) + (assoc :revision (app-perm-revision/latest-id)) + (update :groups (fn [group-perms] + (-> group-perms + (select-keys [(:id (perms-group/admin)) + (:id (perms-group/all-users))]) + (update-vals #(select-keys % [:root]))))))) (s/defn update-global-graph! "Update the global app permission graph to the state specified by @@ -59,17 +43,16 @@ [diff-old changes] (data/diff (:groups old-graph) (:groups new-graph))] (perms/log-permissions-changes diff-old changes) (perms/check-revision-numbers old-graph new-graph) - (when-let [[[group-id permission] & other-groups] (seq changes)] - (when (or (not= group-id (:id (perms-group/all-users))) + (when-let [[[all-users-group-id [[root permission] & other-colls]] & other-groups] (seq changes)] + (when (or (not= all-users-group-id (:id (perms-group/all-users))) + (not= root :root) + (seq other-colls) (seq other-groups)) - (throw (ex-info (tru "You can configure for the ''All Users'' group only") + (throw (ex-info (tru "You can configure permissions only on the root and only for the ''All Users'' group") {:group-ids (keys changes) :status-code 400}))) (db/transaction - (when (not= permission (all-users-app-permission)) - (all-users-app-permission! permission) - (doseq [collection-id (db/select-field :collection_id 'App)] - (graph/update-collection-permissions! group-id collection-id permission)) - (perms/save-perms-revision! - CollectionPermissionGraphRevision (:revision old-graph) old-graph changes)) + (graph/update-collection-permissions! :apps all-users-group-id root permission) + (perms/save-perms-revision! AppPermissionGraphRevision (:revision old-graph) + old-graph changes) (global-graph))))) diff --git a/src/metabase/models/app_permission_graph_revision.clj b/src/metabase/models/app_permission_graph_revision.clj new file mode 100644 index 00000000000..8ec3b81de07 --- /dev/null +++ b/src/metabase/models/app_permission_graph_revision.clj @@ -0,0 +1,22 @@ +(ns metabase.models.app-permission-graph-revision + (:require [metabase.util :as u] + [metabase.util.i18n :refer [tru]] + [toucan.db :as db] + [toucan.models :as models])) + +(models/defmodel AppPermissionGraphRevision :app_permission_graph_revision) + +(u/strict-extend #_{:clj-kondo/ignore [:metabase/disallow-class-or-type-on-model]} (class AppPermissionGraphRevision) + models/IModel + (merge models/IModelDefaults + {:types (constantly {:before :json + :after :json}) + :properties (constantly {:created-at-timestamped? true}) + :pre-update (fn [& _] (throw (Exception. (tru "You cannot update an AppPermissionGraphRevision!"))))})) + +(defn latest-id + "Return the ID of the newest `AppPermissionGraphRevision`, or zero if none have been made yet. + (This is used by the app graph update logic that checks for changes since the original graph was fetched)." + [] + (or (:id (db/select-one [AppPermissionGraphRevision [:%max.id :id]])) + 0)) diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj index 304d14fb422..01849434b52 100644 --- a/src/metabase/models/collection.clj +++ b/src/metabase/models/collection.clj @@ -192,6 +192,7 @@ (m/assoc-some root-collection :name (case (keyword collection-namespace) :snippets (tru "Top folder") + :apps (tru "All apps") (tru "Our analytics")) :namespace collection-namespace :id "root")) @@ -871,14 +872,25 @@ (let [collection (if (integer? collection-or-id) (db/select-one [Collection :id :namespace] :id (collection-or-id)) collection-or-id)] - ;; HACK Collections in the "snippets" namespace have no-op permissions unless EE enhancements are enabled - ;; - ;; TODO -- Pretty sure snippet perms should be feature flagged by `advanced-permissions` instead - (if (and (= (u/qualified-name (:namespace collection)) "snippets") - (not (premium-features/enable-enhancements?))) + (cond + ;; HACK Collections in the "snippets" namespace have no-op permissions unless EE enhancements are enabled + ;; + ;; TODO -- Pretty sure snippet perms should be feature flagged by `advanced-permissions` instead + (and (= (u/qualified-name (:namespace collection)) "snippets") + (not (premium-features/enable-enhancements?))) #{} + + ;; Collections in the "apps" namespace use the permissions of the root unless advanced permissions are enabled + (and (= (u/qualified-name (:namespace collection)) "apps") + (not (premium-features/has-feature? :advanced-permissions))) + #{(let [root (assoc root-collection :namespace :apps)] + (case read-or-write + :read (perms/collection-read-path root) + :write (perms/collection-readwrite-path root)))} + ;; This is not entirely accurate as you need to be a superuser to modifiy a collection itself (e.g., changing its ;; name) but if you have write perms you can add/remove cards + :else #{(case read-or-write :read (perms/collection-read-path collection-or-id) :write (perms/collection-readwrite-path collection-or-id))}))) @@ -1131,7 +1143,7 @@ (defmethod allowed-namespaces :default [_] - #{nil}) + #{nil :apps}) (defn check-collection-namespace "Check that object's `:collection_id` refers to a Collection in an allowed namespace (see diff --git a/src/metabase/models/collection/graph.clj b/src/metabase/models/collection/graph.clj index 777fa69e590..6e9c6891b65 100644 --- a/src/metabase/models/collection/graph.clj +++ b/src/metabase/models/collection/graph.clj @@ -29,7 +29,8 @@ {(s/optional-key :root) CollectionPermissions ; when doing a delta between old graph and new graph root won't always su/IntGreaterThanZero CollectionPermissions}) ; be present, which is why it's *optional* -(def ^:private PermissionsGraph +(def PermissionsGraph + "A versioned map of group permissions. See [[GroupPermissionsGraph]]." {:revision s/Int :groups {su/IntGreaterThanZero GroupPermissionsGraph}}) @@ -69,7 +70,7 @@ [:not [:like :location (hx/literal (format "/%d/%%" collection-id))]]))}] (set (map :id (db/query honeysql-form))))) -(defn collection-permission-graph +(defn- collection-permission-graph "Return the permission graph for the collections with id in `collection-ids` and the root collection." ([collection-ids] (collection-permission-graph collection-ids nil)) ([collection-ids collection-namespace] @@ -120,21 +121,19 @@ (s/defn update-collection-permissions! "Update the permissions for group ID with `group-id` on collection with ID `collection-id` in the optional `collection-namespace` to `new-collection-perms`." - ([group-id collection-id new-collection-perms] - (update-collection-permissions! nil group-id collection-id new-collection-perms)) - ([collection-namespace :- (s/maybe su/KeywordOrString) - group-id :- su/IntGreaterThanZero - collection-id :- (s/cond-pre (s/eq :root) su/IntGreaterThanZero) - new-collection-perms :- CollectionPermissions] - (let [collection-id (if (= collection-id :root) - (assoc collection/root-collection :namespace collection-namespace) - collection-id)] - ;; remove whatever entry is already there (if any) and add a new entry if applicable - (perms/revoke-collection-permissions! group-id collection-id) - (case new-collection-perms - :write (perms/grant-collection-readwrite-permissions! group-id collection-id) - :read (perms/grant-collection-read-permissions! group-id collection-id) - :none nil)))) + [collection-namespace :- (s/maybe su/KeywordOrString) + group-id :- su/IntGreaterThanZero + collection-id :- (s/cond-pre (s/eq :root) su/IntGreaterThanZero) + new-collection-perms :- CollectionPermissions] + (let [collection-id (if (= collection-id :root) + (assoc collection/root-collection :namespace collection-namespace) + collection-id)] + ;; remove whatever entry is already there (if any) and add a new entry if applicable + (perms/revoke-collection-permissions! group-id collection-id) + (case new-collection-perms + :write (perms/grant-collection-readwrite-permissions! group-id collection-id) + :read (perms/grant-collection-read-permissions! group-id collection-id) + :none nil))) (s/defn ^:private update-group-permissions! [collection-namespace :- (s/maybe su/KeywordOrString) diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj index 066b6331628..aa36ca3511c 100644 --- a/src/metabase/models/permissions.clj +++ b/src/metabase/models/permissions.clj @@ -408,6 +408,14 @@ [collection-or-id :- MapOrID] (str (collection-readwrite-path collection-or-id) "read/")) +(s/defn app-root-collection-permission :- Path + "Return path for the app root collection permission `read-or-write`." + [read-or-write :- (s/enum :read :write)] + (let [app-root-collection {:metabase.models.collection.root/is-root? true, :namespace :apps}] + (case read-or-write + :write (collection-readwrite-path app-root-collection) + :read (collection-read-path app-root-collection)))) + (s/defn table-read-path :- Path "Return the permissions path required to fetch the Metadata for a Table." ([table-or-id] @@ -1227,7 +1235,8 @@ "Save changes made to permission graph for logging/auditing purposes. This doesn't do anything if `*current-user-id*` is unset (e.g. for testing or REPL usage). * `model` -- revision model, should be one of - [PermissionsRevision, CollectionPermissionGraphRevision, ApplicationPermissionsRevision] + [PermissionsRevision, CollectionPermissionGraphRevision, ApplicationPermissionsRevision + AppPermissionGraphRevision] * `before` -- the graph before the changes * `changes` -- set of changes applied in this revision." [model current-revision before changes] diff --git a/test/metabase/api/activity_test.clj b/test/metabase/api/activity_test.clj index 471809076fd..65468400a3a 100644 --- a/test/metabase/api/activity_test.clj +++ b/test/metabase/api/activity_test.clj @@ -135,7 +135,7 @@ :display "table" :archived true :visualization_settings {}}] - Collection [{coll-id :id} {}] + Collection [{coll-id :id} {:namespace :apps}] App [{app-id :id} {:collection_id coll-id}] Dashboard [page {:name "rand-name" :description "rand-name" @@ -186,7 +186,7 @@ Dashboard [dash1 {:name "rand-name" :description "rand-name" :creator_id (mt/user->id :crowberto)}] - Collection [{coll-id :id} {}] + Collection [{coll-id :id} {:namespace :apps}] App [{app-id :id} {:collection_id coll-id}] Dashboard [dash2 {:name "other-dashboard" :description "just another dashboard" diff --git a/test/metabase/api/app_test.clj b/test/metabase/api/app_test.clj index 6b212814399..19740aff0d3 100644 --- a/test/metabase/api/app_test.clj +++ b/test/metabase/api/app_test.clj @@ -25,42 +25,42 @@ :color "#123456"}] (mt/test-drivers (mt/normal-drivers-with-feature :actions/custom) (testing "parent_id is ignored when creating apps" - (mt/with-temporary-setting-values [all-users-app-permission :none] - (mt/with-temp* [Collection [{collection-id :id}]] - (let [coll-params (assoc base-params :parent_id collection-id) - response (mt/user-http-request :crowberto :post 200 "app" {:collection coll-params})] - (is (pos-int? (:id response))) - (is (pos-int? (:collection_id response))) - (is (partial= (assoc base-params :location "/") - (:collection response))) - (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response) :none}}} - (graph/graph)) - "''All Users'' should have the default permission on the app collection"))))) + (mt/with-temp* [Collection [{collection-id :id}]] + (let [coll-params (assoc base-params :parent_id collection-id) + response (mt/user-http-request :crowberto :post 200 "app" {:collection coll-params})] + (is (pos-int? (:id response))) + (is (pos-int? (:collection_id response))) + (is (partial= (assoc base-params :location "/") + (:collection response))) + (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response) :none}}} + (graph/graph :apps)) + "''All Users'' should have the default permission on the app collection")))) (testing "Create app in the root" - (mt/with-temporary-setting-values [all-users-app-permission :read] + (mt/with-all-users-permission (perms/app-root-collection-permission :read) (let [response (mt/user-http-request :crowberto :post 200 "app" {:collection base-params})] - (is (pos-int? (:id response))) - (is (pos-int? (:collection_id response))) - (is (partial= (assoc base-params :location "/") - (:collection response))) - (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response) :read}}} - (graph/graph)) - "''All Users'' should have the default permission on the app collection")))) + (is (pos-int? (:id response))) + (is (pos-int? (:collection_id response))) + (is (partial= (assoc base-params :location "/") + (:collection response))) + (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response) :read}}} + (graph/graph :apps)) + "''All Users'' should have the default permission on the app collection")))) (testing "With initial dashboard and nav_items" - (mt/with-temp Dashboard [{dashboard-id :id}] - (let [nav_items [{:options {:click_behavior {}}}]] - (is (partial= {:collection (assoc base-params :location "/") - :dashboard_id dashboard-id - :nav_items nav_items} - (mt/user-http-request :rasta :post 200 "app" {:collection base-params - :dashboard_id dashboard-id - :nav_items nav_items})))))))))) + (mt/with-all-users-permission (perms/app-root-collection-permission :write) + (mt/with-temp* [Dashboard [{dashboard-id :id}]] + (let [nav_items [{:options {:click_behavior {}}}]] + (is (partial= {:collection (assoc base-params :location "/") + :dashboard_id dashboard-id + :nav_items nav_items} + (mt/user-http-request :rasta :post 200 "app" {:collection base-params + :dashboard_id dashboard-id + :nav_items nav_items}))))))))))) (deftest update-test (mt/test-drivers (mt/normal-drivers-with-feature :actions/custom) (let [app-data {:nav_items [{:options {:item "stuff"}}] :options {:frontend "stuff"}}] - (mt/with-temp* [Collection [{collection_id :id}] + (mt/with-temp* [Collection [{collection_id :id} {:namespace :apps}] App [{app_id :id} (assoc app-data :collection_id collection_id)] Dashboard [{dashboard_id :id}]] (let [expected (assoc app-data :collection_id collection_id :dashboard_id dashboard_id)] @@ -76,34 +76,36 @@ (mt/user-http-request :crowberto :put 200 (str "app/" app_id) {:dashboard_id dashboard_id :options nil}))))))) (testing "Collection permissions" - (mt/with-non-admin-groups-no-root-collection-perms - (mt/with-temp* [Collection [{collection_id :id}] - App [{app_id :id} {:collection_id collection_id}]] - (is (= "You don't have permissions to do that." - (mt/user-http-request :rasta :put 403 (str "app/" app_id) {}))))) - (mt/with-temp* [Collection [{collection_id :id}] + (mt/with-temp* [Collection [{collection_id :id} {:namespace :apps}] App [{app_id :id} {:collection_id collection_id}]] - (is (partial= {:collection_id collection_id} - (mt/user-http-request :rasta :put 200 (str "app/" app_id) {}))))))) + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :put 403 (str "app/" app_id) {})))) + (mt/with-all-users-permission (perms/app-root-collection-permission :write) + (mt/with-temp* [Collection [{collection_id :id} {:namespace :apps}] + App [{app_id :id} {:collection_id collection_id}]] + (is (partial= {:collection_id collection_id} + (mt/user-http-request :rasta :put 200 (str "app/" app_id) {})))))))) (deftest list-apps-test (mt/test-drivers (mt/normal-drivers-with-feature :actions/custom) (let [app-data {:nav_items [{:options {:item "stuff"}}] :options {:frontend "stuff"}}] - (mt/with-temp* [Collection [{collection_id :id :as collection}] + (mt/with-temp* [Collection [{collection_id :id :as collection} {:namespace :apps}] Dashboard [{dashboard_id :id}] App [{app-id :id} (assoc app-data :collection_id collection_id :dashboard_id dashboard_id)]] (let [expected (merge app-data {:id app-id :collection_id collection_id :dashboard_id dashboard_id - :collection (assoc collection :can_write true)})] + :collection (-> collection + (assoc :can_write true) + (update :namespace name))})] (testing "can query non-archived apps" (is (partial= [expected] (mt/user-http-request :crowberto :get 200 "app")))))) (testing "can only see apps with permission for" - (mt/with-non-admin-groups-no-root-collection-perms - (mt/with-temp* [Collection [collection-1 {:name "Collection 1"}] - Collection [collection-2 {:name "Collection 2"}] + (mt/with-model-cleanup [Permissions] + (mt/with-temp* [Collection [collection-1 {:name "Collection 1", :namespace :apps}] + Collection [collection-2 {:name "Collection 2", :namespace :apps}] Dashboard [{dashboard_id :id}] App [{app-id :id} (assoc app-data :collection_id (:id collection-1) :dashboard_id dashboard_id)] App [_ (assoc app-data :collection_id (:id collection-2) :dashboard_id dashboard_id)]] @@ -111,47 +113,56 @@ (let [expected (merge app-data {:id app-id :collection_id (:id collection-1) :dashboard_id dashboard_id - :collection (assoc collection-1 :can_write false)})] + :collection (-> collection-1 + (assoc :can_write false) + (update :namespace name))})] (is (partial= [expected] (mt/user-http-request :rasta :get 200 "app"))))))) (testing "archives" - (mt/with-temp* [Collection [collection-1 {:name "Collection 1"}] - Collection [collection-2 {:name "Collection 2" :archived true}] - Dashboard [{dashboard_id :id}] - App [{app-1-id :id} (assoc app-data :collection_id (:id collection-1) :dashboard_id dashboard_id)] - App [{app-2-id :id} (assoc app-data :collection_id (:id collection-2) :dashboard_id dashboard_id)]] - (testing "listing normal apps" - (let [expected (merge app-data {:id app-1-id - :collection_id (:id collection-1) - :dashboard_id dashboard_id - :collection (assoc collection-1 :can_write true)})] - (is (partial= [expected] - (mt/user-http-request :rasta :get 200 "app"))))) - (testing "listing archived" - (let [expected (merge app-data {:id app-2-id - :collection_id (:id collection-2) - :dashboard_id dashboard_id - :collection (assoc collection-2 :can_write true)})] - (is (partial= [expected] - (mt/user-http-request :rasta :get 200 "app/?archived=true")))))))))) + (mt/with-model-cleanup [Permissions] + (mt/with-all-users-permission (perms/app-root-collection-permission :write) + (mt/with-temp* [Collection [collection-1 {:name "Collection 1", :namespace :apps}] + Collection [collection-2 {:name "Collection 2", :namespace :apps, :archived true}] + Dashboard [{dashboard_id :id}] + App [{app-1-id :id} (assoc app-data :collection_id (:id collection-1) :dashboard_id dashboard_id)] + App [{app-2-id :id} (assoc app-data :collection_id (:id collection-2) :dashboard_id dashboard_id)]] + (testing "listing normal apps" + (let [expected (merge app-data {:id app-1-id + :collection_id (:id collection-1) + :dashboard_id dashboard_id + :collection (-> collection-1 + (assoc :can_write true) + (update :namespace name))})] + (is (partial= [expected] + (mt/user-http-request :rasta :get 200 "app"))))) + (testing "listing archived" + (let [expected (merge app-data {:id app-2-id + :collection_id (:id collection-2) + :dashboard_id dashboard_id + :collection (-> collection-2 + (assoc :can_write true) + (update :namespace name))})] + (is (partial= [expected] + (mt/user-http-request :rasta :get 200 "app/?archived=true")))))))))))) (deftest fetch-app-test (let [app-data {:nav_items [{:options {:item "stuff"}}] :options {:frontend "stuff"}}] - (mt/with-non-admin-groups-no-root-collection-perms - (mt/with-temp* [Collection [{collection_id :id :as collection}] - Dashboard [{dashboard_id :id}] - App [{app-id :id} (assoc app-data :collection_id collection_id :dashboard_id dashboard_id)]] - (testing "that we can see app details" - (let [expected (merge app-data {:id app-id - :collection_id collection_id - :dashboard_id dashboard_id - :collection (assoc collection :can_write true)})] - (is (partial= expected - (mt/user-http-request :crowberto :get 200 (str "app/" app-id)))))) - (testing "that app detail properly checks permissions" - (is (= "You don't have permissions to do that." - (mt/user-http-request :rasta :get 403 (str "app/" app-id))))))))) + (mt/with-temp* [Collection [{collection_id :id :as collection} {:namespace :apps}] + Dashboard [{dashboard_id :id}] + App [{app-id :id} (assoc app-data :collection_id collection_id :dashboard_id dashboard_id)]] + (testing "that we can see app details" + (let [expected (merge app-data {:id app-id + :collection_id collection_id + :dashboard_id dashboard_id + :collection (-> collection + (assoc :can_write true) + (update :namespace name))})] + (is (partial= expected + (mt/user-http-request :crowberto :get 200 (str "app/" app-id)))))) + (testing "that app detail properly checks permissions" + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :get 403 (str "app/" app-id)))))))) (defn- normalized-models [models] (->> models (sort-by :id) json/generate-string)) @@ -170,8 +181,8 @@ (deftest scaffold-test (mt/with-model-cleanup [Card Dashboard Collection Permissions] - (testing "Golden path" - (mt/with-temporary-setting-values [all-users-app-permission :read] + (mt/with-all-users-permission (perms/app-root-collection-permission :read) + (testing "Golden path" (let [app (mt/user-http-request :crowberto :post 200 "app/scaffold" {:table-ids [(data/id :venues)] @@ -202,23 +213,24 @@ [:= :dataset true]]}] :order-by [:id]})))) (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id app) :read}}} - (graph/graph)) + (graph/graph :apps)) "''All Users'' should have the default permission on the app collection") (is (= (scaffolded-models app) - (api-models app)))))) - (testing "Bad or duplicate tables" - (is (= (format "Some tables could not be found. Given: (%s %s) Found: (%s)" - (data/id :venues) - Integer/MAX_VALUE - (data/id :venues)) - (mt/user-http-request - :crowberto :post 400 "app/scaffold" - {:table-ids [(data/id :venues) (data/id :venues) Integer/MAX_VALUE] - :app-name (str "My test app " (gensym))})))))) + (api-models app))))) + + (testing "Bad or duplicate tables" + (is (= (format "Some tables could not be found. Given: (%s %s) Found: (%s)" + (data/id :venues) + Integer/MAX_VALUE + (data/id :venues)) + (mt/user-http-request + :crowberto :post 400 "app/scaffold" + {:table-ids [(data/id :venues) (data/id :venues) Integer/MAX_VALUE] + :app-name (str "My test app " (gensym))}))))))) (deftest scaffold-app-test (mt/with-model-cleanup [Card Dashboard] - (mt/with-temp* [Collection [{collection-id :id}] + (mt/with-temp* [Collection [{collection-id :id} {:namespace :apps}] App [{app-id :id} {:collection_id collection-id}]] (testing "Without existing pages" (let [app (mt/user-http-request @@ -313,11 +325,23 @@ response2 (mt/user-http-request :crowberto :post 200 "app" {:collection base-params})] (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response1) :none (:collection_id response2) :none}}} - (graph/graph))) - (mt/user-http-request :crowberto :put 200 "app/global-graph" - (assoc-in (mt/user-http-request :crowberto :get 200 "app/global-graph") - [:groups (:id (perms-group/all-users))] - :write)) - (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response1) :write - (:collection_id response2) :write}}} - (graph/graph)))))))))) + (graph/graph :apps))) + (testing "''All Users'' can't see these apps" + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :get 403 (str "app/" (:id response1))))) + (is (= "You don't have permissions to do that." + (mt/user-http-request :rasta :get 403 (str "app/" (:id response2)))))) + (is (partial= {:groups {(:id (perms-group/all-users)) {:root "write"}}} + (mt/user-http-request :crowberto :put 200 "app/global-graph" + (assoc-in (mt/user-http-request :crowberto :get 200 "app/global-graph") + [:groups (:id (perms-group/all-users))] + {:root :write})))) + (is (partial= {:groups {(:id (perms-group/all-users)) {(:collection_id response1) :none + (:collection_id response2) :none}}} + (graph/graph :apps)) + "collection permissions shouldn't change") + (testing "Now ''All Users'' can see these apps" + (is (partial= response1 + (mt/user-http-request :rasta :get 200 (str "app/" (:id response1))))) + (is (partial= response2 + (mt/user-http-request :rasta :get 200 (str "app/" (:id response2))))))))))))) diff --git a/test/metabase/api/bookmark_test.clj b/test/metabase/api/bookmark_test.clj index 5c75ffee198..f363f9fe51e 100644 --- a/test/metabase/api/bookmark_test.clj +++ b/test/metabase/api/bookmark_test.clj @@ -9,57 +9,60 @@ [metabase.models.collection :refer [Collection]] [metabase.models.dashboard :refer [Dashboard]] [metabase.models.interface :as mi] + [metabase.models.permissions :as perms] [metabase.test :as mt] [metabase.util :as u] [toucan.db :as db])) (deftest bookmarks-test + (mt/initialize-if-needed! :db) (testing "POST /api/bookmark/:model/:model-id" - (mt/with-temp* [Collection [{coll-id :id :as collection} {:name "Test Collection"}] - Card [card {:name "Test Card", :display "area", :collection_id coll-id}] - Dashboard [dashboard {:name "Test Dashboard", :is_app_page true, :collection_id coll-id}] - App [app {:collection_id coll-id, :dashboard_id (:id dashboard)}]] - (testing "check that we can bookmark a Collection" - (is (= (u/the-id collection) - (->> (mt/user-http-request :rasta :post 200 (str "bookmark/collection/" (u/the-id collection))) - :collection_id)))) - (testing "check that we can bookmark a Card" - (is (= (u/the-id card) - (->> (mt/user-http-request :rasta :post 200 (str "bookmark/card/" (u/the-id card))) - :card_id)))) - (let [card-result (->> (mt/user-http-request :rasta :get 200 "bookmark") - (filter #(= (:type %) "card")) - first)] - (testing "check a card bookmark has `:display` key" - (is (contains? card-result :display))) - (testing "check a card bookmark has `:dataset` key" - (is (contains? card-result :dataset)))) - (testing "check that we can bookmark a Dashboard" - (is (= (u/the-id dashboard) - (->> (mt/user-http-request :rasta :post 200 (str "bookmark/dashboard/" (u/the-id dashboard))) - :dashboard_id)))) - (testing "check that we can retreive the user's bookmarks" - (let [result (mt/user-http-request :rasta :get 200 "bookmark")] - (is (= #{"card" "collection" "dashboard"} - (into #{} (map :type) result))) - (testing "that app_id is hydrated on app collections" - (is (partial= [{:item_id (:id collection), :name "Test Collection", :app_id (:id app)}] - (filter #(= (:type %) "collection") result)))) - (testing "that is_app_page is returned for dashboards" - (is (partial= [{:item_id (:id dashboard), :name "Test Dashboard", :is_app_page true, :app_id (:id app)}] - (filter #(= (:type %) "dashboard") result)))))) - (testing "check that we can delete bookmarks" - (mt/user-http-request :rasta :delete 204 (str "bookmark/card/" (u/the-id card))) - (is (= #{"collection" "dashboard"} - (->> (mt/user-http-request :rasta :get 200 "bookmark") - (map :type) - set))) - (mt/user-http-request :rasta :delete 204 (str "bookmark/collection/" (u/the-id collection))) - (mt/user-http-request :rasta :delete 204 (str "bookmark/dashboard/" (u/the-id dashboard))) - (is (= #{} - (->> (mt/user-http-request :rasta :get 200 "bookmark") - (map :type) - set))))))) + (mt/with-all-users-permission (perms/app-root-collection-permission :write) + (mt/with-temp* [Collection [{coll-id :id :as collection} {:name "Test Collection", :namespace :apps}] + Card [card {:name "Test Card", :display "area", :collection_id coll-id}] + Dashboard [dashboard {:name "Test Dashboard", :is_app_page true, :collection_id coll-id}] + App [app {:collection_id coll-id, :dashboard_id (:id dashboard)}]] + (testing "check that we can bookmark a Collection" + (is (= (u/the-id collection) + (->> (mt/user-http-request :rasta :post 200 (str "bookmark/collection/" (u/the-id collection))) + :collection_id)))) + (testing "check that we can bookmark a Card" + (is (= (u/the-id card) + (->> (mt/user-http-request :rasta :post 200 (str "bookmark/card/" (u/the-id card))) + :card_id)))) + (let [card-result (->> (mt/user-http-request :rasta :get 200 "bookmark") + (filter #(= (:type %) "card")) + first)] + (testing "check a card bookmark has `:display` key" + (is (contains? card-result :display))) + (testing "check a card bookmark has `:dataset` key" + (is (contains? card-result :dataset)))) + (testing "check that we can bookmark a Dashboard" + (is (= (u/the-id dashboard) + (->> (mt/user-http-request :rasta :post 200 (str "bookmark/dashboard/" (u/the-id dashboard))) + :dashboard_id)))) + (testing "check that we can retreive the user's bookmarks" + (let [result (mt/user-http-request :rasta :get 200 "bookmark")] + (is (= #{"card" "collection" "dashboard"} + (into #{} (map :type) result))) + (testing "that app_id is hydrated on app collections" + (is (partial= [{:item_id (:id collection), :name "Test Collection", :app_id (:id app)}] + (filter #(= (:type %) "collection") result)))) + (testing "that is_app_page is returned for dashboards" + (is (partial= [{:item_id (:id dashboard), :name "Test Dashboard", :is_app_page true, :app_id (:id app)}] + (filter #(= (:type %) "dashboard") result)))))) + (testing "check that we can delete bookmarks" + (mt/user-http-request :rasta :delete 204 (str "bookmark/card/" (u/the-id card))) + (is (= #{"collection" "dashboard"} + (->> (mt/user-http-request :rasta :get 200 "bookmark") + (map :type) + set))) + (mt/user-http-request :rasta :delete 204 (str "bookmark/collection/" (u/the-id collection))) + (mt/user-http-request :rasta :delete 204 (str "bookmark/dashboard/" (u/the-id dashboard))) + (is (= #{} + (->> (mt/user-http-request :rasta :get 200 "bookmark") + (map :type) + set)))))))) (defn bookmark-models [user-id & models] (doseq [model models] diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 1a7fbbefe7e..a59eabbfc05 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -5,7 +5,8 @@ [honeysql.core :as hsql] [metabase.api.collection :as api.collection] [metabase.models :refer [App Card Collection Dashboard DashboardCard ModerationReview NativeQuerySnippet - PermissionsGroup PermissionsGroupMembership Pulse PulseCard PulseChannel + PermissionsGroup PermissionsGroupMembership + Pulse PulseCard PulseChannel PulseChannelRecipient Revision Timeline TimelineEvent User]] [metabase.models.collection :as collection] [metabase.models.collection-test :as collection-test] @@ -110,28 +111,49 @@ (str/includes? collection-name "Personal Collection")))) (map :name))))))) - (mt/with-temp* [Collection [{coll1-id :id} {:name "Archived Collection", :archived true}] - Collection [{coll2-id :id} {:name "Regular Collection"}] - App [{app1-id :id} {:collection_id coll1-id}] - App [{app2-id :id} {:collection_id coll2-id}]] + (mt/with-temp* [Collection [_ {:name "Archived Collection", :archived true}] + Collection [_ {:name "Regular Collection"}]] (letfn [(remove-other-collections [collections] (filter (fn [{collection-name :name}] (or (#{"Our analytics" "Archived Collection" "Regular Collection"} collection-name) (str/includes? collection-name "Personal Collection"))) collections))] (testing "check that we don't see collections if they're archived" - (is (= [["Our analytics" nil] - ["Rasta Toucan's Personal Collection" nil] - ["Regular Collection" app2-id]] + (is (= ["Our analytics" + "Rasta Toucan's Personal Collection" + "Regular Collection"] (->> (mt/user-http-request :rasta :get 200 "collection") remove-other-collections - (map (juxt :name :app_id)))))) + (map :name))))) (testing "Check that if we pass `?archived=true` we instead see archived Collections" - (is (= [["Archived Collection" app1-id]] + (is (= ["Archived Collection"] (->> (mt/user-http-request :rasta :get 200 "collection" :archived :true) remove-other-collections - (map (juxt :name :app_id)))))))) + (map :name))))))) + + (testing "app collections" + (mt/with-temp* [Collection [{coll1-id :id} {:name "Archived Collection", :namespace :apps, :archived true}] + Collection [{coll2-id :id} {:name "Regular Collection", :namespace :apps}] + App [{app1-id :id} {:collection_id coll1-id}] + App [{app2-id :id} {:collection_id coll2-id}]] + (letfn [(remove-other-collections [collections] + (filter (fn [{collection-name :name}] + (or (#{"All apps" "Archived Collection" "Regular Collection"} collection-name) + (str/includes? collection-name "Personal Collection"))) + collections))] + (testing "check that we don't see collections if they're archived" + (is (= [["All apps" nil] + ["Regular Collection" app2-id]] + (->> (mt/user-http-request :crowberto :get 200 "collection?namespace=apps") + remove-other-collections + (map (juxt :name :app_id)))))) + + (testing "Check that if we pass `?archived=true` we instead see archived Collections" + (is (= [["Archived Collection" app1-id]] + (->> (mt/user-http-request :crowberto :get 200 "collection?namespace=apps&archived=true") + remove-other-collections + (map (juxt :name :app_id))))))))) (testing "?namespace= parameter" (mt/with-temp* [Collection [{normal-id :id} {:name "Normal Collection"}] @@ -184,41 +206,35 @@ (testing "sanity check" (is (some? personal-collection))) (with-collection-hierarchy [a b c d e f g] - (mt/with-temp* [App [{app-a-id :id} {:collection_id (:id a)}] - App [{app-c-id :id} {:collection_id (:id c)}] - App [{app-g-id :id} {:collection_id (:id g)}]] - (let [ids (set (map :id (cons personal-collection [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" - (is (= [{:name "A" - :app_id app-a-id - :children [{:name "B", :children []} - {:name "C" - :app_id app-c-id - :children [{:name "D" - :children [{:name "E", :children []}]} - {:name "F" - :children [{:name "G" - :app_id app-g-id - :children []}]}]}]} - {:name "Rasta Toucan's Personal Collection", :children []}] - (collection-tree-view ids [:name :app_id] response)))) - (testing "Make sure each Collection comes back with the expected keys" - (is (= {:description nil - :archived false - :entity_id (:entity_id personal-collection) - :slug "rasta_toucan_s_personal_collection" - :color "#31698A" - :name "Rasta Toucan's Personal Collection" - :personal_owner_id (mt/user->id :rasta) - :id (:id (collection/user->personal-collection (mt/user->id :rasta))) - :location "/" - :namespace nil - :children [] - :authority_level nil} - (some #(when (= (:id %) (:id (collection/user->personal-collection (mt/user->id :rasta)))) - %) - response))))))) + (let [ids (set (map :id (cons personal-collection [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" + (is (= [{:name "A" + :children [{:name "B", :children []} + {:name "C" + :children [{:name "D" + :children [{:name "E", :children []}]} + {:name "F" + :children [{:name "G" + :children []}]}]}]} + {:name "Rasta Toucan's Personal Collection", :children []}] + (collection-tree-view ids [:name :app_id] response)))) + (testing "Make sure each Collection comes back with the expected keys" + (is (= {:description nil + :archived false + :entity_id (:entity_id personal-collection) + :slug "rasta_toucan_s_personal_collection" + :color "#31698A" + :name "Rasta Toucan's Personal Collection" + :personal_owner_id (mt/user->id :rasta) + :id (:id (collection/user->personal-collection (mt/user->id :rasta))) + :location "/" + :namespace nil + :children [] + :authority_level nil} + (some #(when (= (:id %) (:id (collection/user->personal-collection (mt/user->id :rasta)))) + %) + response)))))) (testing "Excludes archived collections (#19603)" (mt/with-temp* [Collection [a {:name "A"}] Collection [b {:name "B archived" @@ -324,13 +340,19 @@ (deftest fetch-collection-test (testing "GET /api/collection/:id" (testing "check that we can see collection details" - (mt/with-temp* [Collection [collection {:name "Coin Collection"}] - App [{app-id :id} {:collection_id (:id collection)}]] - (perms/grant-collection-read-permissions! (perms-group/all-users) collection) - (is (= ["Coin Collection" app-id] - ((juxt :name :app_id) + (mt/with-temp Collection [collection {:name "Coin Collection"}] + (is (= "Coin Collection" + (:name (mt/user-http-request :rasta :get 200 (str "collection/" (u/the-id collection)))))))) + (testing "check that we can see app collection details" + (mt/with-all-users-permission (perms/app-root-collection-permission :read) + (mt/with-temp* [Collection [collection {:name "Coin Collection", :namespace :apps}] + App [{app-id :id} {:collection_id (:id collection)}]] + (is (= ["Coin Collection" app-id] + ((juxt :name :app_id) + (mt/user-http-request :rasta :get 200 (str "collection/" (u/the-id collection))))))))) + (testing "check that collections detail properly checks permissions" (mt/with-non-admin-groups-no-root-collection-perms (mt/with-temp Collection [collection] @@ -686,9 +708,10 @@ (deftest filter-facet-test (testing "Filter facets" (mt/with-temp* [Collection [_ {:name "Top level collection"}] - Collection [{app-coll-id :id} {:name "App with items"}] + Collection [{app-coll-id :id} {:name "App with items", :namespace :apps}] App [{app-id :id} {:collection_id app-coll-id}] Collection [_ {:name "subcollection" + :namespace :apps :location (format "/%d/" app-coll-id) :authority_level "official"}] Card [_ {:name "card" :collection_id app-coll-id}] @@ -697,36 +720,35 @@ Dashboard [_ {:name "page" :collection_id app-coll-id :is_app_page true}]] (let [items (->> "/items?models=dashboard&models=card&models=collection" (str "collection/" app-coll-id) - (mt/user-http-request :rasta :get 200) + (mt/user-http-request :crowberto :get 200) :data)] (is (= #{"card" "dash" "subcollection"} (into #{} (map :name) items)))) (let [items (->> "/items?models=dashboard&models=card&models=collection&models=dataset" (str "collection/" app-coll-id) - (mt/user-http-request :rasta :get 200) + (mt/user-http-request :crowberto :get 200) :data)] (is (= #{"card" "dash" "subcollection" "dataset"} (into #{} (map :name) items)))) (let [items (->> "/items?models=page" (str "collection/" app-coll-id) - (mt/user-http-request :rasta :get 200) + (mt/user-http-request :crowberto :get 200) :data)] (is (= #{"page"} (into #{} (map :name) items)))) (let [items (mt/user-http-request - :rasta :get 200 "collection/root/items?models=app")] + :crowberto :get 200 "collection/root/items?models=app&namespace=apps")] (is (partial= [{:id app-coll-id :app_id app-id :model "app"}] (:data items)))) (let [items (mt/user-http-request - :rasta :get 200 "collection/root/items")] - (is (= #{["app" "App with items"] - ["collection" "Top level collection"] - ["collection" "Rasta Toucan's Personal Collection"]} + :crowberto :get 200 "collection/root/items")] + (is (= #{["collection" "Top level collection"] + ["collection" "Crowberto Corv's Personal Collection"]} (into #{} (map (juxt :model :name)) (:data items))))) (let [items (->> (str "collection/" app-coll-id "/items") - (mt/user-http-request :rasta :get 200) + (mt/user-http-request :crowberto :get 200) :data)] (is (= #{"card" "dash" "subcollection" "dataset" "page"} (into #{} (map :name) items))))))) @@ -1539,7 +1561,7 @@ (update :entity_id string?)))))) (testing "I shouldn't be allowed to move an App away from root." - (mt/with-temp* [Collection [collection-a] + (mt/with-temp* [Collection [collection-a {:namespace :apps}] App [_app {:collection_id (:id collection-a)}] Collection [collection-b]] (is (= "You don't have permissions to do that." diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj index 413865c120b..19e990f010a 100644 --- a/test/metabase/api/search_test.clj +++ b/test/metabase/api/search_test.clj @@ -97,21 +97,22 @@ #(assoc % :collection {:id true, :name true :authority_level nil :app_id false}) (default-search-results))) -(defn- do-with-search-items [search-string in-root-collection? f] - (let [data-map (fn [instance-name] - {:name (format instance-name search-string)}) +(defn- do-with-search-items [search-string {:keys [in-root-collection? in-app-collection?]} f] + (let [data-map (fn [instance-name app-collection?] + (cond-> {:name (format instance-name search-string)} + app-collection? (assoc :namespace :apps))) coll-data-map (fn [instance-name collection] - (merge (data-map instance-name) + (merge (data-map instance-name false) (when-not in-root-collection? {:collection_id (u/the-id collection)})))] - (mt/with-temp* [Collection [coll (data-map "collection %s collection")] + (mt/with-temp* [Collection [coll (data-map "collection %s collection" in-app-collection?)] Card [card (coll-data-map "card %s card" coll)] Card [dataset (assoc (coll-data-map "dataset %s dataset" coll) :dataset true)] Dashboard [dashboard (coll-data-map "dashboard %s dashboard" coll)] Pulse [pulse (coll-data-map "pulse %s pulse" coll)] - Metric [metric (data-map "metric %s metric")] - Segment [segment (data-map "segment %s segment")]] + Metric [metric (data-map "metric %s metric" false)] + Segment [segment (data-map "segment %s segment" false)]] (f {:collection coll :card card :dataset dataset @@ -121,10 +122,13 @@ :segment segment})))) (defmacro ^:private with-search-items-in-root-collection [search-string & body] - `(do-with-search-items ~search-string true (fn [~'_] ~@body))) + `(do-with-search-items ~search-string {:in-root-collection? true} (fn [~'_] ~@body))) (defmacro ^:private with-search-items-in-collection [created-items-sym search-string & body] - `(do-with-search-items ~search-string false (fn [~created-items-sym] ~@body))) + `(do-with-search-items ~search-string nil (fn [~created-items-sym] ~@body))) + +(defmacro ^:private with-search-items-in-app-collection [created-items-sym search-string & body] + `(do-with-search-items ~search-string {:in-app-collection? true} (fn [~created-items-sym] ~@body))) (def ^:private ^:dynamic *search-request-results-database-id* "Filter out all results from `search-request` that don't have this Database ID. Default: the default H2 `test-data` @@ -614,7 +618,7 @@ (deftest app-test (testing "App collections should come with app_id set" - (with-search-items-in-collection {:keys [collection]} "test" + (with-search-items-in-app-collection {:keys [collection]} "test" (mt/with-temp App [_app {:collection_id (:id collection)}] (is (= (mapv (fn [result] @@ -622,14 +626,14 @@ (not (#{"metric" "segment"} (:model result))) (assoc-in [:collection :app_id] true) (= (:model result) "collection") (assoc :model "app" :app_id true))) (default-results-with-collection)) - (search-request-data :rasta :q "test")))))) + (search-request-data :crowberto :q "test")))))) (testing "App collections should filterable as \"app\"" - (mt/with-temp* [Collection [collection {:name "App collection to find"}] + (mt/with-temp* [Collection [collection {:name "App collection to find", :namespace :apps}] App [_ {:collection_id (:id collection)}] Collection [_ {:name "Another collection to find"}]] (is (partial= [(assoc (select-keys collection [:name]) :model "app")] - (search-request-data :rasta :q "find" :models "app")))))) + (search-request-data :crowberto :q "find" :models "app")))))) (deftest page-test (testing "Search results should pages with model \"page\"" @@ -643,10 +647,10 @@ (deftest collection-app-id-test (testing "app_id and id of containing collection should not be confused (#25213)" - (mt/with-temp* [Collection [{coll-id :id}] + (mt/with-temp* [Collection [{coll-id :id} {:namespace :apps}] ;; The ignored elements are there to make sure the IDs ;; coll-id and app-id are different. - Collection [{ignored-collection-id :id}] + Collection [{ignored-collection-id :id} {:namespace :apps}] App [_ignored-app {:collection_id ignored-collection-id}] App [{app-id :id} {:collection_id coll-id}] Dashboard [_ {:name "Not a page but contains important text!" @@ -664,4 +668,4 @@ (is (not= app-id coll-id) "app-id and coll-id should be different. Fix the test!") (is (partial= (repeat 4 {:collection {:app_id app-id :id coll-id}}) - (:data (make-search-request :rasta [:q "important text"]))))))) + (:data (make-search-request :crowberto [:q "important text"]))))))) diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj index 6d5cdb75e91..c2eef2ecaf1 100644 --- a/test/metabase/models/card_test.clj +++ b/test/metabase/models/card_test.clj @@ -117,7 +117,7 @@ (try (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Card can only go in Collections in the \"default\" namespace" + #"A Card can only go in Collections in the \"default\" or :apps namespace" (db/insert! Card (assoc (tt/with-temp-defaults Card) :collection_id collection-id, :name card-name)))) (finally (db/delete! Card :name card-name))))) @@ -126,7 +126,7 @@ (mt/with-temp Card [{card-id :id}] (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Card can only go in Collections in the \"default\" namespace" + #"A Card can only go in Collections in the \"default\" or :apps namespace" (db/update! Card card-id {:collection_id collection-id}))))))) (deftest normalize-result-metadata-test diff --git a/test/metabase/models/collection/graph_test.clj b/test/metabase/models/collection/graph_test.clj index 0313290dcfa..5d102962d36 100644 --- a/test/metabase/models/collection/graph_test.clj +++ b/test/metabase/models/collection/graph_test.clj @@ -437,9 +437,10 @@ (deftest modify-perms-for-app-collections-test (testing "that we cannot modify perms for app collections" - (mt/with-temp* [Collection [{coll-id :id}] + (mt/with-temp* [Collection [{coll-id :id} {:namespace :apps}] App [_app {:collection_id coll-id}]] (is (thrown-with-msg? clojure.lang.ExceptionInfo #"Cannot set app permissions using this endpoint" - (graph/update-graph! (assoc-in (graph/graph) + (graph/update-graph! :apps + (assoc-in (graph/graph :apps) [:groups (u/the-id (perms-group/all-users)) coll-id] :read))))))) diff --git a/test/metabase/models/collection_test.clj b/test/metabase/models/collection_test.clj index 24ee3ca7714..c4d6eceadf9 100644 --- a/test/metabase/models/collection_test.clj +++ b/test/metabase/models/collection_test.clj @@ -1468,7 +1468,7 @@ (mt/with-temp Collection [{collection-id :id} {:namespace "x"}] (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Card can only go in Collections in the \"default\" namespace" + #"A Card can only go in Collections in the \"default\" or :apps namespace" (collection/check-collection-namespace Card collection-id))))) (testing "Should throw exception if Collection does not exist" diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj index bd08f962afa..2786d9e40eb 100644 --- a/test/metabase/models/dashboard_test.clj +++ b/test/metabase/models/dashboard_test.clj @@ -270,7 +270,7 @@ (try (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Dashboard can only go in Collections in the \"default\" namespace" + #"A Dashboard can only go in Collections in the \"default\" or :apps namespace" (db/insert! Dashboard (assoc (tt/with-temp-defaults Dashboard) :collection_id collection-id, :name dashboard-name)))) (finally (db/delete! Dashboard :name dashboard-name))))) @@ -279,7 +279,7 @@ (mt/with-temp Dashboard [{card-id :id}] (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Dashboard can only go in Collections in the \"default\" namespace" + #"A Dashboard can only go in Collections in the \"default\" or :apps namespace" (db/update! Dashboard card-id {:collection_id collection-id}))))))) (deftest validate-parameters-test diff --git a/test/metabase/models/pulse_test.clj b/test/metabase/models/pulse_test.clj index 430b9a791fb..fe90b7fa860 100644 --- a/test/metabase/models/pulse_test.clj +++ b/test/metabase/models/pulse_test.clj @@ -400,7 +400,7 @@ (try (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Pulse can only go in Collections in the \"default\" namespace" + #"A Pulse can only go in Collections in the \"default\" or :apps namespace" (db/insert! Pulse (assoc (tt/with-temp-defaults Pulse) :collection_id collection-id, :name pulse-name)))) (finally (db/delete! Pulse :name pulse-name))))) @@ -409,7 +409,7 @@ (mt/with-temp Pulse [{card-id :id}] (is (thrown-with-msg? clojure.lang.ExceptionInfo - #"A Pulse can only go in Collections in the \"default\" namespace" + #"A Pulse can only go in Collections in the \"default\" or :apps namespace" (db/update! Pulse card-id {:collection_id collection-id}))))))) diff --git a/test/metabase/test.clj b/test/metabase/test.clj index 4f7ba902547..661016bd29f 100644 --- a/test/metabase/test.clj +++ b/test/metabase/test.clj @@ -202,6 +202,7 @@ secret-value-equals? select-keys-sequentially throw-if-called + with-all-users-permission with-column-remappings with-discarded-collections-perms-changes with-env-keys-renamed-by diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj index 654a43600c6..a2ee69735fd 100644 --- a/test/metabase/test/util.clj +++ b/test/metabase/test/util.clj @@ -806,6 +806,25 @@ :namespace (name ~collection-namespace)) (fn [] ~@body))) +(defn do-with-all-users-permission + "Call `f` without arguments in a context where the ''All Users'' group + is granted the permission specified by `permission-path`. + + For most use cases see the macro [[with-all-users-permission]]." + [permission-path f] + (tt/with-temp Permissions [_ {:group_id (:id (perms-group/all-users)) + :object permission-path}] + (f))) + +(defmacro with-all-users-permission + "Run `body` with the ''All Users'' group being granted the permission + specified by `permission-path`. + + (mt/with-all-users-permission (perms/app-root-collection-permission :read) + ...)" + [permission-path & body] + `(do-with-all-users-permission ~permission-path (fn [] ~@body))) + (defn doall-recursive "Like `doall`, but recursively calls doall on map values and nested sequences, giving you a fully non-lazy object. Useful for tests when you need the entire object to be realized in the body of a `binding`, `with-redefs`, or -- GitLab