From c7287b5a33eabf47f5cd9573c41a7ca4a5cee9b7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cam=20Sau=CC=88l?= <cammsaul@gmail.com>
Date: Tue, 7 Jul 2015 04:36:11 -0700
Subject: [PATCH] ICanReadWrite protocol

---
 src/metabase/api/common.clj       | 70 ++++++++++---------------------
 src/metabase/api/meta/table.clj   |  3 --
 src/metabase/db/internal.clj      |  2 +-
 src/metabase/models/database.clj  |  9 ++--
 src/metabase/models/field.clj     | 28 +++++++++----
 src/metabase/models/interface.clj | 48 +++++++++++++++------
 src/metabase/models/table.clj     | 42 +++++++++++--------
 7 files changed, 108 insertions(+), 94 deletions(-)

diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index d2e22b51a31..e929d275a87 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -8,6 +8,7 @@
             [metabase.api.common.internal :refer :all]
             [metabase.db :refer :all]
             [metabase.db.internal :refer [entity->korma]]
+            [metabase.models.interface :as models]
             [metabase.util :as u]
             [metabase.util.password :as password])
   (:import com.metabase.corvus.api.ApiException
@@ -29,17 +30,6 @@
   (atom nil)) ; default binding is just something that will return nil when dereferenced
 
 
