Add the ability to check roles to openid integration.

This commit is contained in:
Andrey Antukh 2021-04-16 13:07:41 +02:00 committed by Andrés Moya
parent 465a25145d
commit 2828ccda7f
5 changed files with 93 additions and 27 deletions

View file

@ -111,6 +111,9 @@
(s/def ::oidc-token-uri ::us/string) (s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string) (s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string) (s/def ::oidc-user-uri ::us/string)
(s/def ::oidc-scopes ::us/set-of-str)
(s/def ::oidc-roles ::us/set-of-str)
(s/def ::oidc-roles-attr ::us/keyword)
(s/def ::host ::us/string) (s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer) (s/def ::http-server-port ::us/integer)
(s/def ::http-session-cookie-name ::us/string) (s/def ::http-session-cookie-name ::us/string)
@ -190,6 +193,9 @@
::oidc-token-uri ::oidc-token-uri
::oidc-auth-uri ::oidc-auth-uri
::oidc-user-uri ::oidc-user-uri
::oidc-scopes
::oidc-roles-attr
::oidc-roles
::host ::host
::http-server-port ::http-server-port
::http-session-idle-max-age ::http-session-idle-max-age

View file

@ -13,7 +13,9 @@
[app.util.logging :as l] [app.util.logging :as l]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
[clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[lambdaisland.uri :as u])) [lambdaisland.uri :as u]))
@ -32,9 +34,7 @@
(defn register-profile (defn register-profile
[{:keys [rpc] :as cfg} info] [{:keys [rpc] :as cfg} info]
(let [method-fn (get-in rpc [:methods :mutation :login-or-register]) (let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info) profile (method-fn info)]
:backend (:backend info)
:fullname (:fullname info)})]
(cond-> profile (cond-> profile
(some? (:invitation-token info)) (some? (:invitation-token info))
(assoc :invitation-token (:invitation-token info))))) (assoc :invitation-token (:invitation-token info)))))
@ -60,7 +60,7 @@
:redirect_uri (build-redirect-uri cfg) :redirect_uri (build-redirect-uri cfg)
:response_type "code" :response_type "code"
:state state :state state
:scope (:scope provider)} :scope (str/join " " (:scopes provider []))}
query (u/map->query-string params)] query (u/map->query-string params)]
(-> (u/uri (:auth-uri provider)) (-> (u/uri (:auth-uri provider))
(assoc :query query) (assoc :query query)
@ -98,10 +98,10 @@
res (http/send! req)] res (http/send! req)]
(when (= 200 (:status res)) (when (= 200 (:status res))
(let [data (json/read-str (:body res))] (let [{:keys [name] :as data} (json/read-str (:body res) :key-fn keyword)]
{:email (get data "email") (-> data
:backend (:name provider) (assoc :backend (:name provider))
:fullname (get data "name")}))) (assoc :fullname name)))))
(catch Exception e (catch Exception e
(l/error :hint "unexpected exception on retrieve-user-info" (l/error :hint "unexpected exception on retrieve-user-info"
@ -109,7 +109,7 @@
nil))) nil)))
(defn retrieve-info (defn retrieve-info
[{:keys [tokens] :as cfg} request] [{:keys [tokens provider] :as cfg} request]
(let [state (get-in request [:params :state]) (let [state (get-in request [:params :state])
state (tokens :verify {:token state :iss :oauth}) state (tokens :verify {:token state :iss :oauth})
info (some->> (get-in request [:params :code]) info (some->> (get-in request [:params :code])
@ -119,6 +119,23 @@
(ex/raise :type :internal (ex/raise :type :internal
:code :unable-to-auth)) :code :unable-to-auth))
;; If the provider is OIDC, we can proceed to check
;; roles if they are defined.
(when (and (= "oidc" (:name provider))
(seq (:roles provider)))
(let [provider-roles (into #{} (:roles provider))
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
roles (get info attr)]
(cond
(string? roles) (into #{} (str/words roles))
(vector? roles) (into #{} roles)
:else #{}))]
;; check if profile has a configured set of roles
(when-not (set/subset? provider-roles profile-roles)
(ex/raise :type :internal
:code :unable-to-auth
:hint "not enought permissions"))))
(cond-> info (cond-> info
(some? (:invitation-token state)) (some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state))))) (assoc :invitation-token (:invitation-token state)))))
@ -198,7 +215,10 @@
:token-uri (cf/get :oidc-token-uri) :token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri) :auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri) :user-uri (cf/get :oidc-user-uri)
:scope "openid profile email name" :scopes (into #{"openid" "profile" "email" "name"}
(cf/get :oidc-scopes #{}))
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}] :name "oidc"}]
(if (and (string? (:base-uri opts)) (if (and (string? (:base-uri opts))
(string? (:client-id opts)) (string? (:client-id opts))
@ -218,10 +238,9 @@
[cfg] [cfg]
(let [opts {:client-id (cf/get :google-client-id) (let [opts {:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret) :client-secret (cf/get :google-client-secret)
:scope (str "email profile " :scopes #{"email" "profile" "openid"
"https://www.googleapis.com/auth/userinfo.email " "https://www.googleapis.com/auth/userinfo.email"
"https://www.googleapis.com/auth/userinfo.profile " "https://www.googleapis.com/auth/userinfo.profile"}
"openid")
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth" :auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token" :token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo" :user-uri "https://openidconnect.googleapis.com/v1/userinfo"
@ -237,7 +256,8 @@
[cfg] [cfg]
(let [opts {:client-id (cf/get :github-client-id) (let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret) :client-secret (cf/get :github-client-secret)
:scope "user:email" :scopes #{"read:user"
"user:email"}
:auth-uri "https://github.com/login/oauth/authorize" :auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token" :token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user" :user-uri "https://api.github.com/user"
@ -256,7 +276,7 @@
opts {:base-uri base opts {:base-uri base
:client-id (cf/get :gitlab-client-id) :client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret) :client-secret (cf/get :gitlab-client-secret)
:scope "read_user" :scopes #{"read_user"}
:auth-uri (str base "/oauth/authorize") :auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token") :token-uri (str base "/oauth/token")
:user-uri (str base "/api/v4/user") :user-uri (str base "/api/v4/user")

View file

@ -6,6 +6,7 @@
(ns app.rpc.mutations.profile (ns app.rpc.mutations.profile
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -303,16 +304,35 @@
(defn login-or-register (defn login-or-register
[{:keys [conn] :as cfg} {:keys [email backend] :as params}] [{:keys [conn] :as cfg} {:keys [email backend] :as params}]
(letfn [(create-profile [conn {:keys [fullname email]}] (letfn [(info->props [info]
(dissoc info :name :fullname :email :backend))
(info->lang [{:keys [locale] :as info}]
(when (and (string? locale)
(not (str/blank? locale)))
locale))
(create-profile [conn {:keys [email] :as info}]
(db/insert! conn :profile (db/insert! conn :profile
{:id (uuid/next) {:id (uuid/next)
:fullname fullname :fullname (:fullname info)
:email (str/lower email) :email (str/lower email)
:lang (info->lang info)
:auth-backend backend :auth-backend backend
:is-active true :is-active true
:password "!" :password "!"
:props (db/tjson (info->props info))
:is-demo false})) :is-demo false}))
(update-profile [conn info profile]
(let [props (d/merge (:props profile)
(info->props info))]
(db/update! conn :profile
{:props (db/tjson props)
:modified-at (dt/now)}
{:id (:id profile)})
(assoc profile :props props)))
(register-profile [conn params] (register-profile [conn params]
(let [profile (->> (create-profile conn params) (let [profile (->> (create-profile conn params)
(create-profile-relations conn))] (create-profile-relations conn))]
@ -321,7 +341,9 @@
(let [profile (profile/retrieve-profile-data-by-email conn email) (let [profile (profile/retrieve-profile-data-by-email conn email)
profile (if profile profile (if profile
(profile/populate-additional-data conn profile) (->> profile
(update-profile conn params)
(profile/populate-additional-data conn))
(register-profile conn params))] (register-profile conn params))]
(profile/strip-private-attrs profile)))) (profile/strip-private-attrs profile))))
@ -346,7 +368,6 @@
(update-profile conn params) (update-profile conn params)
nil)) nil))
;; --- Mutation: Update Password ;; --- Mutation: Update Password
(declare validate-password!) (declare validate-password!)

View file

@ -133,6 +133,20 @@
::s/invalid))] ::s/invalid))]
(s/def ::email (s/conformer cfn str))) (s/def ::email (s/conformer cfn str)))
;; --- SPEC: set-of-str
(letfn [(conformer [s]
(cond
(string? s) (into #{} (str/split s #"\s*,\s*"))
(set? s) (if (every? string? s)
s
::s/invalid)
:else ::s/invalid))
(unformer [s]
(str/join "," s))]
(s/def ::set-of-str (s/conformer conformer unformer)))
;; --- Macros ;; --- Macros
(defn spec-assert* (defn spec-assert*

View file

@ -29,8 +29,7 @@
(defn- parse-locale (defn- parse-locale
[locale] [locale]
(let [locale (-> (.-language globals/navigator) (let [locale (-> (str/lower locale)
(str/lower)
(str/replace "-" "_"))] (str/replace "-" "_"))]
(cond-> [locale] (cond-> [locale]
(str/includes? locale "_") (str/includes? locale "_")
@ -66,11 +65,17 @@
(set! translations data)) (set! translations data))
(defn set-locale! (defn set-locale!
[lang] [lname]
(if lang (if lname
(do (let [supported (into #{} (map :value supported-locales))
(swap! storage assoc ::locale lang) lname (loop [locales (seq (parse-locale lname))]
(reset! locale lang)) (if-let [locale (first locales)]
(if (contains? supported locale)
locale
(recur (rest locales)))
cfg/default-language))]
(swap! storage assoc ::locale lname)
(reset! locale lname))
(do (do
(swap! storage dissoc ::locale) (swap! storage dissoc ::locale)
(reset! locale (autodetect))))) (reset! locale (autodetect)))))