diff --git a/.dir-locals.el b/.dir-locals.el index 240f200944ce8031f67b547284a3efbc3f96a1aa..7e8f02585098cd448c17069d196d8052cc66ce13 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -33,6 +33,7 @@ (expect-with-all-drivers 1) (expect-with-dataset 1) (expect-with-datasets 1) + (format-color 2) (ins 1) (let-400 1) (let-404 1) diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj index 4c7b7068d7e367e28f71f1ebbc604dfa73085038..f1fee83d1f5dc9c202cf0bc6e15095b6e0b1b6d4 100644 --- a/src/metabase/api/routes.clj +++ b/src/metabase/api/routes.clj @@ -17,15 +17,15 @@ [tiles :as tiles] [user :as user] [util :as util]) - [metabase.middleware.auth :as auth])) + [metabase.middleware :as middleware])) (def ^:private +apikey "Wrap API-ROUTES so they may only be accessed with proper apikey credentials." - auth/enforce-api-key) + middleware/enforce-api-key) (def ^:private +auth "Wrap API-ROUTES so they may only be accessed with proper authentiaction credentials." - auth/enforce-authentication) + middleware/enforce-authentication) (defroutes routes (context "/activity" [] (+auth activity/routes)) diff --git a/src/metabase/core.clj b/src/metabase/core.clj index 0086c7b9723fb1f0ee642ada0e6fcdbfa1e70357..61e8b5e1ea5f3bf7e225111f71f3e156cdab64a6 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -1,8 +1,7 @@ ;; -*- comment-column: 35; -*- (ns metabase.core (:gen-class) - (:require [clojure.java.browse :refer [browse-url]] - [clojure.string :as s] + (:require [clojure.string :as s] [clojure.tools.logging :as log] [colorize.core :as color] [ring.adapter.jetty :as ring-jetty] @@ -13,17 +12,15 @@ [keyword-params :refer [wrap-keyword-params]] [params :refer [wrap-params]] [session :refer [wrap-session]]) - [medley.core :as medley] + [medley.core :as m] (metabase [config :as config] [db :as db] [driver :as driver] [events :as events] + [middleware :as mb-middleware] [routes :as routes] [setup :as setup] [task :as task]) - (metabase.middleware [auth :as auth] - [log-api-call :refer :all] - [format :refer :all]) (metabase.models [setting :refer [defsetting]] [database :refer [Database]] [user :refer [User]]))) @@ -54,21 +51,21 @@ (def app "The primary entry point to the HTTP server" (-> routes/routes - (log-api-call :request :response) - add-security-headers ; [METABASE] Add HTTP headers to API responses to prevent them from being cached - format-response ; [METABASE] Do formatting before converting to JSON so serializer doesn't barf - (wrap-json-body ; extracts json POST body and makes it avaliable on request + (mb-middleware/log-api-call :request :response) + mb-middleware/add-security-headers ; [METABASE] Add HTTP headers to API responses to prevent them from being cached + mb-middleware/format-response ; [METABASE] Do formatting before converting to JSON so serializer doesn't barf + (wrap-json-body ; extracts json POST body and makes it avaliable on request {:keywords? true}) - wrap-json-response ; middleware to automatically serialize suitable objects as JSON in responses - wrap-keyword-params ; converts string keys in :params to keyword keys - wrap-params ; parses GET and POST params as :query-params/:form-params and both as :params - auth/bind-current-user ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil - auth/wrap-current-user-id ; looks for :metabase-session-id and sets :metabase-user-id if Session ID is valid - auth/wrap-api-key ; looks for a Metabase API Key on the request and assocs as :metabase-api-key - auth/wrap-session-id ; looks for a Metabase Session ID and assoc as :metabase-session-id - wrap-cookies ; Parses cookies in the request map and assocs as :cookies - wrap-session ; reads in current HTTP session and sets :session/key - wrap-gzip)) ; GZIP response if client can handle it + wrap-json-response ; middleware to automatically serialize suitable objects as JSON in responses + wrap-keyword-params ; converts string keys in :params to keyword keys + wrap-params ; parses GET and POST params as :query-params/:form-params and both as :params + mb-middleware/bind-current-user ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil + mb-middleware/wrap-current-user-id ; looks for :metabase-session-id and sets :metabase-user-id if Session ID is valid + mb-middleware/wrap-api-key ; looks for a Metabase API Key on the request and assocs as :metabase-api-key + mb-middleware/wrap-session-id ; looks for a Metabase Session ID and assoc as :metabase-session-id + wrap-cookies ; Parses cookies in the request map and assocs as :cookies + wrap-session ; reads in current HTTP session and sets :session/key + wrap-gzip)) ; GZIP response if client can handle it (defn- -init-create-setup-token "Create and set a new setup token, and open the setup URL on the user's system." @@ -129,14 +126,14 @@ "Start the embedded Jetty web server." [] (when-not @jetty-instance - (let [jetty-config (cond-> (medley/filter-vals identity {:port (config/config-int :mb-jetty-port) - :host (config/config-str :mb-jetty-host) - :max-threads (config/config-int :mb-jetty-maxthreads) - :min-threads (config/config-int :mb-jetty-minthreads) - :max-queued (config/config-int :mb-jetty-maxqueued) - :max-idle-time (config/config-int :mb-jetty-maxidletime)}) - (config/config-str :mb-jetty-join) (assoc :join? (config/config-bool :mb-jetty-join)) - (config/config-str :mb-jetty-daemon) (assoc :daemon? (config/config-bool :mb-jetty-daemon)))] + (let [jetty-config (cond-> (m/filter-vals identity {:port (config/config-int :mb-jetty-port) + :host (config/config-str :mb-jetty-host) + :max-threads (config/config-int :mb-jetty-maxthreads) + :min-threads (config/config-int :mb-jetty-minthreads) + :max-queued (config/config-int :mb-jetty-maxqueued) + :max-idle-time (config/config-int :mb-jetty-maxidletime)}) + (config/config-str :mb-jetty-join) (assoc :join? (config/config-bool :mb-jetty-join)) + (config/config-str :mb-jetty-daemon) (assoc :daemon? (config/config-bool :mb-jetty-daemon)))] (log/info "Launching Embedded Jetty Webserver with config:\n" (with-out-str (clojure.pprint/pprint jetty-config))) (->> (ring-jetty/run-jetty app jetty-config) (reset! jetty-instance))))) diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj new file mode 100644 index 0000000000000000000000000000000000000000..2432a1df06bcda093d857c013e37b9ddf7cc2c83 --- /dev/null +++ b/src/metabase/middleware.clj @@ -0,0 +1,239 @@ +(ns metabase.middleware + "Metabase-specific middleware functions & configuration." + (:require [clojure.math.numeric-tower :as math] + [clojure.tools.logging :as log] + [clojure.walk :as walk] + (cheshire factory + [generate :refer [add-encoder encode-str]]) + [korma.core :as k] + [medley.core :refer [filter-vals map-vals]] + [metabase.api.common :refer [*current-user* *current-user-id*]] + [metabase.config :as config] + [metabase.db :refer [sel]] + (metabase.models [interface :refer [api-serialize]] + [session :refer [Session]] + [user :refer [User]]) + [metabase.util :as u])) + +;;; # ------------------------------------------------------------ UTIL FNS ------------------------------------------------------------ + +(defn- api-call? + "Is this ring request an API call (does path start with `/api`)?" + [{:keys [^String uri]}] + (and (>= (count uri) 4) + (= (.substring uri 0 4) "/api"))) + + +;;; # ------------------------------------------------------------ AUTH & SESSION MANAGEMENT ------------------------------------------------------------ + +(def ^:const metabase-session-cookie "metabase.SESSION_ID") +(def ^:const metabase-session-header "x-metabase-session") +(def ^:const metabase-api-key-header "x-metabase-apikey") + +(def ^:const response-unauthentic {:status 401 :body "Unauthenticated"}) +(def ^:const response-forbidden {:status 403 :body "Forbidden"}) + + +(defn wrap-session-id + "Middleware that sets the `:metabase-session-id` keyword on the request if a session id can be found. + + We first check the request :cookies for `metabase.SESSION_ID`, then if no cookie is found we look in the + http headers for `X-METABASE-SESSION`. If neither is found then then no keyword is bound to the request." + [handler] + (fn [{:keys [cookies headers] :as request}] + (if-let [session-id (or (get-in cookies [metabase-session-cookie :value]) (headers metabase-session-header))] + ;; alternatively we could always associate the keyword and just let it be nil if there is no value + (handler (assoc request :metabase-session-id session-id)) + (handler request)))) + + +(defn wrap-current-user-id + "Add `:metabase-user-id` to the request if a valid session token was passed." + [handler] + (fn [{:keys [metabase-session-id] :as request}] + ;; TODO - what kind of validations can we do on the sessionid to make sure it's safe to handle? str? alphanumeric? + (handler (or (when metabase-session-id + (when-let [session (first (k/select Session + ;; NOTE: we join with the User table and ensure user.is_active = true + (k/with User (k/where {:is_active true})) + (k/fields :created_at :user_id) + (k/where {:id metabase-session-id})))] + (let [session-age-ms (- (System/currentTimeMillis) (.getTime ^java.util.Date (get session :created_at (java.util.Date. 0))))] + ;; If the session exists and is not expired (max-session-age > session-age) then validation is good + (when (and session (> (config/config-int :max-session-age) (quot session-age-ms 60000))) + (assoc request :metabase-user-id (:user_id session)))))) + request)))) + + +(defn enforce-authentication + "Middleware that returns a 401 response if REQUEST has no associated `:metabase-user-id`." + [handler] + (fn [{:keys [metabase-user-id] :as request}] + (if metabase-user-id + (handler request) + response-unauthentic))) + + +(defn bind-current-user + "Middleware that binds `metabase.api.common/*current-user*` and `*current-user-id*` + + * `*current-user-id*` int ID or nil of user associated with request + * `*current-user*` delay that returns current user (or nil) from DB" + [handler] + (fn [request] + (if-let [current-user-id (:metabase-user-id request)] + (binding [*current-user-id* current-user-id + *current-user* (delay (sel :one `[User ~@(:metabase.models.interface/default-fields User) :is_active :is_staff], :id current-user-id))] + (handler request)) + (handler request)))) + + +(defn wrap-api-key + "Middleware that sets the `:metabase-api-key` keyword on the request if a valid API Key can be found. + We check the request headers for `X-METABASE-APIKEY` and if it's not found then then no keyword is bound to the request." + [handler] + (fn [{:keys [headers] :as request}] + (if-let [api-key (headers metabase-api-key-header)] + (handler (assoc request :metabase-api-key api-key)) + (handler request)))) + + +(defn enforce-api-key + "Middleware that enforces validation of the client via API Key, cancelling the request processing if the check fails. + + Validation is handled by first checking for the presence of the `:metabase-api-key` on the request. If the api key + is available then we validate it by checking it against the configured `:mb-api-key` value set in our global config. + + If the request `:metabase-api-key` matches the configured `:mb-api-key` value then the request continues, otherwise we + reject the request and return a 403 Forbidden response." + [handler] + (fn [{:keys [metabase-api-key] :as request}] + (if (= (config/config-str :mb-api-key) metabase-api-key) + (handler request) + ;; default response is 403 + response-forbidden))) + + +;;; # ------------------------------------------------------------ SECURITY HEADERS ------------------------------------------------------------ + +(defn add-security-headers + "Add HTTP headers to tell browsers not to cache API responses." + [handler] + (fn [request] + (let [response (handler request)] + (update response :headers merge (when (api-call? request) + {"Cache-Control" "max-age=0, no-cache, must-revalidate, proxy-revalidate" + "Expires" "Tue, 03 Jul 2001 06:00:00 GMT" ; rando date in the past + "Last-Modified" "{now} GMT"}))))) + + +;;; # ------------------------------------------------------------ JSON SERIALIZATION CONFIG ------------------------------------------------------------ + +;; Tell the JSON middleware to use a date format that includes milliseconds +(intern 'cheshire.factory 'default-date-format "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + +;; ## Custom JSON encoders + +;; stringify JDBC clobs +(add-encoder org.h2.jdbc.JdbcClob (fn [clob ^com.fasterxml.jackson.core.JsonGenerator json-generator] + (.writeString json-generator (u/jdbc-clob->str clob)))) + +;; stringify Postgres binary objects (e.g. PostGIS geometries) +(add-encoder org.postgresql.util.PGobject encode-str) + +;; Do the same for PG arrays +(add-encoder org.postgresql.jdbc4.Jdbc4Array encode-str) + +;; Encode BSON IDs like strings +(add-encoder org.bson.types.ObjectId encode-str) + +;; serialize sql dates (i.e., QueryProcessor results) like YYYY-MM-DD instead of as a full-blown timestamp +(add-encoder java.sql.Date (fn [^java.sql.Date date ^com.fasterxml.jackson.core.JsonGenerator json-generator] + (.writeString json-generator (.toString date)))) + +(defn- remove-fns-and-delays + "Remove values that are fns or delays from map M." + [m] + (filter-vals #(not (or (delay? %) + (fn? %))) + ;; Convert typed maps such as metabase.models.database/DatabaseInstance to plain maps because empty, which is used internally by filter-vals, + ;; will fail otherwise + (into {} m))) + +(defn format-response + "Middleware that recurses over Clojure object before it gets converted to JSON and makes adjustments neccessary so the formatter doesn't barf. + e.g. functions and delays are stripped and H2 Clobs are converted to strings." + [handler] + (let [-format-response (fn -format-response [obj] + (cond + (map? obj) (->> (api-serialize obj) + remove-fns-and-delays + (map-vals -format-response)) ; recurse over all vals in the map + (coll? obj) (map -format-response obj) ; recurse over all items in the collection + :else obj))] + (fn [request] + (-format-response (handler request))))) + + + +;;; # ------------------------------------------------------------ LOGGING ------------------------------------------------------------ + +(def ^:private ^:const sensitive-fields + "Fields that we should censor before logging." + #{:password}) + +(defn- scrub-sensitive-fields + "Replace values of fields in `sensitive-fields` with `\"**********\"` before logging." + [request] + (walk/prewalk (fn [form] + (if-not (and (vector? form) + (= (count form) 2) + (keyword? (first form)) + (contains? sensitive-fields (first form))) + form + [(first form) "**********"])) + request)) + +(defn- log-request [{:keys [uri request-method body query-string]}] + (log/debug (u/format-color 'blue "%s %s " + (.toUpperCase (name request-method)) (str uri + (when-not (empty? query-string) + (str "?" query-string))) + (when (or (string? body) (coll? body)) + (str "\n" (u/pprint-to-str (scrub-sensitive-fields body))))))) + +(defn- log-response [{:keys [uri request-method]} {:keys [status body]} elapsed-time] + (let [log-error #(log/error %) ; these are macros so we can't pass by value :sad: + log-debug #(log/debug %) + log-warn #(log/warn %) + [error? color log-fn] (cond + (>= status 500) [true 'red log-error] + (= status 403) [true 'red log-warn] + (>= status 400) [true 'red log-debug] + :else [false 'green log-debug])] + (log-fn (str (u/format-color color "%s %s %d (%d ms)" (.toUpperCase (name request-method)) uri status elapsed-time) + ;; only print body on error so we don't pollute our environment by over-logging + (when (and error? + (or (string? body) (coll? body))) + (str "\n" (u/pprint-to-str body))))))) + +(defn log-api-call + "Middleware to log `:request` and/or `:response` by passing corresponding OPTIONS." + [handler & options] + (let [{:keys [request response]} (set options) + log-request? request + log-response? response] + (fn [request] + (if-not (api-call? request) (handler request) + (do + (when log-request? + (log-request request)) + (let [start-time (System/nanoTime) + response (handler request) + elapsed-time (-> (- (System/nanoTime) start-time) + double + (/ 1000000.0) + math/round)] + (when log-response? + (log-response request response elapsed-time)) + response)))))) diff --git a/src/metabase/middleware/auth.clj b/src/metabase/middleware/auth.clj deleted file mode 100644 index 3e99d6eb9cb4ec201e44fc5bca2b46d70ba30356..0000000000000000000000000000000000000000 --- a/src/metabase/middleware/auth.clj +++ /dev/null @@ -1,101 +0,0 @@ -(ns metabase.middleware.auth - "Middleware for dealing with authentication and session management." - (:require [korma.core :as k] - [metabase.config :as config] - [metabase.db :refer [sel]] - [metabase.api.common :refer [*current-user* *current-user-id*]] - (metabase.models [session :refer [Session]] - [user :refer [User current-user-fields]]))) - - -(def ^:const metabase-session-cookie "metabase.SESSION_ID") -(def ^:const metabase-session-header "x-metabase-session") -(def ^:const metabase-api-key-header "x-metabase-apikey") - -(def ^:const response-unauthentic {:status 401 :body "Unauthenticated"}) -(def ^:const response-forbidden {:status 403 :body "Forbidden"}) - - -(defn wrap-session-id - "Middleware that sets the `:metabase-session-id` keyword on the request if a session id can be found. - - We first check the request :cookies for `metabase.SESSION_ID`, then if no cookie is found we look in the - http headers for `X-METABASE-SESSION`. If neither is found then then no keyword is bound to the request." - [handler] - (fn [{:keys [cookies headers] :as request}] - (if-let [session-id (or (get-in cookies [metabase-session-cookie :value]) (headers metabase-session-header))] - ;; alternatively we could always associate the keyword and just let it be nil if there is no value - (handler (assoc request :metabase-session-id session-id)) - (handler request)))) - -(defn wrap-current-user-id - "Add `:metabase-user-id` to the request if a valid session token was passed." - [handler] - (fn [{:keys [metabase-session-id] :as request}] - ;; TODO - what kind of validations can we do on the sessionid to make sure it's safe to handle? str? alphanumeric? - (handler (or (when metabase-session-id - (when-let [session (first (k/select Session - ;; NOTE: we join with the User table and ensure user.is_active = true - (k/with User (k/where {:is_active true})) - (k/fields :created_at :user_id) - (k/where {:id metabase-session-id})))] - (let [session-age-ms (- (System/currentTimeMillis) (.getTime ^java.util.Date (get session :created_at (java.util.Date. 0))))] - ;; If the session exists and is not expired (max-session-age > session-age) then validation is good - (when (and session (> (config/config-int :max-session-age) (quot session-age-ms 60000))) - (assoc request :metabase-user-id (:user_id session)))))) - request)))) - - -(defn enforce-authentication - "Middleware that returns a 401 response if REQUEST has no associated `:metabase-user-id`." - [handler] - (fn [{:keys [metabase-user-id] :as request}] - (if metabase-user-id - (handler request) - response-unauthentic))) - - -(defmacro sel-current-user [current-user-id] - `(sel :one [User ~@current-user-fields] - :id ~current-user-id)) - - -(defn bind-current-user - "Middleware that binds `metabase.api.common/*current-user*` and `*current-user-id*` - - *current-user-id* int ID or nil of user associated with request - *current-user* delay that returns current user (or nil) from DB" - [handler] - (fn [request] - (if-let [current-user-id (:metabase-user-id request)] - (binding [*current-user-id* current-user-id - *current-user* (delay (sel-current-user current-user-id))] - (handler request)) - (handler request)))) - - -(defn wrap-api-key - "Middleware that sets the :metabase-api-key keyword on the request if a valid API Key can be found. - - We check the request headers for `X-METABASE-APIKEY` and if it's not found then then no keyword is bound to the request." - [handler] - (fn [{:keys [headers] :as request}] - (if-let [api-key (headers metabase-api-key-header)] - (handler (assoc request :metabase-api-key api-key)) - (handler request)))) - - -(defn enforce-api-key - "Middleware that enforces validation of the client via API Key, cancelling the request processing if the check fails. - - Validation is handled by first checking for the presence of the :metabase-api-key on the request. If the api key - is available then we validate it by checking it against the configured :mb-api-key value set in our global config. - - If the request :metabase-api-key matches the configured :mb-api-key value then the request continues, otherwise we - reject the request and return a 403 Forbidden response." - [handler] - (fn [{:keys [metabase-api-key] :as request}] - (if (= (config/config-str :mb-api-key) metabase-api-key) - (handler request) - ;; default response is 403 - response-forbidden))) diff --git a/src/metabase/middleware/format.clj b/src/metabase/middleware/format.clj deleted file mode 100644 index 36117d5967d117c96f98f5f5bceb58b93adfc064..0000000000000000000000000000000000000000 --- a/src/metabase/middleware/format.clj +++ /dev/null @@ -1,69 +0,0 @@ -(ns metabase.middleware.format - (:require [clojure.core.match :refer [match]] - (cheshire factory - [generate :refer [add-encoder encode-str]]) - [medley.core :refer [filter-vals map-vals]] - [metabase.middleware.log-api-call :refer [api-call?]] - [metabase.models.interface :refer [api-serialize]] - [metabase.util :as util])) - -(declare -format-response) - -;; ## SHADY HACK -;; Tell the JSON middleware to use a date format that includes milliseconds -(intern 'cheshire.factory 'default-date-format "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - - -;; ## Custom JSON encoders - -;; stringify JDBC clobs -(add-encoder org.h2.jdbc.JdbcClob (fn [clob ^com.fasterxml.jackson.core.JsonGenerator json-generator] - (.writeString json-generator (util/jdbc-clob->str clob)))) - -;; stringify Postgres binary objects (e.g. PostGIS geometries) -(add-encoder org.postgresql.util.PGobject encode-str) - -;; Do the same for PG arrays -(add-encoder org.postgresql.jdbc4.Jdbc4Array encode-str) - -;; Encode BSON IDs like strings -(add-encoder org.bson.types.ObjectId encode-str) - -;; serialize sql dates (i.e., QueryProcessor results) like YYYY-MM-DD instead of as a full-blown timestamp -(add-encoder java.sql.Date (fn [^java.sql.Date date ^com.fasterxml.jackson.core.JsonGenerator json-generator] - (.writeString json-generator (.toString date)))) - -(defn add-security-headers - "Add HTTP headers to tell browsers not to cache API responses." - [handler] - (fn [request] - (let [response (handler request)] - (update response :headers merge (when (api-call? request) - {"Cache-Control" "max-age=0, no-cache, must-revalidate, proxy-revalidate" - "Expires" "Tue, 03 Jul 2001 06:00:00 GMT" ; rando date in the past - "Last-Modified" "{now} GMT"}))))) - -;; ## FORMAT RESPONSE MIDDLEWARE -(defn format-response - "Middleware that recurses over Clojure object before it gets converted to JSON and makes adjustments neccessary so the formatter doesn't barf. - e.g. functions and delays are stripped and H2 Clobs are converted to strings." - [handler] - (fn [request] - (-format-response (handler request)))) - -(defn- remove-fns-and-delays - "Remove values that are fns or delays from map M." - [m] - (filter-vals #(not (or (delay? %) - (fn? %))) - ;; Convert typed maps such as metabase.models.database/DatabaseInstance to plain maps because empty, which is used internally by filter-vals, - ;; will fail otherwise - (into {} m))) - -(defn- -format-response [obj] - (cond - (map? obj) (->> (api-serialize obj) - remove-fns-and-delays - (map-vals -format-response)) ; recurse over all vals in the map - (coll? obj) (map -format-response obj) ; recurse over all items in the collection - :else obj)) diff --git a/src/metabase/middleware/log_api_call.clj b/src/metabase/middleware/log_api_call.clj deleted file mode 100644 index 4e096109054f30f9124bad21a653f3760453dc67..0000000000000000000000000000000000000000 --- a/src/metabase/middleware/log_api_call.clj +++ /dev/null @@ -1,78 +0,0 @@ -(ns metabase.middleware.log-api-call - "Middleware to log API calls. Primarily for debugging purposes." - (:require [clojure.math.numeric-tower :as math] - [clojure.pprint :refer [pprint]] - [clojure.tools.logging :as log] - [colorize.core :as color])) - -(declare api-call? - log-request - log-response) - -(def ^:private sensitive-fields - "Fields that we should censor before logging." - #{:password}) - -(defn- scrub-sensitive-fields - "Replace values of fields in `sensitive-fields` with `\"**********\"` before logging." - [request] - (clojure.walk/prewalk (fn [form] - (if-not (and (vector? form) - (= (count form) 2) - (keyword? (first form)) - (contains? sensitive-fields (first form))) - form - [(first form) "**********"])) - request)) - -(def ^:private only-display-output-on-error - "Set this to `false` to see all API responses." - true) - -(defn log-api-call - "Middleware to log `:request` and/or `:response` by passing corresponding OPTIONS." - [handler & options] - (let [{:keys [request response]} (set options) - log-request? request - log-response? response] - (fn [request] - (if-not (api-call? request) (handler request) - (do - (when log-request? - (log-request request)) - (let [start-time (System/nanoTime) - response (handler request) - elapsed-time (-> (- (System/nanoTime) start-time) - double - (/ 1000000.0) - math/round)] - (when log-response? - (log-response request response elapsed-time)) - response)))))) - -(defn api-call? - "Is this ring request an API call (does path start with `/api`)?" - [{:keys [^String uri]}] - (and (>= (count uri) 4) - (= (.substring uri 0 4) "/api"))) - -(defn- log-request [{:keys [uri request-method body query-string]}] - (log/debug (color/blue (format "%s %s " (.toUpperCase (name request-method)) (str uri - (when-not (empty? query-string) - (str "?" query-string)))) - (when (or (string? body) (coll? body)) - (str "\n" (with-out-str (pprint (scrub-sensitive-fields body)))))))) - -(defn- log-response [{:keys [uri request-method]} {:keys [status body]} elapsed-time] - (let [log-error (fn [& args] (log/error (apply str args))) ; inconveniently these are not macros - log-debug (fn [& args] (log/debug (apply str args))) - log-warn (fn [& args] (log/warn (apply str args))) - [error? color-fn log-fn] (cond - (>= status 500) [true color/red log-error] - (= status 403) [true color/red log-warn] - (>= status 400) [true color/red log-debug] - :else [false color/green log-debug])] - (log-fn (color-fn (format "%s %s %d (%d ms)" (.toUpperCase (name request-method)) uri status elapsed-time) - (when (or error? (not only-display-output-on-error)) - (when (or (string? body) (coll? body)) - (str "\n" (with-out-str (pprint body))))))))) diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index ce14738436684a5af2bccc01a2e8c5615c962ad9..33764008815948b9db2905ce77068cdc31fe0814 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -53,13 +53,6 @@ (cascade-delete 'ViewLog :user_id id))) -(def ^:const current-user-fields - "The fields we should return for `*current-user*` (used by `metabase.middleware.current-user`)" - (concat (:metabase.models.interface/default-fields User) - [:is_active - :is_staff])) ; but not `password` ! - - ;; ## Related Functions (declare create-user diff --git a/test/metabase/api/notify_test.clj b/test/metabase/api/notify_test.clj index 736f87eda7384211495e6f240206b71e8ddb7aba..bf937c6ee31bdf30c9eca6116b4803ed914df630 100644 --- a/test/metabase/api/notify_test.clj +++ b/test/metabase/api/notify_test.clj @@ -1,15 +1,15 @@ (ns metabase.api.notify-test (:require [clj-http.lite.client :as client] [expectations :refer :all] - [metabase.http-client :as http] - [metabase.middleware.auth :as auth])) + (metabase [http-client :as http] + [middleware :as middleware]))) ;; ## /api/notify/* AUTHENTICATION Tests ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint -(expect (get auth/response-forbidden :body) (http/client :post 403 "notify/db/100")) +(expect (get middleware/response-forbidden :body) (http/client :post 403 "notify/db/100")) ;; ## POST /api/notify/db/:id diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index 0a36de959fcc9b9f9b453e11c6ee9b4fc877e665..03a3aa842852e15c22cd8537aa588efe0f7be34b 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -3,8 +3,8 @@ (:require [expectations :refer :all] [metabase.db :refer :all] [metabase.driver.mongo.test-data :as mongo-data :refer [mongo-test-db-id]] - [metabase.http-client :as http] - [metabase.middleware.auth :as auth] + (metabase [http-client :as http] + [middleware :as middleware]) (metabase.models [field :refer [Field]] [foreign-key :refer [ForeignKey]] [table :refer [Table]]) @@ -19,8 +19,8 @@ ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint -(expect (get auth/response-unauthentic :body) (http/client :get 401 "table")) -(expect (get auth/response-unauthentic :body) (http/client :get 401 (format "table/%d" (id :users)))) +(expect (get middleware/response-unauthentic :body) (http/client :get 401 "table")) +(expect (get middleware/response-unauthentic :body) (http/client :get 401 (format "table/%d" (id :users)))) ;; ## GET /api/table?org diff --git a/test/metabase/api/user_test.clj b/test/metabase/api/user_test.clj index 3c3dabee270215cc7364e2d0db2c93321e92f3fd..3d7ca755df13cc0c8564ae0ab3ee7906e322717d 100644 --- a/test/metabase/api/user_test.clj +++ b/test/metabase/api/user_test.clj @@ -3,8 +3,8 @@ (:require [expectations :refer :all] [korma.core :as k] [metabase.db :refer :all] - [metabase.http-client :as http] - [metabase.middleware.auth :as auth] + (metabase [http-client :as http] + [middleware :as middleware]) (metabase.models [session :refer [Session]] [user :refer [User]]) [metabase.test.data :refer :all] @@ -15,8 +15,8 @@ ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint -(expect (get auth/response-unauthentic :body) (http/client :get 401 "user")) -(expect (get auth/response-unauthentic :body) (http/client :get 401 "user/current")) +(expect (get middleware/response-unauthentic :body) (http/client :get 401 "user")) +(expect (get middleware/response-unauthentic :body) (http/client :get 401 "user/current")) ;; ## Helper Fns diff --git a/test/metabase/middleware/format_test.clj b/test/metabase/middleware/format_test.clj deleted file mode 100644 index 0bf5372a44c9d9eaf21767738dce86666b1b4765..0000000000000000000000000000000000000000 --- a/test/metabase/middleware/format_test.clj +++ /dev/null @@ -1,29 +0,0 @@ -(ns metabase.middleware.format-test - (:require [expectations :refer :all] - [metabase.middleware.format :refer :all])) - -;; `format`, being a middleware function, expects a `handler` -;; and returns a function that actually affects the response. -;; Since we're just interested in testing the returned function pass it `identity` as a handler -;; so whatever we pass it is unaffected -(def fmt (format-response identity)) - -;; check basic stripping -(expect {:a 1} - (fmt {:a 1 - :b (fn [] 2)})) - -;; check recursive stripping w/ map -(expect {:response {:a 1}} - (fmt {:response {:a 1 - :b (fn [] 2)}})) - -;; check recursive stripping w/ array -(expect [{:a 1}] - (fmt [{:a 1 - :b (fn [] 2)}])) - -;; check combined recursive stripping -(expect [{:a [{:b 1}]}] - (fmt [{:a [{:b 1 - :c (fn [] 2)} ]}])) diff --git a/test/metabase/middleware/auth_test.clj b/test/metabase/middleware_test.clj similarity index 85% rename from test/metabase/middleware/auth_test.clj rename to test/metabase/middleware_test.clj index 58ac400655c86014fe3e3009d7c51339cfa6350f..002c168cb552a5a7b523abb5a7aaf9f3e2f752ec 100644 --- a/test/metabase/middleware/auth_test.clj +++ b/test/metabase/middleware_test.clj @@ -1,9 +1,9 @@ -(ns metabase.middleware.auth-test +(ns metabase.middleware-test (:require [expectations :refer :all] [korma.core :as k] [ring.mock.request :as mock] [metabase.api.common :refer [*current-user-id* *current-user*]] - [metabase.middleware.auth :refer :all] + [metabase.middleware :refer :all] [metabase.models.session :refer [Session]] [metabase.test.data :refer :all] [metabase.test.data.users :refer :all] @@ -170,3 +170,33 @@ ;; invalid apikey, expect 403 (expect response-forbidden (api-key-enforced-handler (request-with-api-key "foobar"))) + + + +;;; # ------------------------------------------------------------ FORMATTING TESTS ------------------------------------------------------------ + +;; `format`, being a middleware function, expects a `handler` +;; and returns a function that actually affects the response. +;; Since we're just interested in testing the returned function pass it `identity` as a handler +;; so whatever we pass it is unaffected +(def fmt (format-response identity)) + +;; check basic stripping +(expect {:a 1} + (fmt {:a 1 + :b (fn [] 2)})) + +;; check recursive stripping w/ map +(expect {:response {:a 1}} + (fmt {:response {:a 1 + :b (fn [] 2)}})) + +;; check recursive stripping w/ array +(expect [{:a 1}] + (fmt [{:a 1 + :b (fn [] 2)}])) + +;; check combined recursive stripping +(expect [{:a [{:b 1}]}] + (fmt [{:a [{:b 1 + :c (fn [] 2)} ]}]))