-;;; ## GENERAL HELPER FNS / MACROS
-
-;; TODO - move this to something like `metabase.util.debug`
-(defmacro with-current-user
-  "Primarily for debugging purposes. Evaulates BODY as if `*current-user*` was the User with USER-ID."
-  [user-id & body]
-  `(binding [*current-user-id* ~user-id
-             *current-user* (delay (sel :one 'metabase.models.user/User :id ~user-id))]
-     ~@body))
-
-
 ;;; ## CONDITIONAL RESPONSE FUNCTIONS / MACROS
 
 (defn check
@@ -421,44 +411,28 @@
     `(defroutes ~'routes ~@api-routes ~@additional-routes)))
 
 
-;; ## NEW PERMISSIONS CHECKING MACROS
-;; Since checking `@can_read`/`@can_write` is such a common pattern, these
-;; macros eliminate a bit of the redundancy around doing so.
-;; They support two forms:
-;;
-;;     (read-check my-table) ; checks @(:can_read my-table)
-;;     (read-check Table 1)  ; checks @(:can_read (sel :one Table :id 1))
-;;
-;; *  The first form is useful when you've already fetched an object (especially in threading forms such as `->404`).
-;; *  The second form takes care of fetching the object for you and is useful in cases where you won't need the object afterward
-;;    or want to combine the `sel` and permissions check statements into a single form.
-;;
-;; Both forms will throw a 404 if the object doesn't exist (saving you one more check!) and return the selected object.
-
-(defmacro read-check
-  "Checks that `@can_read` is true for this object."
+(defn read-check
+  "Check whether we can read an existing OBJ, or ENTITY with ID."
   ([obj]
-   `(let-404 [{:keys [~'can_read] :as obj#} ~obj]
-      (check-403 @~'can_read)
-      obj#))
+   (check-404 obj)
+   (check-403 (if (satisfies? models/ICanReadWrite obj) (models/can-read? obj)
+                  @(:can_read obj)))
+   obj)
   ([entity id]
-   (cond
-     ;; simple optimization : since @can-read is always true for a Database
-     ;; the read-check macro will just resolve to true in this simple case
-     ;; use `name` so we can match 'Database or 'metabase.models.database/Database
-     ;;
-     ;; TODO - it would be nice to generalize the read-checking pattern, and make it
-     ;; a separate multimethod or protocol so other models besides DB can write optimized
-     ;; implementations. Currently, we always fetch an *entire* object to do read checking,
-     ;; which is wasteful.
-     (= (name entity) "Database") `(comment "@(:can-read database) is always true.") ; put some non-constant value here so Eastwood doesn't complain about unused return values
-     :else                        `(read-check (sel :one ~entity :id ~id)))))
-
-(defmacro write-check
-  "Checks that `@can_write` is true for this object."
+   {:pre [(models/metabase-entity? entity)
+          (integer? id)]}
+   (if (satisfies? models/ICanReadWrite entity) (models/can-read? entity id)
+       (read-check (entity id)))))
+
+(defn write-check
+  "Check whether we can write an existing OBJ, or ENTITY with ID."
   ([obj]
-   `(let-404 [{:keys [~'can_write] :as obj#} ~obj]
-      (check-403 @~'can_write)
-      obj#))
+   (check-404 obj)
+   (check-403 (if (satisfies? models/ICanReadWrite obj) (models/can-write? obj)
+                  @(:can_write obj)))
+   obj)
   ([entity id]
-   `(write-check (sel :one ~entity :id ~id))))
+   {:pre [(models/metabase-entity? entity)
+          (integer? id)]}
+   (if (satisfies? models/ICanReadWrite entity) (models/can-write? entity id)
+       (write-check (entity id)))))
diff --git a/src/metabase/api/meta/table.clj b/src/metabase/api/meta/table.clj
index 552bac779f0..b4f3e4c653d 100644
--- a/src/metabase/api/meta/table.clj
+++ b/src/metabase/api/meta/table.clj
@@ -107,7 +107,4 @@
         new_order))
     {:result "success"}))
 
-;; TODO - GET /:id/segments
-;; TODO - POST /:id/segments
-
 (define-routes)
diff --git a/src/metabase/db/internal.clj b/src/metabase/db/internal.clj
index d20df092367..154e1413635 100644
--- a/src/metabase/db/internal.clj
+++ b/src/metabase/db/internal.clj
@@ -42,7 +42,7 @@
     *  Symbols like `'metabase.models.user/User` are handled the same way as strings."
   (memoize
    (fn -entity->korma [entity]
-     ;; {:post [(= (type %) :korma.core/Entity)]}
+     {:post [(:metabase.models.interface/entity %)]}
      (cond (vector? entity) (-entity->korma (first entity))
            (string? entity) (-entity->korma (symbol entity))
            (symbol? entity) (try (eval entity)
diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index 2709fdb72a2..338dbaf9f3d 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -17,6 +17,8 @@
     (cond-> this
       (not (:is_superuser @*current-user*)) (dissoc :details))))
 
+(extend-ICanReadWrite DatabaseInstance :read :always, :write :superuser)
+
 (defentity Database
   [(table :metabase_database)
    (hydration-keys database db)
@@ -24,10 +26,9 @@
    timestamped]
 
   (post-select [_ db]
-    (map->DatabaseInstance
-     (assoc db
-            :can_read  (delay true)
-            :can_write (delay (:is_superuser @*current-user*)))))
+    (map->DatabaseInstance db))
 
   (pre-cascade-delete [_ {:keys [id] :as database}]
     (cascade-delete 'metabase.models.table/Table :db_id id)))
+
+(extend-ICanReadWrite DatabaseEntity :read :always, :write :superuser)
diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj
index 9417e5a2d98..71272f40b16 100644
--- a/src/metabase/models/field.clj
+++ b/src/metabase/models/field.clj
@@ -74,6 +74,14 @@
     :info        ; Non-numerical value that is not meant to be used
     :sensitive}) ; A Fields that should *never* be shown *anywhere*
 
+(defrecord FieldInstance []
+  clojure.lang.IFn
+  (invoke [this k]
+    (get this k)))
+
+(extend-ICanReadWrite FieldInstance :read :always, :write :superuser)
+
+
 (defentity Field
   [(table :metabase_field)
    (hydration-keys destination field origin)
@@ -100,19 +108,21 @@
       (future (create-field-values-if-needed (sel :one [this :id :table_id :base_type :special_type :field_type] :id id)))))
 
   (post-select [_ {:keys [table_id] :as field}]
-    (u/assoc* field
-      :table               (delay (sel :one 'metabase.models.table/Table :id table_id))
-      :db                  (delay @(:db @(:table <>)))
-      :target              (delay (field->fk-field field))
-      :can_read            (delay @(:can_read @(:table <>)))
-      :can_write           (delay @(:can_write @(:table <>)))
-      :human_readable_name (when (name :field)
-                             (delay (common/name->human-readable-name (:name field))))))
+    (map->FieldInstance
+      (u/assoc* field
+        :table               (delay (sel :one 'metabase.models.table/Table :id table_id))
+        :db                  (delay @(:db @(:table <>)))
+        :target              (delay (field->fk-field field))
+        :human_readable_name (when (name :field)
+                               (delay (common/name->human-readable-name (:name field)))))))
 
   (pre-cascade-delete [_ {:keys [id]}]
     (cascade-delete ForeignKey (where (or (= :origin_id id)
                                           (= :destination_id id))))
-    (cascade-delete 'metabase.models.field-values/FieldValues :field_id id) ))
+    (cascade-delete 'metabase.models.field-values/FieldValues :field_id id)))
+
+(extend-ICanReadWrite FieldEntity :read :always, :write :superuser)
+
 
 (defn field->fk-field
   "Attempts to follow a `ForeignKey` from the the given `Field` to a destination `Field`.
diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj
index 8f882ad14e3..7cb1af3c617 100644
--- a/src/metabase/models/interface.clj
+++ b/src/metabase/models/interface.clj
@@ -8,6 +8,22 @@
             metabase.db.internal
             [metabase.util :as u]))
 
+(defprotocol ICanReadWrite
+  (can-read?  [obj] [entity ^Integer id])
+  (can-write? [obj] [entity ^Integer id]))
+
+(defn extend-ICanReadWrite
+  "Add standard implementations of `can-read?` and `can-write?` to KLASS."
+  [klass & {:keys [read write]}]
+  (let [key->method #(case %
+                       :always    (constantly true)
+                       :superuser (fn [& args]
+                                    (:is_superuser @@(resolve 'metabase.api.common/*current-user*))))]
+    (extend klass
+      ICanReadWrite {:can-read?  (key->method read)
+                     :can-write? (key->method write)})))
+
+
 ;;; ## ---------------------------------------- ENTITIES ----------------------------------------
 
 (defprotocol IEntity
@@ -56,14 +72,15 @@
 
 (defn- identity-second [_ obj] obj)
 (def ^:private constantly-nil (constantly nil))
+(def ^:private constantly-true (constantly true))
 
 (def ^:const ^:private default-entity-method-implementations
-  {:pre-insert           #'identity-second
-   :post-insert          #'identity-second
-   :pre-update           #'identity-second
-   :post-update          #'constantly-nil
-   :post-select          #'identity-second
-   :pre-cascade-delete   #'constantly-nil})
+  {:pre-insert         #'identity-second
+   :post-insert        #'identity-second
+   :pre-update         #'identity-second
+   :post-update        #'constantly-nil
+   :post-select        #'identity-second
+   :pre-cascade-delete #'constantly-nil})
 
 (def ^:const ^:private type-fns
   {:json    {:in  'metabase.db.internal/write-json
@@ -112,17 +129,19 @@
 
 (defmacro defentity
   "Similar to korma `defentity`, but creates a new record type where you can specify protocol implementations."
-  [entity entity-forms & methods]
-  {:pre [(vector? entity-forms)
-         (every? list? methods)]}
+  [entity entity-forms & methods+specs]
+  {:pre [(vector? entity-forms)]}
   (let [entity-symb               (symbol (format "%sEntity" (name entity)))
         internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity)))
-        entity-map                (eval `(macrolet-entity-map ~entity ~@entity-forms))]
+        entity-map                (eval `(macrolet-entity-map ~entity ~@entity-forms))
+        [methods specs]           (split-with list? methods+specs)]
     `(do
        (defrecord ~entity-symb []
          clojure.lang.IFn
          (~'invoke [~'this ~'id]
-           (-invoke-entity ~'this ~'id)))
+           (-invoke-entity ~'this ~'id))
+
+         ~@specs)
 
        (extend ~entity-symb
          IEntity ~(merge default-entity-method-implementations
@@ -140,7 +159,12 @@
                                (for [[method-name & impl] methods]
                                  {(keyword method-name) `(fn ~@impl)}))))
        (def ~entity
-         (~(symbol (format "map->%sEntity" (name entity))) ~entity-map)))))
+         (~(symbol (format "map->%sEntity" (name entity))) ~(assoc entity-map ::entity true))))))
+
+(defn metabase-entity?
+  "Is ENTITY a valid metabase model entity?"
+  [entity]
+  (::entity entity))
 
 
 ;;; # ---------------------------------------- INSTANCE ----------------------------------------
diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj
index 8978266fdce..f4a60161dd8 100644
--- a/src/metabase/models/table.clj
+++ b/src/metabase/models/table.clj
@@ -12,6 +12,13 @@
   "Valid values for `Table.entity_type` (field may also be `nil`)."
   #{:person :event :photo :place})
 
+(defrecord TableInstance []
+  clojure.lang.IFn
+  (invoke [this k]
+    (get this k)))
+
+(extend-ICanReadWrite TableInstance :read :always, :write :superuser)
+
 (defentity Table
   [(table :metabase_table)
    (hydration-keys table)
@@ -19,23 +26,24 @@
    timestamped]
 
   (post-select [_ {:keys [id db db_id description] :as table}]
-    (u/assoc* table
-      :db                  (or db (delay (sel :one db/Database :id db_id)))
-      :fields              (delay (sel :many Field :table_id id :active true (order :position :ASC) (order :name :ASC)))
-      :field_values        (delay
-                            (let [field-ids (sel :many :field [Field :id]
-                                                 :table_id id
-                                                 :active true
-                                                 :field_type [not= "sensitive"]
-                                                 (order :position :asc)
-                                                 (order :name :asc))]
-                              (sel :many :field->field [FieldValues :field_id :values] :field_id [in field-ids])))
-      :description         (u/jdbc-clob->str description)
-      :pk_field            (delay (:id (sel :one :fields [Field :id] :table_id id (where {:special_type "id"}))))
-      :can_read            (delay @(:can_read @(:db <>)))
-      :can_write           (delay @(:can_write @(:db <>)))
-      :human_readable_name (when (:name table)
-                             (delay (common/name->human-readable-name (:name table))))))
+    (map->TableInstance
+     (u/assoc* table
+       :db                  (or db (delay (sel :one db/Database :id db_id)))
+       :fields              (delay (sel :many Field :table_id id :active true (order :position :ASC) (order :name :ASC)))
+       :field_values        (delay
+                             (let [field-ids (sel :many :field [Field :id]
+                                                  :table_id id
+                                                  :active true
+                                                  :field_type [not= "sensitive"]
+                                                  (order :position :asc)
+                                                  (order :name :asc))]
+                               (sel :many :field->field [FieldValues :field_id :values] :field_id [in field-ids])))
+       :description         (u/jdbc-clob->str description)
+       :pk_field            (delay (:id (sel :one :fields [Field :id] :table_id id (where {:special_type "id"}))))
+       :human_readable_name (when (:name table)
+                              (delay (common/name->human-readable-name (:name table)))))))
 
   (pre-cascade-delete [_ {:keys [id] :as table}]
     (cascade-delete Field :table_id id)))
+
+(extend-ICanReadWrite TableEntity :read :always, :write :superuser)
-- 
GitLab