diff options
author | Griffin Smith <grfn@gws.fyi> | 2021-12-19T04·37-0500 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2021-12-19T04·43+0000 |
commit | 2bc742964163217982d43d74e4a06968de09d67b (patch) | |
tree | 20094a736d75e839c6689de8ae79cb50af3813c5 /users | |
parent | 1205b42ee0436287fea654510db9323e8d59a395 (diff) |
feat(grfn/bbbg): Allow Organizers to sign in via Discord r/3298
Allow users with the Organizers role to sign in via a Discord Oauth2 handshake, creating a user in the users table and adding the ID of that user to the session. Change-Id: I39d9e17433e71b07314b9eabb787fb9214289772 Reviewed-on: https://cl.tvl.fyi/c/depot/+/4409 Tested-by: BuildkiteCI Reviewed-by: grfn <grfn@gws.fyi> Autosubmit: grfn <grfn@gws.fyi>
Diffstat (limited to 'users')
-rw-r--r-- | users/grfn/bbbg/deps.edn | 2 | ||||
-rw-r--r-- | users/grfn/bbbg/deps.nix | 182 | ||||
-rw-r--r-- | users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql | 1 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/db/user.clj | 10 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/discord.clj | 43 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/discord/auth.clj | 83 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/handlers/home.clj | 46 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/user.clj | 8 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/util/core.clj | 15 | ||||
-rw-r--r-- | users/grfn/bbbg/src/bbbg/web.clj | 37 |
10 files changed, 409 insertions, 18 deletions
diff --git a/users/grfn/bbbg/deps.edn b/users/grfn/bbbg/deps.edn index 1028771457cb..c1a05c7a7c90 100644 --- a/users/grfn/bbbg/deps.edn +++ b/users/grfn/bbbg/deps.edn @@ -14,6 +14,8 @@ ring/ring {:mvn/version "1.9.4"} compojure/compojure {:mvn/version "1.6.2"} javax.servlet/servlet-api {:mvn/version "2.5"} + ring-oauth2/ring-oauth2 {:mvn/version "0.2.0"} + clj-http/clj-http {:mvn/version "3.12.3"} ;; Web hiccup/hiccup {:mvn/version "1.0.5"} diff --git a/users/grfn/bbbg/deps.nix b/users/grfn/bbbg/deps.nix index e1c17f5609b2..9110ccb9ae05 100644 --- a/users/grfn/bbbg/deps.nix +++ b/users/grfn/bbbg/deps.nix @@ -55,6 +55,19 @@ let repos = [ } rec { + name = "joda-time/joda-time"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "joda-time"; + groupId = "joda-time"; + sha512 = "012fb9aa9b00b456f72a92374855a7f062f8617c026c436eee2cda67dffa2f8622201909c0f4f454bb346ff5a3ed6f60c236fafb19fa66f612d9861f27b38d3a"; + version = "2.10"; + + }; + paths = [ src ]; + } + + rec { name = "commons-codec/commons-codec"; src = fetchMavenArtifact { inherit repos; @@ -302,6 +315,19 @@ let repos = [ } rec { + name = "httpasyncclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpasyncclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "0a80db5dbf772f02d02ba6c7c163e8da9517dd7195714b495acb845c429580c1fc926d3e71c115e75be8c145651dce2fdfa0dc380132f7809c14b3ad95492aee"; + version = "4.1.4"; + + }; + paths = [ src ]; + } + + rec { name = "logback-jackson/ch.qos.logback.contrib"; src = fetchMavenArtifact { inherit repos; @@ -341,6 +367,19 @@ let repos = [ } rec { + name = "ring-oauth2/ring-oauth2"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "ring-oauth2"; + groupId = "ring-oauth2"; + sha512 = "3ed765b4bbb5749fcdcdb501b93ab656a413ade5af24c7aa34639718ed1fd0a5f325b05bd135540d56e55cbb456a2cb7852ba0e45bc5233e28229986eef75bb9"; + version = "0.2.0"; + + }; + paths = [ src ]; + } + + rec { name = "tools.macro/org.clojure"; src = fetchMavenArtifact { inherit repos; @@ -419,6 +458,32 @@ let repos = [ } rec { + name = "slingshot/slingshot"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "slingshot"; + groupId = "slingshot"; + sha512 = "ff2b2a27b441d230261c7f3ec8c38aa551865e05ab6438a74bd12bfcbc5f6bdc88199d42aaf5932b47df84f3d2700c8f514b9f4e9b5da28d29da7ff6b09a7fb5"; + version = "0.12.2"; + + }; + paths = [ src ]; + } + + rec { + name = "httpcore-nio/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore-nio"; + groupId = "org.apache.httpcomponents"; + sha512 = "002af5f72b68a4ff1b1ff46b788013283d195e1d62ee1d7b102aa930b30f77f7e215a6d18edbea0fccd18fb1fa3a66cc4aef6070d72d6d1886f0044dfe0e16c7"; + version = "4.4.10"; + + }; + paths = [ src ]; + } + + rec { name = "ring-jetty-adapter/ring"; src = fetchMavenArtifact { inherit repos; @@ -536,6 +601,32 @@ let repos = [ } rec { + name = "clj-time/clj-time"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-time"; + groupId = "clj-time"; + sha512 = "cfeb46af59fd4112aa5a5d0087a39355f0fc19514b4c02bc6c3d9f81c9bda40491686207836e9a7943aebeb82a3b36f4e8b7407a8908c5ef151122644b278d75"; + version = "0.15.2"; + + }; + paths = [ src ]; + } + + rec { + name = "clj-http/clj-http"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "clj-http"; + groupId = "clj-http"; + sha512 = "9884557d4f38068cb3234aec80acc0de8f9716645529693ffd9bd6db8221f5d1cf9e2d1b8bf7c7df4215d71372b02d83043ebf8fc27dc422552b32c9bdba1602"; + version = "3.12.3"; + + }; + paths = [ src ]; + } + + rec { name = "jul-to-slf4j/org.slf4j"; src = fetchMavenArtifact { inherit repos; @@ -562,6 +653,32 @@ let repos = [ } rec { + name = "httpcore/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpcore"; + groupId = "org.apache.httpcomponents"; + sha512 = "f16a652f4a7b87dbf7cb16f8590d54a3f719c4c7b2f8883ce59db2d73be4701b64f2ca8a2c45aca6a5dbeaddeedff0c280a03722f70c076e239b645faa54eff9"; + version = "4.4.14"; + + }; + paths = [ src ]; + } + + rec { + name = "httpclient-cache/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient-cache"; + groupId = "org.apache.httpcomponents"; + sha512 = "e150e8dc49c8c9972d8b324b56bb292b15e2f0e686f1292c4edac975615dfb16e5edb8ab325e614732a7d43a03061ca4fe93fe1e1f7487851a4d4d3af50a61f9"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { name = "instaparse/instaparse"; src = fetchMavenArtifact { inherit repos; @@ -627,6 +744,19 @@ let repos = [ } rec { + name = "riddley/riddley"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "riddley"; + groupId = "riddley"; + sha512 = "b478ecba9d1ab9d38c84a42354586fcece763000907b40c97bc43c0f16dc560b0860144efe410193cb3b7cb0149fbc1724fdd737cc3ba53de23618f5b30e6f9f"; + version = "0.1.12"; + + }; + paths = [ src ]; + } + + rec { name = "java.classpath/org.clojure"; src = fetchMavenArtifact { inherit repos; @@ -679,6 +809,19 @@ let repos = [ } rec { + name = "commons-logging/commons-logging"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "commons-logging"; + groupId = "commons-logging"; + sha512 = "ed00dbfabd9ae00efa26dd400983601d076fe36408b7d6520084b447e5d1fa527ce65bd6afdcb58506c3a808323d28e88f26cb99c6f5db9ff64f6525ecdfa557"; + version = "1.2"; + + }; + paths = [ src ]; + } + + rec { name = "clojure.java-time/clojure.java-time"; src = fetchMavenArtifact { inherit repos; @@ -809,6 +952,19 @@ let repos = [ } rec { + name = "httpclient/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpclient"; + groupId = "org.apache.httpcomponents"; + sha512 = "3567739186e551f84cad3e4b6b270c5b8b19aba297675a96bcdff3663ff7d20d188611d21f675fe5ff1bfd7d8ca31362070910d7b92ab1b699872a120aa6f089"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { name = "crypto-equality/crypto-equality"; src = fetchMavenArtifact { inherit repos; @@ -965,6 +1121,19 @@ let repos = [ } rec { + name = "potemkin/potemkin"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "potemkin"; + groupId = "potemkin"; + sha512 = "5abc050bf7ff0b27d8c45aaa5e378201980815b711b2db99735db73304576c17e285026ea48a714bf0b0df7ad7a008de38b7d182cdc0e8989f4be1e6b3afa8aa"; + version = "0.4.5"; + + }; + paths = [ src ]; + } + + rec { name = "netty-resolver/io.netty"; src = fetchMavenArtifact { inherit repos; @@ -1134,6 +1303,19 @@ let repos = [ } rec { + name = "httpmime/org.apache.httpcomponents"; + src = fetchMavenArtifact { + inherit repos; + artifactId = "httpmime"; + groupId = "org.apache.httpcomponents"; + sha512 = "e1b0ee84bce78576074dc1b6836a69d8f5518eade38562e6890e3ddaa72b7f54bf735c8e2286142c58cddf45f745da31261e5d73b7d8092eb6ecfb20946eb36c"; + version = "4.5.13"; + + }; + paths = [ src ]; + } + + rec { name = "log4j-over-slf4j/org.slf4j"; src = fetchMavenArtifact { inherit repos; diff --git a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql index 8c3276e1e61d..9718d84748ae 100644 --- a/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql +++ b/users/grfn/bbbg/resources/migrations/20211212165646-init-schema.up.sql @@ -26,6 +26,7 @@ CREATE TABLE "event_attendee" ( -- ;; CREATE TABLE "user" ( "id" UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + "username" TEXT NOT NULL, "discord_user_id" TEXT NOT NULL, "created_at" TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() ); diff --git a/users/grfn/bbbg/src/bbbg/db/user.clj b/users/grfn/bbbg/src/bbbg/db/user.clj new file mode 100644 index 000000000000..7db73e378d91 --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/db/user.clj @@ -0,0 +1,10 @@ +(ns bbbg.db.user + (:require [bbbg.db :as db] + [bbbg.user :as user])) + +(defn create! [db attrs] + (db/insert! db + :public.user + (select-keys attrs [::user/id + ::user/username + ::user/discord-user-id]))) diff --git a/users/grfn/bbbg/src/bbbg/discord.clj b/users/grfn/bbbg/src/bbbg/discord.clj new file mode 100644 index 000000000000..ce8568ad827c --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/discord.clj @@ -0,0 +1,43 @@ +(ns bbbg.discord + (:refer-clojure :exclude [get]) + (:require [clj-http.client :as http] + [clojure.string :as str] + [bbbg.util.core :as u])) + +(def base-uri "https://discord.com/api") + +(defn api-uri [path] + (str base-uri + (when-not (str/starts-with? path "/") "/") + path)) + +(defn get + ([token path] + (get token path {})) + ([token path params] + (:body + (http/get (api-uri path) + (-> params + (assoc :accept :json + :as :json) + (assoc-in [:headers "authorization"] + (str "Bearer " (:token token)))))))) + +(defn me [token] + (get token "/users/@me")) + +(defn guilds [token] + (get token "/users/@me/guilds")) + +(defn guild-member [token guild-id] + (get token (str "/users/@me/guilds/" guild-id "/member"))) + +(comment + (def token {:token (u/pass "bbbg/test-token")}) + (me token) + (guilds token) + (guild-member token "841295283564052510") + + (get token "/guilds/841295283564052510/roles") + + ) diff --git a/users/grfn/bbbg/src/bbbg/discord/auth.clj b/users/grfn/bbbg/src/bbbg/discord/auth.clj new file mode 100644 index 000000000000..fddd15fd1859 --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/discord/auth.clj @@ -0,0 +1,83 @@ +(ns bbbg.discord.auth + (:require + [bbbg.discord :as discord] + [bbbg.util.core :as u] + clj-time.coerce + [clojure.spec.alpha :as s] + [config.core :refer [env]] + [ring.middleware.oauth2 :refer [wrap-oauth2]])) + +(s/def ::client-id string?) +(s/def ::client-secret string?) +(s/def ::bbbg-guild-id string?) +(s/def ::bbbg-organizer-role string?) + +(s/def ::config (s/keys :req [::client-id + ::client-secret + ::bbbg-guild-id + ::bbbg-organizer-role])) + +;;; + +(defn env->config [] + (s/assert + ::config + {::client-id (:discord-client-id env) + ::client-secret (:discord-client-secret env) + ::bbbg-guild-id (:bbbg-guild-id env "841295283564052510") + ::bbbg-organizer-role (:bbbg-organizer-role + env + ;; TODO this might not be the right id + "902593101758091294")})) + +(defn dev-config [] + (s/assert + ::config + {::client-id (u/pass "bbbg/discord-client-id") + ::client-secret (u/pass "bbbg/discord-client-secret") + ::bbbg-guild-id "841295283564052510" + ;; TODO this might not be the right id + ::bbbg-organizer-role "874846495873040395"})) + +;;; + +(def access-token-url + "https://discord.com/api/oauth2/token") + +(def authorization-url + "https://discord.com/api/oauth2/authorize") + +(def revoke-url + "https://discord.com/api/oauth2/token/revoke") + +(def scopes ["guilds" + "guilds.members.read" + "identify"]) + +(defn discord-oauth-profile [env] + {:authorize-uri authorization-url + :access-token-uri access-token-url + :client-id (::client-id env) + :client-secret (::client-secret env) + :scopes scopes + :launch-uri "/auth/discord" + :redirect-uri "/auth/discord/redirect" + :landing-uri "/auth/success"}) + +(defn wrap-discord-auth [handler env] + (wrap-oauth2 handler {:discord (discord-oauth-profile env)})) + +(defn check-discord-auth + "Check that the user with the given token has the correct level of discord + auth" + [{::keys [bbbg-guild-id bbbg-organizer-role]} token] + (and (some (comp #{bbbg-guild-id} :id) + (discord/guilds token)) + (some #{bbbg-organizer-role} + (:roles (discord/guild-member token bbbg-guild-id))))) + +(comment + (#'ring.middleware.oauth2/valid-profile? + (discord-oauth-profile + (dev-config))) + ) diff --git a/users/grfn/bbbg/src/bbbg/handlers/home.clj b/users/grfn/bbbg/src/bbbg/handlers/home.clj index d5ba72878ab1..480706574569 100644 --- a/users/grfn/bbbg/src/bbbg/handlers/home.clj +++ b/users/grfn/bbbg/src/bbbg/handlers/home.clj @@ -1,17 +1,49 @@ (ns bbbg.handlers.home (:require + [bbbg.db.user :as db.user] + [bbbg.discord.auth :as discord.auth] [bbbg.handlers.core :refer [page-response]] - [compojure.core :refer [GET routes]])) + [bbbg.user :as user] + [bbbg.views.flash :as flash] + [compojure.core :refer [GET routes]] + [ring.util.response :refer [redirect]] + [bbbg.discord :as discord])) -(defn- home-page [] +(defn- home-page [{:keys [authenticated?]}] [:nav.home-nav [:ul [:li [:a {:href "/signup-forms"} "Event Signup Form"]] - [:li [:a {:href "/login"} - "Sign In"]]]]) + (when-not authenticated? + [:li [:a {:href "/auth/discord"} + "Sign In"]])]]) -(defn home-routes [_env] +(defn auth-failure [] + [:div.auth-failure + [:p + "Sorry, only users with the Organizers role in discord can sign in"] + [:p + [:a {:href "/"} "Go Back"]]]) + +(defn home-routes [{:keys [db] :as env}] (routes - (GET "/" [] - (page-response (home-page))))) + (GET "/" request + (let [authenticated? (some? (get-in request [:session ::user/id]))] + (page-response (home-page {:authenticated? authenticated?})))) + + (GET "/auth/success" request + (let [token (get-in request [:oauth2/access-tokens :discord])] + (if (discord.auth/check-discord-auth env token) + (let [discord-user (discord/me token) + user (db.user/create! + db + #::user{:username (:username discord-user) + :discord-user-id (:id discord-user)})] + (-> (redirect "/") + (assoc-in [:session ::user/id] (::user/id user)) + (flash/add-flash + {:flash/message "Successfully Signed In" + :flash/type :success}))) + (-> + (page-response (auth-failure)) + (assoc :status 401))))))) diff --git a/users/grfn/bbbg/src/bbbg/user.clj b/users/grfn/bbbg/src/bbbg/user.clj new file mode 100644 index 000000000000..f48c8d73388e --- /dev/null +++ b/users/grfn/bbbg/src/bbbg/user.clj @@ -0,0 +1,8 @@ +(ns bbbg.user + (:require [clojure.spec.alpha :as s])) + +(s/def ::id uuid?) + +(s/def ::discord-id string?) + +(s/def ::username string?) diff --git a/users/grfn/bbbg/src/bbbg/util/core.clj b/users/grfn/bbbg/src/bbbg/util/core.clj index 7f2a8516bf86..9ef8ef6bee77 100644 --- a/users/grfn/bbbg/src/bbbg/util/core.clj +++ b/users/grfn/bbbg/src/bbbg/util/core.clj @@ -1,5 +1,9 @@ (ns bbbg.util.core - (:import java.util.UUID)) + (:require + [clojure.java.shell :refer [sh]] + [clojure.string :as str]) + (:import + java.util.UUID)) (defn remove-nils "Remove all keys with nil values from m" @@ -115,3 +119,12 @@ (cons f (step (rest s) (conj seen (distinction-fn f))))))) xs seen)))] (step coll #{}))) + +(defn pass [n] + (let [{:keys [exit out err]} (sh "pass" n)] + (if (= 0 exit) + (str/trim out) + (throw (Exception. + (format "`pass` command failed\nStandard output:%s\nStandard Error:%s" + out + err)))))) diff --git a/users/grfn/bbbg/src/bbbg/web.clj b/users/grfn/bbbg/src/bbbg/web.clj index cbef8d0e5d32..21e70d1470ad 100644 --- a/users/grfn/bbbg/src/bbbg/web.clj +++ b/users/grfn/bbbg/src/bbbg/web.clj @@ -1,5 +1,6 @@ (ns bbbg.web (:require + [bbbg.discord.auth :as discord.auth :refer [wrap-discord-auth]] [bbbg.handlers.attendees :as attendees] [bbbg.handlers.events :as events] [bbbg.handlers.home :as home] @@ -7,6 +8,7 @@ [bbbg.styles :refer [stylesheet]] [bbbg.util.core :as u] [bbbg.views.flash :refer [wrap-page-flash]] + clj-time.coerce [clojure.spec.alpha :as s] [com.stuartsierra.component :as component] [compojure.core :refer [GET routes]] @@ -27,8 +29,10 @@ (s/and bytes? #(= 16 (count %)))) (s/def ::config - (s/keys :req [::port] - :opt [::cookie-secret])) + (s/merge + (s/keys :req [::port] + :opt [::cookie-secret]) + ::discord.auth/config)) (s/fdef make-server :args (s/cat :config ::config)) @@ -45,14 +49,18 @@ (s/assert ::config (u/remove-nils - {::port (:port env 8888) - ::cookie-secret (some-> env :cookie-secret string->cookie-secret)}))) + (merge + {::port (:port env 8888) + ::cookie-secret (some-> env :cookie-secret string->cookie-secret)} + (discord.auth/env->config))))) (defn dev-config [] (s/assert ::config - {::port 8888 - ::cookie-secret (into-array Byte/TYPE (repeat 16 0))})) + (merge + {::port 8888 + ::cookie-secret (into-array Byte/TYPE (repeat 16 0))} + (discord.auth/dev-config)))) ;;; @@ -72,11 +80,16 @@ (defn middleware [app env] (-> app + (wrap-discord-auth env) wrap-keyword-params wrap-params wrap-page-flash wrap-flash - (wrap-session {:store (cookie-store {:key (:cookie-secret env)})}))) + (wrap-session {:store (cookie-store + {:key (:cookie-secret env) + :readers {'clj-time/date-time + clj-time.coerce/from-string}}) + :cookie-attrs {:same-site :lax}}))) (defn handler [env] (-> (app-routes env) @@ -96,8 +109,12 @@ (dissoc this ::shutdown-fn)) this))) -(defn make-server [{::keys [port cookie-secret]}] +(defn make-server [{::keys [port cookie-secret] + :as env}] (component/using - (map->WebServer {:port port - :cookie-secret cookie-secret}) + (map->WebServer + (merge + {:port port + :cookie-secret cookie-secret} + env)) [:db])) |