Skip to content
Snippets Groups Projects
Commit 3600995f authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'master' of github.com:metabase/metabase-init into new_filters

parents 8d0372e4 c65a7eaa
No related merge requests found
Showing
with 442 additions and 286 deletions
......@@ -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)
......
......@@ -25,3 +25,4 @@ profiles.clj
/coverage
/resources/sample-dataset.db.trace.db
/deploy/artifacts/*
/resources/version.properties
#! /bin/bash
echo "Running 'npm install' to download javascript dependencies..." &&
npm install &&
# Generate the resources/version.properties file
version() {
# Skip on CircleCI since this is interactive
if [ ! $CI ]; then
VERSION=$(./version)
SHORT_VERSION=$(./version --short)
echo "Running 'webpack -p' to assemble and minify frontend assets..." &&
./node_modules/webpack/bin/webpack.js -p &&
echo "Tagging uberjar with version '$VERSION'..."
if [ -f resources/sample-dataset.db.mv.db ]; then
echo "Sample Dataset already generated."
else
echo "Running 'lein generate-sample-dataset' to generate the sample dataset..."
lein generate-sample-dataset
fi &&
# Ok, now generate the appropriate version.properties file.
echo "long=$VERSION" > resources/version.properties
echo "short=$SHORT_VERSION" >> resources/version.properties
fi
}
frontend() {
echo "Running 'npm install' to download javascript dependencies..." &&
npm install &&
echo "Running 'webpack -p' to assemble and minify frontend assets..." &&
./node_modules/webpack/bin/webpack.js -p
}
sample-dataset() {
if [ -f resources/sample-dataset.db.mv.db ]; then
echo "Sample Dataset already generated."
else
echo "Running 'lein generate-sample-dataset' to generate the sample dataset..."
lein generate-sample-dataset
fi
}
echo "Running 'lein uberjar'..." &&
lein uberjar
uberjar() {
echo "Running 'lein uberjar'..."
lein uberjar
}
all() {
version && frontend && sample-dataset && uberjar
}
# Default to running all but let someone specify one or more sub-tasks to run instead if desired
# e.g.
# ./build-uberjar # do everything
# ./build-uberjar version # just update version.properties
# ./build-uberjar version uberjar # just update version.properties and build uberjar
if [ "$1" ]; then
for cmd in "$@"; do
$cmd
done
else
all
fi
......@@ -70,7 +70,7 @@
[expectations "2.1.2"] ; unit tests
[marginalia "0.8.0"] ; for documentation
[ring/ring-mock "0.2.0"]]
:plugins [[cider/cider-nrepl "0.9.1"] ; Interactive development w/ cider NREPL in Emacs
:plugins [[cider/cider-nrepl "0.10.0-SNAPSHOT"] ; Interactive development w/ cider NREPL in Emacs
[jonase/eastwood "0.2.1"] ; Linting
[lein-ancient "0.6.7"] ; Check project for outdated dependencies + plugins w/ 'lein ancient'
[lein-bikeshed "0.2.0"] ; Linting
......@@ -78,8 +78,9 @@
[lein-expectations "0.0.8"] ; run unit tests with 'lein expectations'
[lein-instant-cheatsheet "2.1.4"] ; use awesome instant cheatsheet created by yours truly w/ 'lein instant-cheatsheet'
[lein-marginalia "0.8.0"] ; generate documentation with 'lein marg'
[refactor-nrepl "1.1.0"]] ; support for advanced refactoring in Emacs/LightTable
[refactor-nrepl "2.0.0-SNAPSHOT"]] ; support for advanced refactoring in Emacs/LightTable
:global-vars {*warn-on-reflection* true} ; Emit warnings on all reflection calls
:env {:mb-run-mode "dev"}
:jvm-opts ["-Dlogfile.path=target/log"
"-Xms1024m" ; give JVM a decent heap size to start with
"-Xmx2048m" ; hard limit of 2GB so we stop hitting the 4GB container limit on CircleCI
......
'use strict';
import MetabaseSettings from "metabase/lib/settings";
var AuthControllers = angular.module('metabase.auth.controllers', [
'metabase.auth.services',
'ipCookie',
......@@ -99,6 +101,7 @@ AuthControllers.controller('PasswordReset', ['$scope', '$routeParams', '$locatio
}
$scope.resetSuccess = false;
$scope.passwordComplexity = MetabaseSettings.passwordComplexity(false);
$scope.newUserJoining = ($location.hash() === 'new');
$scope.resetPassword = function(password) {
......
......@@ -8,7 +8,7 @@
<form class="ForgotForm Login-wrapper bg-white Form-new bordered rounded shadowed" name="form" novalidate>
<h3 class="Login-header Form-offset">New password</h3>
<p class="Form-offset text-grey-3 mb4">To keep your data secure, passwords need to be at least <b>8 characters</b> and include <b>at least one uppercase letter</b>, <b>one digit</b>, and <b>one special character.</b></p>
<p class="Form-offset text-grey-3 mb4">To keep your data secure, passwords {{passwordComplexity}}</p>
<mb-form-message></mb-form-message>
......
......@@ -26,7 +26,7 @@ const MetabaseSettings = {
return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
},
passwordComplexity: function() {
passwordComplexity: function(capitalize) {
const complexity = this.get('password_complexity');
const clauseDescription = function(clause) {
......@@ -38,7 +38,7 @@ const MetabaseSettings = {
}
};
let description = "Must be "+complexity.total+" characters long",
let description = (capitalize === false) ? "must be "+complexity.total+" characters long" : "Must be "+complexity.total+" characters long",
clauses = [];
["lower", "upper", "digit", "special"].forEach(function(clause) {
......
......@@ -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))
......
(ns metabase.config
(:require [environ.core :as environ]
(:require (clojure.java [io :as io]
[shell :as shell])
[clojure.string :as s]
[environ.core :as environ]
[medley.core :as m])
(:import (clojure.lang Keyword)))
(:import clojure.lang.Keyword))
(def ^:const app-defaults
(def ^:private ^:const app-defaults
"Global application defaults"
{;; Database Configuration (general options? dburl?)
:mb-run-mode "prod"
......@@ -39,15 +42,16 @@
;; These are convenience functions for accessing config values that ensures a specific return type
(defn ^Integer config-int [k] (when-let [val (config-str k)] (Integer/parseInt val)))
(defn ^Boolean config-bool [k] (when-let [val (config-str k)] (Boolean/parseBoolean val)))
(defn ^Keyword config-kw [k] (when-let [val (config-str k)] (keyword val)))
(defn ^Integer config-int [k] (some-> k config-str Integer/parseInt))
(defn ^Boolean config-bool [k] (some-> k config-str Boolean/parseBoolean))
(defn ^Keyword config-kw [k] (some-> k config-str keyword))
(def ^:const config-all
"Global application configuration as a dictionary.
Combines hard coded defaults with optional user specified overrides from environment variables."
(into {} (map (fn [k] [k (config-str k)]) (keys app-defaults))))
(into {} (for [k (keys app-defaults)]
[k (config-str k)])))
(defn config-match
......@@ -64,5 +68,31 @@
(m/filter-keys (fn [k] (re-matches prefix-regex (str k))) environ/env))
(m/map-keys (fn [k] (let [kstr (str k)] (keyword (subs kstr (+ 1 (count prefix))))))))))
(defn ^Boolean is-dev? [] (= :dev (config-kw :mb-run-mode)))
(defn ^Boolean is-prod? [] (= :prod (config-kw :mb-run-mode)))
(defn ^Boolean is-test? [] (= :test (config-kw :mb-run-mode)))
;;; Version stuff
;; Metabase version is of the format `GIT-TAG (GIT-SHORT-HASH GIT-BRANCH)`
(defn- version-info-from-shell-script []
{:long (-> (shell/sh "./version") :out s/trim)
:short (-> (shell/sh "./version" "--short") :out s/trim)})
(defn- version-info-from-properties-file []
(with-open [reader (io/reader "resources/version.properties")]
(let [props (java.util.Properties.)]
(.load props reader)
(into {} (for [[k v] props]
[(keyword k) v])))))
(defn mb-version-info
"Return information about the current version of Metabase.
This comes from `resources/version.properties` for prod builds and is fetched from `git` via the `./version` script for dev.
(mb-version) -> {:long \"v0.11.1 (6509c49 master)\", :short \"v0.11.1\"}"
[]
(if (is-prod?)
(version-info-from-properties-file)
(version-info-from-shell-script)))
;; -*- 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."
......@@ -95,7 +92,7 @@
(defn init
"General application initialization function which should be run once at application startup."
[]
(log/info "Metabase Initializing ... ")
(log/info (format "Starting Metabase version %s..." ((config/mb-version-info) :long)))
;; First of all, lets register a shutdown hook that will tidy things up for us on app exit
(.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable destroy))
(log/debug "Using Config:\n" (with-out-str (clojure.pprint/pprint config/config-all)))
......@@ -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)))))
......
(ns metabase.middleware.auth
"Middleware for dealing with authentication and session management."
(:require [korma.core :as k]
(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.api.common :refer [*current-user* *current-user-id*]]
(metabase.models [session :refer [Session]]
[user :refer [User current-user-fields]])))
(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"})
(def ^:const response-forbidden {:status 403 :body "Forbidden"})
(defn wrap-session-id
......@@ -28,6 +46,7 @@
(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]
......@@ -55,28 +74,22 @@
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"
* `*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))]
*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.
"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}]
......@@ -88,10 +101,10 @@
(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.
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
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}]
......@@ -99,3 +112,128 @@
(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))))))
(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))
(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)))))))))
......@@ -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
......
(ns metabase.routes
(:require [cheshire.core :as cheshire]
[compojure.core :refer [context defroutes GET]]
[compojure.route :as route]
(:require [clojure.java.io :as io]
[cheshire.core :as json]
(compojure [core :refer [context defroutes GET]]
[route :as route])
[ring.util.response :as resp]
[stencil.core :as stencil]
[metabase.api.routes :as api]
[metabase.setup :as setup]
[metabase.util :as u]))
(metabase.models common
[setting :as setting])
(metabase [config :as config]
[setup :as setup]
[util :as u])
metabase.util.password))
(defn- load-index-template
"Slurp in the Metabase index.html file as a `String`"
[]
(slurp (clojure.java.io/resource "frontend_client/index.html")))
(def load-index
"Memoized version of `load-index-template`"
;(memoize load-index-template)
load-index-template)
(def date-format-rfc2616
(def ^:private ^:const date-format-rfc2616
"Java SimpleDateFormat representing rfc2616 style date used in http headers."
"EEE, dd MMM yyyy HH:mm:ss zzz")
(defn index-page-vars
(defn- index-page-vars
"Static values that we inject into the index.html page via Mustache."
[]
{:ga_code "UA-60817802-1"
......@@ -30,23 +26,26 @@
:password_complexity (metabase.util.password/active-password-complexity)
:setup_token (setup/token-value)
:timezones metabase.models.common/timezones
:version (config/mb-version-info)
;; all of these values are dynamic settings from the admin UI but we include them here for bootstrapping availability
:anon-tracking-enabled (metabase.models.setting/get :anon-tracking-enabled)
:-site-name (metabase.models.setting/get :-site-name)})
:anon-tracking-enabled (setting/get :anon-tracking-enabled)
:-site-name (setting/get :-site-name)})
(defn- index [request]
(-> (io/resource "frontend_client/index.html")
slurp
(stencil/render-string {:bootstrap_json (json/generate-string (index-page-vars))})
resp/response
(resp/content-type "text/html")
(resp/header "Last-Modified" (u/now-with-format date-format-rfc2616))))
;; Redirect naughty users who try to visit a page other than setup if setup is not yet complete
(let [index (fn [request]
(-> (resp/response (stencil/render-string
(load-index)
{:bootstrap_json (cheshire/generate-string (index-page-vars))}))
(resp/content-type "text/html")
(resp/header "Last-Modified" (u/now-with-format date-format-rfc2616))))]
(defroutes routes
(GET "/" [] index) ; ^/$ -> index.html
(GET "/favicon.ico" [] (resp/resource-response "frontend_client/favicon.ico"))
(context "/api" [] api/routes) ; ^/api/ -> API routes
(context "/app" []
(route/resources "/" {:root "frontend_client/app"}) ; ^/app/ -> static files under frontend_client/app
(route/not-found {:status 404 ; return 404 for anything else starting with ^/app/ that doesn't exist
:body "Not found."}))
(GET "*" [] index))) ; Anything else (e.g. /user/edit_current) should serve up index.html; Angular app will handle the rest
(defroutes routes
(GET "/" [] index) ; ^/$ -> index.html
(GET "/favicon.ico" [] (resp/resource-response "frontend_client/favicon.ico"))
(context "/api" [] api/routes) ; ^/api/ -> API routes
(context "/app" []
(route/resources "/" {:root "frontend_client/app"}) ; ^/app/ -> static files under frontend_client/app
(route/not-found {:status 404 ; return 404 for anything else starting with ^/app/ that doesn't exist
:body "Not found."}))
(GET "*" [] index)) ; Anything else (e.g. /user/edit_current) should serve up index.html; Angular app will handle the rest
(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
......
......@@ -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
......
......@@ -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
......
(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)} ]}]))
(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)} ]}]))
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