From a5b1e1079abbdc6fd478b68d827677d943870cea Mon Sep 17 00:00:00 2001 From: Allen Gilliland <agilliland@gmail.com> Date: Tue, 17 Mar 2015 23:21:55 -0700 Subject: [PATCH] add new code for handling initial user creation at install time. * new api endpoint for /api/setup/user which takes an install token and creates a new user. * added a few setup token management functions in metabase.setup namespace. * updated metabase.com (init) function to detect if a setup token is needed and provide a url on cmd line. * unit tests for new api endpoint. --- src/metabase/api/routes.clj | 4 +- src/metabase/api/setup.clj | 38 +++++++++++++++++ src/metabase/core.clj | 21 +++++++--- src/metabase/setup.clj | 25 +++++++++++ test/metabase/api/setup_test.clj | 72 ++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/metabase/api/setup.clj create mode 100644 src/metabase/setup.clj create mode 100644 test/metabase/api/setup_test.clj diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj index 0708eac9b8a..d8c93c757b6 100644 --- a/src/metabase/api/routes.clj +++ b/src/metabase/api/routes.clj @@ -12,6 +12,7 @@ [search :as search] [session :as session] [setting :as setting] + [setup :as setup] [user :as user]) (metabase.api.meta [dataset :as dataset] [db :as db] @@ -42,7 +43,8 @@ (context "/result" [] (+auth result/routes)) (context "/search" [] (+auth search/routes)) (context "/session" [] session/routes) - (context "/setting" [] (+auth setting/routes)) + (context "/setting" [] (+auth setting/routes)) + (context "/setup" [] setup/routes) (context "/user" [] (+auth user/routes)) (route/not-found (fn [{:keys [request-method uri]}] {:status 404 diff --git a/src/metabase/api/setup.clj b/src/metabase/api/setup.clj new file mode 100644 index 00000000000..b1f3455d97b --- /dev/null +++ b/src/metabase/api/setup.clj @@ -0,0 +1,38 @@ +(ns metabase.api.setup + (:require [compojure.core :refer [defroutes POST]] + [metabase.api.common :refer :all] + [metabase.db :refer :all] + (metabase.models [session :refer [Session]] + [user :refer [User set-user-password]]) + [metabase.setup :as setup] + [metabase.util :as util] + [metabase.util.password :as password])) + + +;; special endpoint for creating the first user during setup +;; this endpoint both creates the user AND logs them in and returns a session id +(defendpoint POST "/user" [:as {{:keys [token first_name last_name email password] :as body} :body}] + ;; check our input + (require-params token first_name last_name email password) + (check-400 (and (util/is-email? email) (password/is-complex? password))) + ;; the submitted token must match our setup token + (check-403 (setup/token-match? token)) + ;; extra check. don't continue if there is already a user in the db. + (let [session-id (str (java.util.UUID/randomUUID)) + new-user (ins User + :email email + :first_name first_name + :last_name last_name + :password (str (java.util.UUID/randomUUID)))] + ;; this results in a second db call, but it avoids redundant password code so figure it's worth it + (set-user-password (:id new-user) password) + ;; clear the setup token now, it's no longer needed + (setup/token-clear) + ;; then we create a session right away because we want our new user logged in to continue the setup process + (ins Session + :id session-id + :user_id (:id new-user)) + {:id session-id})) + + +(define-routes) diff --git a/src/metabase/core.clj b/src/metabase/core.clj index d1bd2da87d6..e0b7288550f 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -9,7 +9,7 @@ [format :refer :all]) [metabase.models.user :refer [User]] [metabase.routes :as routes] - [metabase.util :as util] + [metabase.setup :as setup] [ring.adapter.jetty :as ring-jetty] (ring.middleware [cookies :refer [wrap-cookies]] [gzip :refer [wrap-gzip]] @@ -45,10 +45,21 @@ ;; startup database. validates connection & runs any necessary migrations (db/setup-db :auto-migrate (config/config-bool :mb-db-automigrate)) - ;; this is a temporary need until we finalize the code for bootstrapping the first user - (when-not (or (db/exists? User :id 1) - (db/exists? User :email "admin@admin.com")) ; possible for User 1 to have been deleted - (db/ins User :email "admin@admin.com" :first_name "admin" :last_name "admin" :password "admin" :is_superuser true)) + ;; run a very quick check to see if we are doing a first time installation + ;; the test we are using is if there is at least 1 User in the database + (when-not (db/sel :one :fields [User :id]) + (log/info "Looks like this is a new installation ... preparing setup wizard") + (let [setup-token (setup/token-create) + hostname (or (config/config-str :mb-jetty-host) "localhost") + port (config/config-int :mb-jetty-port) + setup-url (str "http://" + (or hostname "localhost") + (when-not (= 80 port) (str ":" port)) + "/setup/" + setup-token)] + (log/info (str "Please use the following url to setup your Metabase installation:\n\n" + setup-url + "\n\n")))) (log/info "Metabase Initialization COMPLETE") true) diff --git a/src/metabase/setup.clj b/src/metabase/setup.clj new file mode 100644 index 00000000000..bce73e93bd2 --- /dev/null +++ b/src/metabase/setup.clj @@ -0,0 +1,25 @@ +(ns metabase.setup) + + +(def ^:private setup-token + (atom nil)) + +(defn token-match? + "Function for checking if the supplied string matches our setup token. + Returns boolean `true` if supplied token matches `@setup-token`, `false` otherwise." + [token] + {:pre [(string? token)]} + (= token @setup-token)) + +(defn token-create + "Create and set a new `@setup-token`. + Returns the newly created token." + [] + (reset! setup-token (.toString (java.util.UUID/randomUUID)))) + +(defn token-clear + "Clear the `@setup-token` if it exists and reset it to nil." + [] + (reset! setup-token nil)) + + diff --git a/test/metabase/api/setup_test.clj b/test/metabase/api/setup_test.clj new file mode 100644 index 00000000000..e466534c728 --- /dev/null +++ b/test/metabase/api/setup_test.clj @@ -0,0 +1,72 @@ +(ns metabase.api.setup-test + "Tests for /api/setup endpoints." + (:require [expectations :refer :all] + [metabase.db :refer :all] + [metabase.http-client :as http] + (metabase.models [session :refer [Session]] + [user :refer [User]]) + [metabase.setup :as setup] + [metabase.test.util :refer [match-$ random-name expect-eval-actual-first]] + [metabase.test-data :refer :all])) + + +;; ## POST /api/setup/user +;; Check that we can create a new superuser via setup-token +(let [setup-token (setup/token-create) + user-name (random-name)] + (expect-eval-actual-first + (match-$ (->> (sel :one User :email (str user-name "@metabase.com")) + (:id) + (sel :one Session :user_id)) + {:id $id}) + (http/client :post 200 "setup/user" {:token setup-token + :first_name user-name + :last_name user-name + :email (str user-name "@metabase.com") + :password "anythingUP12!!"}))) + + +;; Test input validations +(expect "'token' is a required param." + (http/client :post 400 "setup/user" {})) + +(expect "'first_name' is a required param." + (http/client :post 400 "setup/user" {:token "anything"})) + +(expect "'last_name' is a required param." + (http/client :post 400 "setup/user" {:token "anything" + :first_name "anything"})) + +(expect "'email' is a required param." + (http/client :post 400 "setup/user" {:token "anything" + :first_name "anything" + :last_name "anything"})) + +(expect "'password' is a required param." + (http/client :post 400 "setup/user" {:token "anything" + :first_name "anything" + :last_name "anything" + :email "anything"})) + +;; valid email + complex password +(expect "Invalid Request." + (http/client :post 400 "setup/user" {:token "anything" + :first_name "anything" + :last_name "anything" + :email "anything" + :password "anything"})) + +(expect "Invalid Request." + (http/client :post 400 "setup/user" {:token "anything" + :first_name "anything" + :last_name "anything" + :email "anything@email.com" + :password "anything"})) + +;; token match +(expect "You don't have permissions to do that." + (http/client :post 403 "setup/user" {:token "anything" + :first_name "anything" + :last_name "anything" + :email "anything@email.com" + :password "anythingUP12!!"})) -- GitLab