Skip to content
Snippets Groups Projects
Commit a3314aef authored by Cam Saul's avatar Cam Saul
Browse files

Strip DB details from API calls for non-admins

parent cd0da5a1
No related branches found
No related tags found
No related merge requests found
...@@ -15,18 +15,13 @@ ...@@ -15,18 +15,13 @@
[table :as table]) [table :as table])
[metabase.middleware.auth :as auth])) [metabase.middleware.auth :as auth]))
(defn- +apikey (def ^:private +apikey
"Wrap API-ROUTES so they may only be accessed with proper apikey credentials." "Wrap API-ROUTES so they may only be accessed with proper apikey credentials."
[api-routes] auth/enforce-api-key)
(-> api-routes
auth/enforce-apikey))
(defn- +auth (def ^:private +auth
"Wrap API-ROUTES so they may only be accessed with proper authentiaction credentials." "Wrap API-ROUTES so they may only be accessed with proper authentiaction credentials."
[api-routes] auth/enforce-authentication)
(-> api-routes
auth/bind-current-user
auth/enforce-authentication))
(defroutes routes (defroutes routes
(context "/card" [] (+auth card/routes)) (context "/card" [] (+auth card/routes))
......
;; -*- comment-column: 35; -*-
(ns metabase.core (ns metabase.core
(:gen-class) (:gen-class)
(:require [clojure.tools.logging :as log] (:require [clojure.tools.logging :as log]
[clojure.java.browse :refer [browse-url]] [clojure.java.browse :refer [browse-url]]
[colorize.core :as color] [colorize.core :as color]
[medley.core :as medley]
[metabase.config :as config]
[metabase.db :as db]
(metabase.middleware [auth :as auth]
[log-api-call :refer :all]
[format :refer :all])
[metabase.models.setting :refer [defsetting]]
[metabase.models.user :refer [User]]
[metabase.routes :as routes]
[metabase.setup :as setup]
[metabase.task :as task]
[ring.adapter.jetty :as ring-jetty] [ring.adapter.jetty :as ring-jetty]
(ring.middleware [cookies :refer [wrap-cookies]] (ring.middleware [cookies :refer [wrap-cookies]]
[gzip :refer [wrap-gzip]] [gzip :refer [wrap-gzip]]
...@@ -21,7 +11,18 @@ ...@@ -21,7 +11,18 @@
wrap-json-body]] wrap-json-body]]
[keyword-params :refer [wrap-keyword-params]] [keyword-params :refer [wrap-keyword-params]]
[params :refer [wrap-params]] [params :refer [wrap-params]]
[session :refer [wrap-session]]))) [session :refer [wrap-session]])
[medley.core :as medley]
(metabase [config :as config]
[db :as db]
[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]]
[user :refer [User]])))
;; ## CONFIG ;; ## CONFIG
...@@ -32,17 +33,19 @@ ...@@ -32,17 +33,19 @@
"The primary entry point to the HTTP server" "The primary entry point to the HTTP server"
(-> routes/routes (-> routes/routes
(log-api-call :request :response) (log-api-call :request :response)
format-response ; [METABASE] Do formatting before converting to JSON so serializer doesn't barf 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 (wrap-json-body ; extracts json POST body and makes it avaliable on request
{:keywords? true}) {:keywords? true})
wrap-json-response ; middleware to automatically serialize suitable objects as JSON in responses 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-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 wrap-params ; parses GET and POST params as :query-params/:form-params and both as :params
auth/wrap-apikey ; looks for a Metabase API Key on the request and assocs as :metabase-apikey auth/bind-current-user ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil
auth/wrap-sessionid ; looks for a Metabase sessionid and assocs as :metabase-sessionid auth/wrap-current-user-id ; looks for :metabase-session-id and sets :metabase-user-id if Session ID is valid
wrap-cookies ; Parses cookies in the request map and assocs as :cookies auth/wrap-api-key ; looks for a Metabase API Key on the request and assocs as :metabase-api-key
wrap-session ; reads in current HTTP session and sets :session/key auth/wrap-session-id ; looks for a Metabase Session ID and assoc as :metabase-session-id
wrap-gzip)) ; GZIP response if client can handle it 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 (defn- -init-create-setup-token
"Create and set a new setup token, and open the setup URL on the user's system." "Create and set a new setup token, and open the setup URL on the user's system."
...@@ -57,10 +60,7 @@ ...@@ -57,10 +60,7 @@
setup-token)] setup-token)]
(log/info (color/green "Please use the following url to setup your Metabase installation:\n\n" (log/info (color/green "Please use the following url to setup your Metabase installation:\n\n"
setup-url setup-url
"\n\n")) "\n\n"))))
;; Attempt to browse URL on user's system; this will just fail silently if we can't do it
;(browse-url setup-url)
))
(defn init (defn init
......
(ns metabase.middleware.auth (ns metabase.middleware.auth
"Middleware for dealing with authentication and session management." "Middleware for dealing with authentication and session management."
(:require [korma.core :as korma] (:require [korma.core :as k]
[metabase.config :as config] [metabase.config :as config]
[metabase.db :refer [sel]] [metabase.db :refer [sel]]
[metabase.api.common :refer [*current-user* *current-user-id*]] [metabase.api.common :refer [*current-user* *current-user-id*]]
...@@ -8,16 +8,16 @@ ...@@ -8,16 +8,16 @@
[user :refer [User current-user-fields]]))) [user :refer [User current-user-fields]])))
(def metabase-session-cookie "metabase.SESSION_ID") (def ^:const metabase-session-cookie "metabase.SESSION_ID")
(def metabase-session-header "x-metabase-session") (def ^:const metabase-session-header "x-metabase-session")
(def metabase-apikey-header "x-metabase-apikey") (def ^:const metabase-api-key-header "x-metabase-apikey")
(def response-unauthentic {:status 401 :body "Unauthenticated"}) (def ^:const response-unauthentic {:status 401 :body "Unauthenticated"})
(def response-forbidden {:status 403 :body "Forbidden"}) (def ^:const response-forbidden {:status 403 :body "Forbidden"})
(defn wrap-sessionid (defn wrap-session-id
"Middleware that sets the :metabase-sessionid keyword on the request if a session id can be found. "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 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." http headers for `X-METABASE-SESSION`. If neither is found then then no keyword is bound to the request."
...@@ -25,32 +25,34 @@ ...@@ -25,32 +25,34 @@
(fn [{:keys [cookies headers] :as request}] (fn [{:keys [cookies headers] :as request}]
(if-let [session-id (or (get-in cookies [metabase-session-cookie :value]) (headers metabase-session-header))] (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 ;; alternatively we could always associate the keyword and just let it be nil if there is no value
(handler (assoc request :metabase-sessionid session-id)) (handler (assoc request :metabase-session-id session-id))
(handler request)))) (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 enforces authentication of the client, cancelling the request processing if auth fails.
Authentication is determined by validating the :metabase-sessionid on the request against the db session list.
If the session is valid then we associate a :metabase-userid on the request and carry on, but if the validation
fails then we return an HTTP 401 response indicating that the client is not authentic.
NOTE: we are purposely not associating the full current user object here so that we can be modular." (defn enforce-authentication
"Middleware that returns a 401 response if REQUEST has no associated `:metabase-user-id`."
[handler] [handler]
(fn [{:keys [metabase-sessionid] :as request}] (fn [{:keys [metabase-user-id] :as request}]
;; TODO - what kind of validations can we do on the sessionid to make sure it's safe to handle? str? alphanumeric? (if metabase-user-id
(let [session (first (korma/select Session (handler request)
;; NOTE: we join with the User table and ensure user.is_active = true response-unauthentic)))
(korma/with User (korma/where {:is_active true}))
(korma/fields :created_at :user_id)
(korma/where {:id metabase-sessionid})))
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
(if (and session (> (config/config-int :max-session-age) (quot session-age-ms 60000)))
(handler (assoc request :metabase-userid (:user_id session)))
;; default response is 401
response-unauthentic))))
(defmacro sel-current-user [current-user-id] (defmacro sel-current-user [current-user-id]
...@@ -65,35 +67,35 @@ ...@@ -65,35 +67,35 @@
*current-user* delay that returns current user (or nil) from DB" *current-user* delay that returns current user (or nil) from DB"
[handler] [handler]
(fn [request] (fn [request]
(let [current-user-id (:metabase-userid request)] (if-let [current-user-id (:metabase-user-id request)]
(binding [*current-user-id* current-user-id (binding [*current-user-id* current-user-id
*current-user* (if-not current-user-id (atom nil) *current-user* (delay (sel-current-user current-user-id))]
(delay (sel-current-user current-user-id)))] (handler request))
(handler request))))) (handler request))))
(defn wrap-apikey (defn wrap-api-key
"Middleware that sets the :metabase-apikey keyword on the request if a valid API Key can be found. "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." We check the request headers for `X-METABASE-API-KEY` and if it's not found then then no keyword is bound to the request."
[handler] [handler]
(fn [{:keys [headers] :as request}] (fn [{:keys [headers] :as request}]
(if-let [api-key (headers metabase-apikey-header)] (if-let [api-key (headers metabase-api-key-header)]
(handler (assoc request :metabase-apikey api-key)) (handler (assoc request :metabase-api-key api-key))
(handler request)))) (handler request))))
(defn enforce-apikey (defn enforce-api-key
"Middleware that enforces validation of the client via API Key, cancelling the request processing if the check fails. "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-apikey on the request. If the api key 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. 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-apikey matches the configured :mb-api-key value then the request continues, otherwise we 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." reject the request and return a 403 Forbidden response."
[handler] [handler]
(fn [{:keys [metabase-apikey] :as request}] (fn [{:keys [metabase-api-key] :as request}]
(if (= (config/config-str :mb-api-key) metabase-apikey) (if (= (config/config-str :mb-api-key) metabase-api-key)
(handler request) (handler request)
;; default response is 403 ;; default response is 403
response-forbidden))) response-forbidden)))
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
(cheshire factory (cheshire factory
[generate :refer [add-encoder encode-str]]) [generate :refer [add-encoder encode-str]])
[medley.core :refer [filter-vals map-vals]] [medley.core :refer [filter-vals map-vals]]
[metabase.models.interface :refer [api-serialize]]
[metabase.util :as util])) [metabase.util :as util]))
(declare -format-response) (declare -format-response)
...@@ -45,11 +46,14 @@ ...@@ -45,11 +46,14 @@
[m] [m]
(filter-vals #(not (or (delay? %) (filter-vals #(not (or (delay? %)
(fn? %))) (fn? %)))
m)) ;; 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] (defn- -format-response [obj]
(cond (cond
(map? obj) (->> (remove-fns-and-delays obj) ; recurse over all vals in the map (map? obj) (->> (api-serialize obj)
(map-vals -format-response)) remove-fns-and-delays
(coll? obj) (map -format-response obj) ; recurse over all items in the collection (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)) :else obj))
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
[metabase.api.common :refer [*current-user* *current-user-id* check]] [metabase.api.common :refer [*current-user* *current-user-id* check]]
[metabase.util :as u])) [metabase.util :as u]))
(def timezones (def ^:const timezones
["GMT" ["GMT"
"UTC" "UTC"
"US/Alaska" "US/Alaska"
...@@ -17,11 +17,11 @@ ...@@ -17,11 +17,11 @@
;;; ALLEN'S PERMISSIONS IMPLEMENTATION ;;; ALLEN'S PERMISSIONS IMPLEMENTATION
(def perms-none 0) (def ^:const perms-none 0)
(def perms-read 1) (def ^:const perms-read 1)
(def perms-readwrite 2) (def ^:const perms-readwrite 2)
(def permissions (def ^:const permissions
[{:id perms-none :name "None"}, [{:id perms-none :name "None"},
{:id perms-read :name "Read Only"}, {:id perms-read :name "Read Only"},
{:id perms-readwrite :name "Read & Write"}]) {:id perms-readwrite :name "Read & Write"}])
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
(:require [korma.core :refer :all] (:require [korma.core :refer :all]
[metabase.api.common :refer [*current-user*]] [metabase.api.common :refer [*current-user*]]
[metabase.db :refer :all] [metabase.db :refer :all]
[metabase.models.common :refer [assoc-permissions-sets]])) (metabase.models [common :refer [assoc-permissions-sets]]
[interface :refer :all])))
(defentity Database (defentity Database
...@@ -13,33 +14,23 @@ ...@@ -13,33 +14,23 @@
(assoc :hydration-keys #{:database (assoc :hydration-keys #{:database
:db})) :db}))
(defmethod post-select Database [_ db] (defrecord DatabaseInstance []
(assoc db ;; preserve normal IFn behavior so things like ((sel :one Database) :id) work correctly
:can_read (delay true) clojure.lang.IFn
:can_write (delay (:is_superuser @*current-user*)))) (invoke [this k]
(get this k))
{:created_at #inst "2015-06-30T19:51:45.294000000-00:00",
:engine :h2,
:id 3,
:details
{:db
"file:/Users/camsaul/metabase/target/Test_Database;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1"},
:updated_at #inst "2015-06-30T19:51:45.294000000-00:00",
:name "Test Database",
:organization_id nil,
:description nil}
{:description nil, IModelInstanceApiSerialze
:organization_id nil, (api-serialize [this]
:name "Test Database", ;; If current user isn't an admin strip out DB details which may include things like password
:updated_at #inst "2015-06-30T19:51:45.294000000-00:00", (cond-> this
:details (not (:is_superuser @*current-user*)) (dissoc :details))))
{:db
"file:/Users/camsaul/metabase/target/Test_Database;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1"},
:id 3,
:engine "h2",
:created_at #inst "2015-06-30T19:51:45.294000000-00:00"}
(defmethod post-select Database [_ db]
(map->DatabaseInstance
(assoc db
:can_read (delay true)
:can_write (delay (:is_superuser @*current-user*)))))
(defmethod pre-cascade-delete Database [_ {:keys [id] :as database}] (defmethod pre-cascade-delete Database [_ {:keys [id] :as database}]
(cascade-delete 'metabase.models.table/Table :db_id id)) (cascade-delete 'metabase.models.table/Table :db_id id))
(ns metabase.models.interface)
(defprotocol IModelInstanceApiSerialze
(api-serialize [this]
"Called on all objects being written out by the API. Default implementations return THIS as-is, but models can provide
custom methods to strip sensitive data, from non-admins, etc."))
(extend-protocol IModelInstanceApiSerialze
Object
(api-serialize [this]
this)
nil
(api-serialize [_]
nil))
...@@ -40,18 +40,31 @@ ...@@ -40,18 +40,31 @@
;; # DB LIFECYCLE ENDPOINTS ;; # DB LIFECYCLE ENDPOINTS
;; ## GET /api/meta/db/:id ;; ## GET /api/meta/db/:id
;; regular users *should not* see DB details
(expect (expect
(match-$ (db) (match-$ (db)
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $ :updated_at $
:updated_at $ :name "Test Database"
:name "Test Database"
:organization_id nil :organization_id nil
:description nil}) :description nil})
((user->client :rasta) :get 200 (format "meta/db/%d" (db-id)))) ((user->client :rasta) :get 200 (format "meta/db/%d" (db-id))))
;; superusers *should* see DB details
(expect
(match-$ (db)
{:created_at $
:engine "h2"
:id $
:details $
:updated_at $
:name "Test Database"
:organization_id nil
:description nil})
((user->client :crowberto) :get 200 (format "meta/db/%d" (db-id))))
;; ## POST /api/meta/db ;; ## POST /api/meta/db
;; Check that we can create a Database ;; Check that we can create a Database
(let [db-name (random-name)] (let [db-name (random-name)]
...@@ -106,6 +119,7 @@ ...@@ -106,6 +119,7 @@
;; ## GET /api/meta/db ;; ## GET /api/meta/db
;; Test that we can get all the DBs for an Org, ordered by name ;; Test that we can get all the DBs for an Org, ordered by name
;; Database details *should not* come back for Rasta since she's not a superuser
(let [db-name (str "A" (random-name))] ; make sure this name comes before "Test Database" (let [db-name (str "A" (random-name))] ; make sure this name comes before "Test Database"
(expect-eval-actual-first (expect-eval-actual-first
(set (filter identity (set (filter identity
...@@ -115,7 +129,6 @@ ...@@ -115,7 +129,6 @@
{:created_at $ {:created_at $
:engine (name $engine) :engine (name $engine)
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
...@@ -125,7 +138,6 @@ ...@@ -125,7 +138,6 @@
{:created_at $ {:created_at $
:engine "postgres" :engine "postgres"
:id $ :id $
:details {:host "localhost", :port 5432, :dbname "fakedb", :user "cam"}
:updated_at $ :updated_at $
:name $ :name $
:organization_id nil :organization_id nil
......
...@@ -22,7 +22,6 @@ ...@@ -22,7 +22,6 @@
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
......
...@@ -64,7 +64,6 @@ ...@@ -64,7 +64,6 @@
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
...@@ -120,7 +119,6 @@ ...@@ -120,7 +119,6 @@
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
...@@ -193,7 +191,6 @@ ...@@ -193,7 +191,6 @@
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
...@@ -293,7 +290,6 @@ ...@@ -293,7 +290,6 @@
{:created_at $ {:created_at $
:engine "h2" :engine "h2"
:id $ :id $
:details $
:updated_at $ :updated_at $
:name "Test Database" :name "Test Database"
:organization_id nil :organization_id nil
...@@ -444,8 +440,7 @@ ...@@ -444,8 +440,7 @@
:updated_at $, :updated_at $,
:id $, :id $,
:engine "h2", :engine "h2",
:created_at $ :created_at $})})})
:details $})})})
:destination (match-$ users-id-field :destination (match-$ users-id-field
{:id $ {:id $
:table_id $ :table_id $
......
(ns metabase.middleware.auth-test (ns metabase.middleware.auth-test
(:require [expectations :refer :all] (:require [expectations :refer :all]
[korma.core :as korma] [korma.core :as k]
[ring.mock.request :as mock] [ring.mock.request :as mock]
[metabase.api.common :refer [*current-user-id* *current-user*]] [metabase.api.common :refer [*current-user-id* *current-user*]]
[metabase.middleware.auth :refer :all] [metabase.middleware.auth :refer :all]
[metabase.models.session :refer [Session]] [metabase.models.session :refer [Session]]
[metabase.test.data :refer :all] [metabase.test.data :refer :all]
[metabase.test.data.users :refer :all] [metabase.test.data.users :refer :all]
[metabase.util :as util])) [metabase.util :as u]))
;; =========================== TEST wrap-sessionid middleware =========================== ;; =========================== TEST wrap-session-id middleware ===========================
;; create a simple example of our middleware wrapped around a handler that simply returns the request ;; create a simple example of our middleware wrapped around a handler that simply returns the request
;; this works in this case because the only impact our middleware has is on the request ;; this works in this case because the only impact our middleware has is on the request
(def wrapped-handler (def ^:private wrapped-handler
(wrap-sessionid (fn [req] req))) (wrap-session-id identity))
;; no sessionid in the request ;; no session-id in the request
(expect (expect nil
{}
(-> (wrapped-handler (mock/request :get "/anyurl") ) (-> (wrapped-handler (mock/request :get "/anyurl") )
(select-keys [:metabase-sessionid]))) :metabase-session-id))
;; extract sessionid from header ;; extract session-id from header
(expect (expect "foobar"
{:metabase-sessionid "foobar"}
(-> (wrapped-handler (mock/header (mock/request :get "/anyurl") metabase-session-header "foobar")) (-> (wrapped-handler (mock/header (mock/request :get "/anyurl") metabase-session-header "foobar"))
(select-keys [:metabase-sessionid]))) :metabase-session-id))
;; extract sessionid from cookie ;; extract session-id from cookie
(expect (expect "cookie-session"
{:metabase-sessionid "cookie-session"}
(-> (wrapped-handler (assoc (mock/request :get "/anyurl") :cookies {metabase-session-cookie {:value "cookie-session"}})) (-> (wrapped-handler (assoc (mock/request :get "/anyurl") :cookies {metabase-session-cookie {:value "cookie-session"}}))
(select-keys [:metabase-sessionid]))) :metabase-session-id))
;; if both header and cookie sessionids exist, then we expect the cookie to take precedence ;; if both header and cookie session-ids exist, then we expect the cookie to take precedence
(expect (expect "cookie-session"
{:metabase-sessionid "cookie-session"}
(-> (wrapped-handler (-> (mock/header (mock/request :get "/anyurl") metabase-session-header "foobar") (-> (wrapped-handler (-> (mock/header (mock/request :get "/anyurl") metabase-session-header "foobar")
(assoc :cookies {metabase-session-cookie {:value "cookie-session"}}))) (assoc :cookies {metabase-session-cookie {:value "cookie-session"}})))
(select-keys [:metabase-sessionid]))) :metabase-session-id))
;; =========================== TEST enforce-authentication middleware =========================== ;; =========================== TEST enforce-authentication middleware ===========================
;; create a simple example of our middleware wrapped around a handler that simply returns the request ;; create a simple example of our middleware wrapped around a handler that simply returns the request
(def auth-enforced-handler (def ^:private auth-enforced-handler
(enforce-authentication (fn [req] req))) (wrap-current-user-id (enforce-authentication identity)))
(defn request-with-sessionid (defn- request-with-session-id
"Creates a mock Ring request with the given sessionid applied" "Creates a mock Ring request with the given session-id applied"
[sessionid] [session-id]
(-> (mock/request :get "/anyurl") (-> (mock/request :get "/anyurl")
(assoc :metabase-sessionid sessionid))) (assoc :metabase-session-id session-id)))
;; no sessionid in the request ;; no session-id in the request
(expect (expect response-unauthentic
{:status (:status response-unauthentic)
:body (:body response-unauthentic)}
(auth-enforced-handler (mock/request :get "/anyurl"))) (auth-enforced-handler (mock/request :get "/anyurl")))
(defn- random-session-id []
{:post [(string? %)]}
(.toString (java.util.UUID/randomUUID)))
;; valid sessionid ;; valid session ID
(let [sessionid (.toString (java.util.UUID/randomUUID))] (expect (user->id :rasta)
(assert sessionid) (let [session-id (random-session-id)]
;; validate that we are authenticated (k/insert Session (k/values {:id session-id, :user_id (user->id :rasta), :created_at (u/new-sql-timestamp)}))
(expect-let [res (korma/insert Session (korma/values {:id sessionid :user_id (user->id :rasta) :created_at (util/new-sql-timestamp)}))] (-> (auth-enforced-handler (request-with-session-id session-id))
{:metabase-userid (user->id :rasta)} :metabase-user-id)))
(-> (auth-enforced-handler (request-with-sessionid sessionid))
(select-keys [:metabase-userid]))))
;; expired sessionid ;; expired session-id
(let [sessionid (.toString (java.util.UUID/randomUUID))] ;; create a new session (specifically created some time in the past so it's EXPIRED)
(assert sessionid) ;; should fail due to session expiration
;; create a new session (specifically created some time in the past so it's EXPIRED) (expect response-unauthentic
;; should fail due to session expiration (let [session-id (random-session-id)]
(expect-let [res (korma/insert Session (korma/values {:id sessionid :user_id (user->id :rasta) :created_at (java.sql.Timestamp. 0)}))] (k/insert Session (k/values {:id session-id, :user_id (user->id :rasta), :created_at (java.sql.Timestamp. 0)}))
{:status (:status response-unauthentic) (auth-enforced-handler (request-with-session-id session-id))))
:body (:body response-unauthentic)}
(auth-enforced-handler (request-with-sessionid sessionid))))
;; inactive user sessionid ;; inactive user session-id
(let [sessionid (.toString (java.util.UUID/randomUUID))] ;; create a new session (specifically created some time in the past so it's EXPIRED)
(assert sessionid) ;; should fail due to inactive user
;; create a new session (specifically created some time in the past so it's EXPIRED) ;; NOTE that :trashbird is our INACTIVE test user
;; should fail due to inactive user (expect response-unauthentic
;; NOTE that :trashbird is our INACTIVE test user (let [session-id (random-session-id)]
(expect-let [res (korma/insert Session (korma/values {:id sessionid :user_id (user->id :trashbird) :created_at (util/new-sql-timestamp)}))] (k/insert Session (k/values {:id session-id, :user_id (user->id :trashbird), :created_at (u/new-sql-timestamp)}))
{:status (:status response-unauthentic) (auth-enforced-handler (request-with-session-id session-id))))
:body (:body response-unauthentic)}
(auth-enforced-handler (request-with-sessionid sessionid))))
;; =========================== TEST bind-current-user middleware =========================== ;; =========================== TEST bind-current-user middleware ===========================
;; create a simple example of our middleware wrapped around a handler that simply returns our bound variables for users ;; create a simple example of our middleware wrapped around a handler that simply returns our bound variables for users
(def user-bound-handler (def ^:private user-bound-handler
(bind-current-user (fn [req] {:userid *current-user-id* (bind-current-user (fn [_] {:user-id *current-user-id*
:user (select-keys @*current-user* [:id :email])}))) :user (select-keys @*current-user* [:id :email])})))
(defn- request-with-user-id
(defn request-with-userid "Creates a mock Ring request with the given user-id applied"
"Creates a mock Ring request with the given userid applied" [user-id]
[userid]
(-> (mock/request :get "/anyurl") (-> (mock/request :get "/anyurl")
(assoc :metabase-userid userid))) (assoc :metabase-user-id user-id)))
;; with valid user-id ;; with valid user-id
(expect (expect
{:userid (user->id :rasta) {:user-id (user->id :rasta)
:user {:id (user->id :rasta) :user {:id (user->id :rasta)
:email (:email (fetch-user :rasta))}} :email (:email (fetch-user :rasta))}}
(user-bound-handler (request-with-userid (user->id :rasta)))) (user-bound-handler (request-with-user-id (user->id :rasta))))
;; with invalid user-id (not sure how this could ever happen, but lets test it anyways) ;; with invalid user-id (not sure how this could ever happen, but lets test it anyways)
(expect (expect
{:userid 0 {:user-id 0
:user {}} :user {}}
(user-bound-handler (request-with-userid 0))) (user-bound-handler (request-with-user-id 0)))
;; =========================== TEST wrap-apikey middleware =========================== ;; =========================== TEST wrap-api-key middleware ===========================
;; create a simple example of our middleware wrapped around a handler that simply returns the request ;; create a simple example of our middleware wrapped around a handler that simply returns the request
;; this works in this case because the only impact our middleware has is on the request ;; this works in this case because the only impact our middleware has is on the request
(def wrapped-apikey-handler (def ^:private wrapped-api-key-handler
(wrap-apikey (fn [req] req))) (wrap-api-key identity))
;; no apikey in the request ;; no apikey in the request
(expect (expect nil
{} (-> (wrapped-api-key-handler (mock/request :get "/anyurl") )
(-> (wrapped-apikey-handler (mock/request :get "/anyurl") ) :metabase-session-id))
(select-keys [:metabase-sessionid])))
;; extract apikey from header ;; extract apikey from header
(expect (expect "foobar"
{:metabase-apikey "foobar"} (-> (wrapped-api-key-handler (mock/header (mock/request :get "/anyurl") metabase-api-key-header "foobar"))
(-> (wrapped-apikey-handler (mock/header (mock/request :get "/anyurl") metabase-apikey-header "foobar")) :metabase-api-key))
(select-keys [:metabase-apikey])))
;; =========================== TEST enforce-apikey middleware =========================== ;; =========================== TEST enforce-api-key middleware ===========================
;; create a simple example of our middleware wrapped around a handler that simply returns the request ;; create a simple example of our middleware wrapped around a handler that simply returns the request
(def apikey-enforced-handler (def ^:private api-key-enforced-handler
(enforce-apikey (fn [req] {:success true}))) (enforce-api-key (constantly {:success true})))
(defn request-with-apikey (defn- request-with-api-key
"Creates a mock Ring request with the given apikey applied" "Creates a mock Ring request with the given apikey applied"
[apikey] [api-key]
(-> (mock/request :get "/anyurl") (-> (mock/request :get "/anyurl")
(assoc :metabase-apikey apikey))) (assoc :metabase-api-key api-key)))
;; no apikey in the request, expect 403 ;; no apikey in the request, expect 403
(expect (expect response-forbidden
{:status (:status response-forbidden) (api-key-enforced-handler (mock/request :get "/anyurl")))
:body (:body response-forbidden)}
(apikey-enforced-handler (mock/request :get "/anyurl")))
;; valid apikey, expect 200 ;; valid apikey, expect 200
(expect (expect
{:success true} {:success true}
(apikey-enforced-handler (request-with-apikey "test-api-key"))) (api-key-enforced-handler (request-with-api-key "test-api-key")))
;; invalid apikey, expect 403 ;; invalid apikey, expect 403
(expect (expect response-forbidden
{:status (:status response-forbidden) (api-key-enforced-handler (request-with-api-key "foobar")))
:body (:body response-forbidden)}
(apikey-enforced-handler (request-with-apikey "foobar")))
...@@ -67,16 +67,16 @@ ...@@ -67,16 +67,16 @@
:restarted] :restarted]
[(do [(do
(stop-task-runner!) (stop-task-runner!)
(with-redefs [metabase.task/hourly-task-delay (constantly 100) (with-redefs [metabase.task/hourly-task-delay (constantly 200)
metabase.task/hourly-tasks-hook mock-hourly-tasks-hook] metabase.task/hourly-tasks-hook mock-hourly-tasks-hook]
(add-hook! #'hourly-tasks-hook inc-task-test-atom-counter-by-system-hour) (add-hook! #'hourly-tasks-hook inc-task-test-atom-counter-by-system-hour)
(reset! task-test-atom-counter 0) (reset! task-test-atom-counter 0)
(start-task-runner!) (start-task-runner!)
[@task-test-atom-counter ; should be 0, since not enough time has elaspsed for the hook to be executed [@task-test-atom-counter ; should be 0, since not enough time has elaspsed for the hook to be executed
(do (Thread/sleep 150) (do (Thread/sleep 300)
@task-test-atom-counter) ; should have been called once (~50ms ago) @task-test-atom-counter) ; should have been called once (~100ms ago)
(do (Thread/sleep 200) (do (Thread/sleep 400)
@task-test-atom-counter) ; should have been called two more times @task-test-atom-counter) ; should have been called two more times
(do (stop-task-runner!) (do (stop-task-runner!)
:stopped)])) :stopped)]))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment