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