(ns metabase-enterprise.sandbox.api.permissions-test (:require [cheshire.core :as json] [clojure.string :as str] [clojure.test :refer :all] [metabase-enterprise.sandbox.models.group-table-access-policy :refer [GroupTableAccessPolicy]] [metabase.models :refer [Card Database PermissionsGroup PersistedInfo Table]] [metabase.models.permissions-group :as perms-group] [metabase.models.persisted-info :as persisted-info] [metabase.query-processor :as qp] [metabase.test :as mt] [metabase.util :as u] [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db])) (defn- db-graph-keypath [group] [:groups (u/the-id group) (mt/id) :data]) (defn- venues-perms-graph-keypath [group] (concat (db-graph-keypath group) [:schemas :PUBLIC (mt/id :venues)])) (deftest revoke-perms-delete-gtaps-test (testing "PUT /api/permissions/graph" (testing "removing sandboxed permissions for a group should delete the associated GTAP (#16190)" (doseq [[message {:keys [updated-db-perms expected-perms]}] {"when revoking all permissions for DB" {:updated-db-perms (constantly {:native :none, :schemas :none}) :expected-perms (constantly nil)} "when revoking all permissions for schema" {:updated-db-perms (constantly {:native :none, :schemas {:PUBLIC :none}}) :expected-perms (constantly nil)} "when revoking all permissions for Table" {:updated-db-perms (fn [] {:native :none, :schemas {:PUBLIC {(mt/id :venues) :none (mt/id :users) :all}}}) :expected-perms (fn [] {:schemas {:PUBLIC {(mt/id :users) "all"}}})} "when revoking segmented query permissions for Table" {:updated-db-perms (fn [] {:native :none, :schemas {:PUBLIC {(mt/id :venues) {:read :all}}}}) :expected-perms (fn [] {:schemas {:PUBLIC {(mt/id :venues) {:read "all"}}}})} "when changing permissions for DB to unrestricted access" {:updated-db-perms (constantly {:native :none, :schemas :all}) :expected-perms (constantly {:schemas "all"})} "when changing permissions for schema to unrestricted access" {:updated-db-perms (constantly {:native :none, :schemas {:PUBLIC :all}}) :expected-perms (constantly {:schemas {:PUBLIC "all"}})} "when changing permissions for Table to :query [grant full query perms]" {:updated-db-perms (fn [] {:native :none, :schemas {:PUBLIC {(mt/id :venues) {:query :all}}}}) :expected-perms (fn [] {:schemas {:PUBLIC {(mt/id :venues) {:query "all"}}}})}}] (mt/with-gtaps {:gtaps {:venues {}}} (testing message (testing "sanity check" (testing "perms graph endpoint should return segmented perms for Venues table" (is (= {:query "segmented"} (get-in (mt/user-http-request :crowberto :get 200 "permissions/graph") (venues-perms-graph-keypath &group))))) (testing "GTAP should exist in application DB" (is (schema= [(s/one {:id su/IntGreaterThanZero :group_id (s/eq (u/the-id &group)) :table_id (s/eq (mt/id :venues)) :card_id (s/eq nil) :attribute_remappings (s/eq nil) s/Keyword s/Any} "GTAP")] (db/select GroupTableAccessPolicy :group_id (u/the-id &group)))))) (let [graph (mt/user-http-request :crowberto :get 200 "permissions/graph") graph' (assoc-in graph (db-graph-keypath &group) (updated-db-perms)) response (mt/user-http-request :crowberto :put 200 "permissions/graph" graph')] (mt/with-temp* [Database [db-2] Table [db-2-table {:db_id (u/the-id db-2)}] GroupTableAccessPolicy [_ {:group_id (u/the-id &group) :table_id (u/the-id db-2-table)}] PermissionsGroup [other-group] GroupTableAccessPolicy [_ {:group_id (u/the-id other-group) :table_id (mt/id :venues)}]] (testing "perms graph should be updated" (testing "in API request response" (is (= (expected-perms) (get-in response (db-graph-keypath &group))))) (testing "on subsequent fetch of the graph" (is (= (expected-perms) (get-in (mt/user-http-request :crowberto :get 200 "permissions/graph") (db-graph-keypath &group)))))) (testing "GTAP should be deleted from application DB" (is (= [] (db/select GroupTableAccessPolicy :group_id (u/the-id &group) :table_id (mt/id :venues))))) (testing "GTAP for same group, other database should not be affected" (is (schema= [(s/one {:id su/IntGreaterThanZero :group_id (s/eq (u/the-id &group)) :table_id (s/eq (u/the-id db-2-table)) :card_id (s/eq nil) :attribute_remappings (s/eq nil)} "GTAP")] (db/select GroupTableAccessPolicy :group_id (u/the-id &group) :table_id (u/the-id db-2-table))))) (testing "GTAP for same table, other group should not be affected" (is (schema= [(s/one {:id su/IntGreaterThanZero :group_id (s/eq (u/the-id other-group)) :table_id (s/eq (mt/id :venues)) :card_id (s/eq nil) :attribute_remappings (s/eq nil)} "GTAP")] (db/select GroupTableAccessPolicy :group_id (u/the-id other-group))))))))))))) (deftest grant-sandbox-perms-dont-delete-gtaps-test (testing "PUT /api/permissions/graph" (testing "granting sandboxed permissions for a group should *not* delete an associated GTAP (#16190)" (mt/with-temp-copy-of-db (mt/with-temp GroupTableAccessPolicy [_ {:group_id (u/the-id (perms-group/all-users)) :table_id (mt/id :venues)}] (let [graph (mt/user-http-request :crowberto :get 200 "permissions/graph") graph' (assoc-in graph (db-graph-keypath (perms-group/all-users)) {:schemas {"PUBLIC" {(mt/id :venues) {:read :all, :query :segmented}}}})] (mt/user-http-request :crowberto :put 200 "permissions/graph" graph') (testing "GTAP should not have been deleted" (is (db/exists? GroupTableAccessPolicy :group_id (u/the-id (perms-group/all-users)), :table_id (mt/id :venues)))))))))) (defn- fake-persist-card! [card] (let [persisted-info (persisted-info/make-ready! (mt/user->id :rasta) card)] (db/update-where! PersistedInfo {:card_id (u/the-id card)} :definition (json/encode (persisted-info/metadata->definition (:result_metadata card) (:table_name persisted-info))) :active true :state "persisted" :query_hash (persisted-info/query-hash (:dataset_query card))))) (deftest persistence-and-permissions (mt/with-model-cleanup [PersistedInfo] (testing "Queries from cache if not sandboxed" (mt/with-current-user (mt/user->id :rasta) (mt/with-temp* [Card [card {:dataset_query (mt/mbql-query venues) :dataset true :database_id (mt/id)}]] (fake-persist-card! card) (is (str/includes? (:query (qp/compile {:database (mt/id) :query {:source-table (str "card__" (u/the-id card))} :type :query})) "metabase_cache"))))) (testing "Queries from source if sandboxed" (mt/with-gtaps {:gtaps {:venues {:query (mt/mbql-query venues) :remappings {:cat ["variable" [:field (mt/id :venues :category_id) nil]]}}} :attributes {"cat" 50}} (mt/with-temp* [Card [card {:dataset_query (mt/mbql-query venues) :dataset true :database_id (mt/id)}]] (fake-persist-card! card) (is (not (str/includes? (:query (qp/compile {:database (mt/id) :query {:source-table (str "card__" (u/the-id card))} :type :query})) "metabase_cache"))))))))