feat(grfn/bbbg): Allow Organizers to sign in via Discord

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>
This commit is contained in:
Griffin Smith 2021-12-18 23:37:14 -05:00 committed by clbot
parent 1205b42ee0
commit 2bc7429641
10 changed files with 409 additions and 18 deletions

View file

@ -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"}

View file

@ -54,6 +54,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -301,6 +314,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -340,6 +366,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -418,6 +457,32 @@ let repos = [
paths = [ src ];
}
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 {
@ -535,6 +600,32 @@ let repos = [
paths = [ src ];
}
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 {
@ -561,6 +652,32 @@ let repos = [
paths = [ src ];
}
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 {
@ -626,6 +743,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -678,6 +808,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -808,6 +951,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -964,6 +1120,19 @@ let repos = [
paths = [ src ];
}
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 {
@ -1133,6 +1302,19 @@ let repos = [
paths = [ src ];
}
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 {

View file

@ -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()
);

View file

@ -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])))

View file

@ -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")
)

View file

@ -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)))
)

View file

@ -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)))))))

View file

@ -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?)

View file

@ -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))))))

View file

@ -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]))