@@ -49,6 +49,7 @@ docker run --rm -p 3000:3000 tbd
|
|||||||
| `GET` | `/auth/callback` | OAuth redirect target: completes the login, sets the session cookie, redirects to `/` |
|
| `GET` | `/auth/callback` | OAuth redirect target: completes the login, sets the session cookie, redirects to `/` |
|
||||||
| `POST` | `/auth/logout` | Ends the session |
|
| `POST` | `/auth/logout` | Ends the session |
|
||||||
| `GET` | `/auth/me` | The logged-in user, `{:login "alice" :id 7 :admin? true}` (401 if not logged in) |
|
| `GET` | `/auth/me` | The logged-in user, `{:login "alice" :id 7 :admin? true}` (401 if not logged in) |
|
||||||
|
| `GET` | `/nrepl` | **Admin only.** Whether the nREPL debug server is on: `{:enabled false}`, or `{:enabled true :bind "127.0.0.1" :port 7888}` |
|
||||||
| `POST` | `/nrepl` | **Admin only.** Body `{"enabled": true}` (JSON or EDN) starts the nREPL debug server, `false` stops it; returns `{:enabled true :bind "127.0.0.1" :port 7888}` |
|
| `POST` | `/nrepl` | **Admin only.** Body `{"enabled": true}` (JSON or EDN) starts the nREPL debug server, `false` stops it; returns `{:enabled true :bind "127.0.0.1" :port 7888}` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -60,7 +61,7 @@ curl 'localhost:3000/api/add?x=2&y=3' # {:result 5}
|
|||||||
|
|
||||||
Login is OAuth2 (authorization-code flow) against a [Gitea](https://about.gitea.com/) instance.
|
Login is OAuth2 (authorization-code flow) against a [Gitea](https://about.gitea.com/) instance.
|
||||||
Anyone with a Gitea account can log in, but routes marked `:admin? true` (currently
|
Anyone with a Gitea account can log in, but routes marked `:admin? true` (currently
|
||||||
`POST /nrepl`) also require the user to be on the `ADMIN_USERS` allowlist: **401** without a
|
`GET` and `POST /nrepl`) also require the user to be on the `ADMIN_USERS` allowlist: **401** without a
|
||||||
login, **403** for a logged-in non-admin. With no allowlist, nobody is an admin.
|
login, **403** for a logged-in non-admin. With no allowlist, nobody is an admin.
|
||||||
|
|
||||||
1. In Gitea (Settings > Applications) create an OAuth2 application, a confidential client, with
|
1. In Gitea (Settings > Applications) create an OAuth2 application, a confidential client, with
|
||||||
@@ -154,7 +155,7 @@ src/tbd/
|
|||||||
<group>/routes.clj Each route group (health, math, ...) owns its route subtree
|
<group>/routes.clj Each route group (health, math, ...) owns its route subtree
|
||||||
<group>/core.clj Business logic for the group, with no HTTP concerns
|
<group>/core.clj Business logic for the group, with no HTTP concerns
|
||||||
auth/ Gitea OAuth (core), session + admin middleware (middleware), /auth routes
|
auth/ Gitea OAuth (core), session + admin middleware (middleware), /auth routes
|
||||||
nrepl/ On-demand nREPL server (core) and the admin-only POST /nrepl route
|
nrepl/ On-demand nREPL server (core) and the admin-only `/nrepl` routes
|
||||||
ui/ ClojureScript re-frame app (shadow-cljs project); ui/src/tbd/ui/{db,events,subs,views,core}.cljs
|
ui/ ClojureScript re-frame app (shadow-cljs project); ui/src/tbd/ui/{db,events,subs,views,core}.cljs
|
||||||
.env-template Settings the server reads from the environment; copy to .env (git-ignored)
|
.env-template Settings the server reads from the environment; copy to .env (git-ignored)
|
||||||
resources/public/ index.html (the shell served at /) and js/ (compiled UI output, git-ignored)
|
resources/public/ index.html (the shell served at /) and js/ (compiled UI output, git-ignored)
|
||||||
|
|||||||
@@ -15,9 +15,16 @@
|
|||||||
(println "[tbd] nREPL toggle failed:" (.getMessage e))
|
(println "[tbd] nREPL toggle failed:" (.getMessage e))
|
||||||
(edn-response 500 {:error (str "could not change nREPL state: " (.getMessage e))})))))
|
(edn-response 500 {:error (str "could not change nREPL state: " (.getMessage e))})))))
|
||||||
|
|
||||||
|
(defn- status-handler [_req]
|
||||||
|
(edn-response 200 (nrepl/status)))
|
||||||
|
|
||||||
(defn routes [config]
|
(defn routes [config]
|
||||||
["/nrepl"
|
["/nrepl"
|
||||||
{:post {:tags ["admin"]
|
{:get {:tags ["admin"]
|
||||||
|
:summary "Read whether the nREPL debug server is on or off (admin only)"
|
||||||
|
:admin? true
|
||||||
|
:handler status-handler}
|
||||||
|
:post {:tags ["admin"]
|
||||||
:summary "Enable or disable the nREPL debug server (admin only)"
|
:summary "Enable or disable the nREPL debug server (admin only)"
|
||||||
:admin? true
|
:admin? true
|
||||||
:parameters {:body [:map [:enabled {:description "true starts the server, false stops it"} :boolean]]}
|
:parameters {:body [:map [:enabled {:description "true starts the server, false stops it"} :boolean]]}
|
||||||
|
|||||||
@@ -22,17 +22,24 @@
|
|||||||
body (assoc :body (ByteArrayInputStream. (.getBytes ^String body)))))))
|
body (assoc :body (ByteArrayInputStream. (.getBytes ^String body)))))))
|
||||||
(cond-> @resp (string? (:body @resp)) (update :body edn/read-string)))))
|
(cond-> @resp (string? (:body @resp)) (update :body edn/read-string)))))
|
||||||
|
|
||||||
|
(defn get-nrepl [app cookie]
|
||||||
|
(let [resp (app {:request-method :get :uri "/nrepl"
|
||||||
|
:headers (cond-> {} cookie (assoc "cookie" cookie))})]
|
||||||
|
(cond-> resp (string? (:body resp)) (update :body edn/read-string))))
|
||||||
|
|
||||||
(deftest anonymous-test
|
(deftest anonymous-test
|
||||||
(let [app (t/make-app (t/gitea-stub "alice"))]
|
(let [app (t/make-app (t/gitea-stub "alice"))]
|
||||||
(doseq [body ["{\"enabled\":true}" "{\"enabled\":\"junk\"}" nil]]
|
(doseq [body ["{\"enabled\":true}" "{\"enabled\":\"junk\"}" nil]]
|
||||||
(let [resp (post-nrepl app nil body)]
|
(let [resp (post-nrepl app nil body)]
|
||||||
(is (= 401 (:status resp)) (str "body: " body))))
|
(is (= 401 (:status resp)) (str "body: " body))))
|
||||||
|
(is (= 401 (:status (get-nrepl app nil))))
|
||||||
(is (= {:enabled false} (nrepl/status)) "nothing was started")))
|
(is (= {:enabled false} (nrepl/status)) "nothing was started")))
|
||||||
|
|
||||||
(deftest non-admin-test
|
(deftest non-admin-test
|
||||||
(let [app (t/make-app (t/gitea-stub "mallory"))
|
(let [app (t/make-app (t/gitea-stub "mallory"))
|
||||||
cookie (t/login! app)]
|
cookie (t/login! app)]
|
||||||
(is (= 403 (:status (post-nrepl app cookie "{\"enabled\":true}"))))
|
(is (= 403 (:status (post-nrepl app cookie "{\"enabled\":true}"))))
|
||||||
|
(is (= 403 (:status (get-nrepl app cookie))))
|
||||||
(is (= {:enabled false} (nrepl/status)) "nothing was started")))
|
(is (= {:enabled false} (nrepl/status)) "nothing was started")))
|
||||||
|
|
||||||
(deftest empty-allowlist-test
|
(deftest empty-allowlist-test
|
||||||
@@ -60,6 +67,24 @@
|
|||||||
(is (= 200 (:status resp)))
|
(is (= 200 (:status resp)))
|
||||||
(is (true? (get-in resp [:body :enabled])))))))
|
(is (true? (get-in resp [:body :enabled])))))))
|
||||||
|
|
||||||
|
(deftest status-test
|
||||||
|
(let [app (t/make-app (t/gitea-stub "alice"))
|
||||||
|
cookie (t/login! app)]
|
||||||
|
(testing "off"
|
||||||
|
(let [resp (get-nrepl app cookie)]
|
||||||
|
(is (= 200 (:status resp)))
|
||||||
|
(is (= "application/edn; charset=utf-8" (get-in resp [:headers "Content-Type"])))
|
||||||
|
(is (= {:enabled false} (:body resp)))))
|
||||||
|
(testing "on"
|
||||||
|
(with-out-str (post-nrepl app cookie "{\"enabled\":true}"))
|
||||||
|
(let [resp (get-nrepl app cookie)]
|
||||||
|
(is (= 200 (:status resp)))
|
||||||
|
(is (= {:enabled true :bind "127.0.0.1"} (dissoc (:body resp) :port)))
|
||||||
|
(is (= (:body resp) (nrepl/status)))))
|
||||||
|
(testing "off again"
|
||||||
|
(with-out-str (post-nrepl app cookie "{\"enabled\":false}"))
|
||||||
|
(is (= {:enabled false} (:body (get-nrepl app cookie)))))))
|
||||||
|
|
||||||
(deftest invalid-body-test
|
(deftest invalid-body-test
|
||||||
(let [app (t/make-app (t/gitea-stub "alice"))
|
(let [app (t/make-app (t/gitea-stub "alice"))
|
||||||
cookie (t/login! app)]
|
cookie (t/login! app)]
|
||||||
@@ -89,4 +114,5 @@
|
|||||||
(->> (m/decode "application/json")))
|
(->> (m/decode "application/json")))
|
||||||
paths (into {} (map (fn [[k v]] [(str (namespace k) "/" (name k)) v])) (:paths spec))]
|
paths (into {} (map (fn [[k v]] [(str (namespace k) "/" (name k)) v])) (:paths spec))]
|
||||||
(is (contains? (get-in paths ["/nrepl"]) :post) "documented")
|
(is (contains? (get-in paths ["/nrepl"]) :post) "documented")
|
||||||
|
(is (contains? (get-in paths ["/nrepl"]) :get) "documented")
|
||||||
(is (contains? paths "/auth/login"))))
|
(is (contains? paths "/auth/login"))))
|
||||||
|
|||||||
Reference in New Issue
Block a user