mirror of
https://github.com/penpot/penpot.git
synced 2025-05-05 00:05:53 +02:00
♻️ Refactor auth code
This commit is contained in:
parent
d021ac0226
commit
14d1cb90bd
30 changed files with 1306 additions and 960 deletions
20
CHANGES.md
20
CHANGES.md
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
## :rocket: Next
|
## :rocket: Next
|
||||||
|
|
||||||
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
|
- The `PENPOT_LOGIN_WITH_LDAP` environment variable is finally removed (after
|
||||||
|
many version with deprecation). It is replaced with the
|
||||||
|
`enable-login-with-ldap` flag.
|
||||||
|
- The `PENPOT_LDAP_ATTRS_PHOTO` finally removed, it was unused for many
|
||||||
|
versions.
|
||||||
|
- If you are using social login (google, github, gitlab or generic OIDC) you
|
||||||
|
will need to ensure to add the following flags respectivelly to let them
|
||||||
|
enabled: `enable-login-with-google`, `enable-login-with-github`,
|
||||||
|
`enable-login-with-gitlab` and `enable-login-with-oidc`. If not, they will
|
||||||
|
remain disabled after application start independently if you set the client-id
|
||||||
|
and client-sectet options.
|
||||||
|
- The `PENPOT_REGISTRATION_ENABLED` is finally removed in favour of
|
||||||
|
`<enable|disable>-registration` flag.
|
||||||
|
- The OIDC providers are now initialized synchronously, and if you are using the
|
||||||
|
discovery mechanism of the generic OIDC integration, the start time of the
|
||||||
|
application will depend on how fast the OIDC provider responds to the
|
||||||
|
discovery http request.
|
||||||
|
|
||||||
### :sparkles: New features
|
### :sparkles: New features
|
||||||
|
|
||||||
- Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982)
|
- Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982)
|
||||||
|
|
137
backend/src/app/auth/ldap.clj
Normal file
137
backend/src/app/auth/ldap.clj
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.auth.ldap
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
|
[clj-ldap.client :as ldap]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[clojure.string]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(defn- prepare-params
|
||||||
|
[cfg]
|
||||||
|
{:ssl? (:ssl cfg)
|
||||||
|
:startTLS? (:tls cfg)
|
||||||
|
:bind-dn (:bind-dn cfg)
|
||||||
|
:password (:bind-password cfg)
|
||||||
|
:host {:address (:host cfg)
|
||||||
|
:port (:port cfg)}})
|
||||||
|
|
||||||
|
(defn- connect
|
||||||
|
"Connects to the LDAP provider and returns a connection. An
|
||||||
|
exception is raised if no connection is possible."
|
||||||
|
^java.lang.AutoCloseable
|
||||||
|
[cfg]
|
||||||
|
(try
|
||||||
|
(-> cfg prepare-params ldap/connect)
|
||||||
|
(catch Throwable cause
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :unable-to-connect-to-ldap
|
||||||
|
:hint "unable to connect to ldap server"
|
||||||
|
:cause cause))))
|
||||||
|
|
||||||
|
(defn- replace-several [s & {:as replacements}]
|
||||||
|
(reduce-kv clojure.string/replace s replacements))
|
||||||
|
|
||||||
|
(defn- search-user
|
||||||
|
[{:keys [conn attrs base-dn] :as cfg} email]
|
||||||
|
(let [query (replace-several (:query cfg) ":username" email)
|
||||||
|
params {:filter query
|
||||||
|
:sizelimit 1
|
||||||
|
:attributes attrs}]
|
||||||
|
(first (ldap/search conn base-dn params))))
|
||||||
|
|
||||||
|
(defn- retrieve-user
|
||||||
|
[{:keys [conn] :as cfg} {:keys [email password]}]
|
||||||
|
(when-let [{:keys [dn] :as user} (search-user cfg email)]
|
||||||
|
(when (ldap/bind? conn dn password)
|
||||||
|
{:fullname (get user (-> cfg :attrs-fullname keyword))
|
||||||
|
:email email
|
||||||
|
:backend "ldap"})))
|
||||||
|
|
||||||
|
(s/def ::fullname ::us/not-empty-string)
|
||||||
|
(s/def ::email ::us/email)
|
||||||
|
(s/def ::backend ::us/not-empty-string)
|
||||||
|
|
||||||
|
(s/def ::info-data
|
||||||
|
(s/keys :req-un [::fullname ::email ::backend]))
|
||||||
|
|
||||||
|
(defn authenticate
|
||||||
|
[cfg params]
|
||||||
|
(with-open [conn (connect cfg)]
|
||||||
|
(when-let [user (-> (assoc cfg :conn conn)
|
||||||
|
(retrieve-user params))]
|
||||||
|
(when-not (s/valid? ::info-data user)
|
||||||
|
(let [explain (s/explain-str ::info-data user)]
|
||||||
|
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :wrong-ldap-response
|
||||||
|
:explain explain)))
|
||||||
|
user)))
|
||||||
|
|
||||||
|
(defn- try-connectivity
|
||||||
|
[cfg]
|
||||||
|
;; If we have ldap parameters, try to establish connection
|
||||||
|
(when (and (:bind-dn cfg)
|
||||||
|
(:bind-password cfg)
|
||||||
|
(:host cfg)
|
||||||
|
(:port cfg))
|
||||||
|
(try
|
||||||
|
(with-open [_ (connect cfg)]
|
||||||
|
(l/info :hint "provider initialized"
|
||||||
|
:provider "ldap"
|
||||||
|
:host (:host cfg)
|
||||||
|
:port (:port cfg)
|
||||||
|
:tls? (:tls cfg)
|
||||||
|
:ssl? (:ssl cfg)
|
||||||
|
:bind-dn (:bind-dn cfg)
|
||||||
|
:base-dn (:base-dn cfg)
|
||||||
|
:query (:query cfg))
|
||||||
|
cfg)
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/error :hint "unable to connect to LDAP server (LDAP auth provider disabled)"
|
||||||
|
:host (:host cfg) :port (:port cfg) :cause cause)
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
(defn- prepare-attributes
|
||||||
|
[cfg]
|
||||||
|
(assoc cfg :attrs [(:attrs-username cfg)
|
||||||
|
(:attrs-email cfg)
|
||||||
|
(:attrs-fullname cfg)]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::provider
|
||||||
|
[_ cfg]
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(some-> cfg try-connectivity prepare-attributes)))
|
||||||
|
|
||||||
|
(s/def ::enabled? ::us/boolean)
|
||||||
|
(s/def ::host ::cf/ldap-host)
|
||||||
|
(s/def ::port ::cf/ldap-port)
|
||||||
|
(s/def ::ssl ::cf/ldap-ssl)
|
||||||
|
(s/def ::tls ::cf/ldap-starttls)
|
||||||
|
(s/def ::query ::cf/ldap-user-query)
|
||||||
|
(s/def ::base-dn ::cf/ldap-base-dn)
|
||||||
|
(s/def ::bind-dn ::cf/ldap-bind-dn)
|
||||||
|
(s/def ::bind-password ::cf/ldap-bind-password)
|
||||||
|
(s/def ::attrs-email ::cf/ldap-attrs-email)
|
||||||
|
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
|
||||||
|
(s/def ::attrs-username ::cf/ldap-attrs-username)
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::provider
|
||||||
|
[_]
|
||||||
|
(s/keys :opt-un [::host ::port
|
||||||
|
::ssl ::tls
|
||||||
|
::enabled?
|
||||||
|
::bind-dn
|
||||||
|
::bind-password
|
||||||
|
::query
|
||||||
|
::attrs-email
|
||||||
|
::attrs-username
|
||||||
|
::attrs-fullname]))
|
|
@ -4,19 +4,23 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.http.oauth
|
(ns app.auth.oidc
|
||||||
|
"OIDC client implementation."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.http.middleware :as hmw]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.queries.profile :as profile]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[clojure.set :as set]
|
[clojure.set :as set]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -25,6 +29,218 @@
|
||||||
[promesa.exec :as px]
|
[promesa.exec :as px]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; HELPERS
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- obfuscate-string
|
||||||
|
[s]
|
||||||
|
(if (< (count s) 10)
|
||||||
|
(apply str (take (count s) (repeat "*")))
|
||||||
|
(str (subs s 0 5)
|
||||||
|
(apply str (take (- (count s) 5) (repeat "*"))))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; OIDC PROVIDER (GENERIC)
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- discover-oidc-config
|
||||||
|
[{:keys [http-client]} {:keys [base-uri] :as opts}]
|
||||||
|
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
||||||
|
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))]
|
||||||
|
(cond
|
||||||
|
(ex/exception? response)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to discover oidc configuration"
|
||||||
|
:discover-uri (str discovery-uri)
|
||||||
|
:cause response)
|
||||||
|
nil)
|
||||||
|
|
||||||
|
(= 200 (:status response))
|
||||||
|
(let [data (json/read (:body response))]
|
||||||
|
{:token-uri (get data :token_endpoint)
|
||||||
|
:auth-uri (get data :authorization_endpoint)
|
||||||
|
:user-uri (get data :userinfo_endpoint)})
|
||||||
|
|
||||||
|
:else
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to discover OIDC configuration"
|
||||||
|
:uri (str discovery-uri)
|
||||||
|
:response-status-code (:status response))
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
(defn- prepare-oidc-opts
|
||||||
|
[cfg]
|
||||||
|
(let [opts {:base-uri (:base-uri cfg)
|
||||||
|
:client-id (:client-id cfg)
|
||||||
|
:client-secret (:client-secret cfg)
|
||||||
|
:token-uri (:token-uri cfg)
|
||||||
|
:auth-uri (:auth-uri cfg)
|
||||||
|
:user-uri (:user-uri cfg)
|
||||||
|
:scopes (:scopes cfg #{"openid" "profile" "email"})
|
||||||
|
:roles-attr (:roles-attr cfg)
|
||||||
|
:roles (:roles cfg)
|
||||||
|
:name "oidc"}
|
||||||
|
|
||||||
|
opts (d/without-nils opts)]
|
||||||
|
|
||||||
|
(when (and (string? (:base-uri opts))
|
||||||
|
(string? (:client-id opts))
|
||||||
|
(string? (:client-secret opts)))
|
||||||
|
(if (and (string? (:token-uri opts))
|
||||||
|
(string? (:user-uri opts))
|
||||||
|
(string? (:auth-uri opts)))
|
||||||
|
opts
|
||||||
|
(some-> (discover-oidc-config cfg opts)
|
||||||
|
(merge opts {:discover? true}))))))
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::generic-provider
|
||||||
|
[_ cfg]
|
||||||
|
(d/without-nils cfg))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::generic-provider
|
||||||
|
[_ cfg]
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(if-let [opts (prepare-oidc-opts cfg)]
|
||||||
|
(do
|
||||||
|
(l/info :hint "provider initialized"
|
||||||
|
:provider :oidc
|
||||||
|
:method (if (:discover? opts) "discover" "manual")
|
||||||
|
:client-id (:client-id opts)
|
||||||
|
:client-secret (obfuscate-string (:client-secret opts))
|
||||||
|
:scopes (str/join "," (:scopes opts))
|
||||||
|
:auth-uri (:auth-uri opts)
|
||||||
|
:user-uri (:user-uri opts)
|
||||||
|
:token-uri (:token-uri opts)
|
||||||
|
:roles-attr (:roles-attr opts)
|
||||||
|
:roles (:roles opts))
|
||||||
|
opts)
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc)
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; GOOGLE AUTH PROVIDER
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::google-provider
|
||||||
|
[_ cfg]
|
||||||
|
(d/without-nils cfg))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::google-provider
|
||||||
|
[_ cfg]
|
||||||
|
(let [opts {:client-id (:client-id cfg)
|
||||||
|
:client-secret (:client-secret cfg)
|
||||||
|
:scopes #{"openid" "email" "profile"}
|
||||||
|
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
|
:token-uri "https://oauth2.googleapis.com/token"
|
||||||
|
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||||
|
:name "google"}]
|
||||||
|
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(if (and (string? (:client-id opts))
|
||||||
|
(string? (:client-secret opts)))
|
||||||
|
(do
|
||||||
|
(l/info :hint "provider initialized"
|
||||||
|
:provider :google
|
||||||
|
:client-id (:client-id opts)
|
||||||
|
:client-secret (obfuscate-string (:client-secret opts)))
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google)
|
||||||
|
nil)))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; GITHUB AUTH PROVIDER
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- retrieve-github-email
|
||||||
|
[{:keys [http-client]} tdata info]
|
||||||
|
(or (some-> info :email p/resolved)
|
||||||
|
(-> (http-client {:uri "https://api.github.com/user/emails"
|
||||||
|
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
|
||||||
|
:timeout 6000
|
||||||
|
:method :get})
|
||||||
|
(p/then (fn [{:keys [status body] :as response}]
|
||||||
|
(when-not (s/int-in-range? 200 300 status)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :unable-to-retrieve-github-emails
|
||||||
|
:hint "unable to retrieve github emails"
|
||||||
|
:http-status status
|
||||||
|
:http-body body))
|
||||||
|
(->> response :body json/read (filter :primary) first :email))))))
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::github-provider
|
||||||
|
[_ cfg]
|
||||||
|
(d/without-nils cfg))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::github-provider
|
||||||
|
[_ cfg]
|
||||||
|
(let [opts {:client-id (:client-id cfg)
|
||||||
|
:client-secret (:client-secret cfg)
|
||||||
|
:scopes #{"read:user" "user:email"}
|
||||||
|
:auth-uri "https://github.com/login/oauth/authorize"
|
||||||
|
:token-uri "https://github.com/login/oauth/access_token"
|
||||||
|
:user-uri "https://api.github.com/user"
|
||||||
|
:name "github"
|
||||||
|
|
||||||
|
;; Additional hooks for provider specific way of
|
||||||
|
;; retrieve emails.
|
||||||
|
:get-email-fn (partial retrieve-github-email cfg)}]
|
||||||
|
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(if (and (string? (:client-id opts))
|
||||||
|
(string? (:client-secret opts)))
|
||||||
|
(do
|
||||||
|
(l/info :hint "provider initialized"
|
||||||
|
:provider :github
|
||||||
|
:client-id (:client-id opts)
|
||||||
|
:client-secret (obfuscate-string (:client-secret opts)))
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github)
|
||||||
|
nil)))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; GITLAB AUTH PROVIDER
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::gitlab-provider
|
||||||
|
[_ cfg]
|
||||||
|
(d/without-nils cfg))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::gitlab-provider
|
||||||
|
[_ cfg]
|
||||||
|
(let [base (:base-uri cfg "https://gitlab.com")
|
||||||
|
opts {:base-uri base
|
||||||
|
:client-id (:client-id cfg)
|
||||||
|
:client-secret (:client-secret cfg)
|
||||||
|
:scopes #{"openid" "profile" "email"}
|
||||||
|
:auth-uri (str base "/oauth/authorize")
|
||||||
|
:token-uri (str base "/oauth/token")
|
||||||
|
:user-uri (str base "/oauth/userinfo")
|
||||||
|
:name "gitlab"}]
|
||||||
|
(when (:enabled? cfg)
|
||||||
|
(if (and (string? (:client-id opts))
|
||||||
|
(string? (:client-secret opts)))
|
||||||
|
(do
|
||||||
|
(l/info :hint "provider initialized"
|
||||||
|
:provider :gitlab
|
||||||
|
:base-uri base
|
||||||
|
:client-id (:client-id opts)
|
||||||
|
:client-secret (obfuscate-string (:client-secret opts)))
|
||||||
|
opts)
|
||||||
|
|
||||||
|
(do
|
||||||
|
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab)
|
||||||
|
nil)))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; HANDLERS
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[{:keys [provider] :as cfg}]
|
[{:keys [provider] :as cfg}]
|
||||||
(let [public (u/uri (:public-uri cfg))]
|
(let [public (u/uri (:public-uri cfg))]
|
||||||
|
@ -81,42 +297,30 @@
|
||||||
:timeout 6000
|
:timeout 6000
|
||||||
:method :get}))
|
:method :get}))
|
||||||
|
|
||||||
(retrieve-emails []
|
(validate-response [response]
|
||||||
(if (some? (:emails-uri provider))
|
(when-not (s/int-in-range? 200 300 (:status response))
|
||||||
(http-client {:uri (:emails-uri provider)
|
|
||||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
|
||||||
:timeout 6000
|
|
||||||
:method :get})
|
|
||||||
(p/resolved {:status 200})))
|
|
||||||
|
|
||||||
(validate-response [[retrieve-res emails-res]]
|
|
||||||
(when-not (s/int-in-range? 200 300 (:status retrieve-res))
|
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
:code :unable-to-retrieve-user-info
|
:code :unable-to-retrieve-user-info
|
||||||
:hint "unable to retrieve user info"
|
:hint "unable to retrieve user info"
|
||||||
:http-status (:status retrieve-res)
|
:http-status (:status response)
|
||||||
:http-body (:body retrieve-res)))
|
:http-body (:body response)))
|
||||||
(when-not (s/int-in-range? 200 300 (:status emails-res))
|
response)
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :unable-to-retrieve-user-info
|
|
||||||
:hint "unable to retrieve user info"
|
|
||||||
:http-status (:status emails-res)
|
|
||||||
:http-body (:body emails-res)))
|
|
||||||
[retrieve-res emails-res])
|
|
||||||
|
|
||||||
(get-email [info]
|
(get-email [info]
|
||||||
|
;; Allow providers hook into this for custom email
|
||||||
|
;; retrieval method.
|
||||||
|
(if-let [get-email-fn (:get-email-fn provider)]
|
||||||
|
(get-email-fn tdata info)
|
||||||
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
||||||
(get info attr-kw)))
|
(get info attr-kw))))
|
||||||
|
|
||||||
(get-name [info]
|
(get-name [info]
|
||||||
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
||||||
(get info attr-kw)))
|
(get info attr-kw)))
|
||||||
|
|
||||||
(process-response [[retrieve-res emails-res]]
|
(process-response [response]
|
||||||
(let [info (json/read (:body retrieve-res))
|
(p/let [info (-> response :body json/read)
|
||||||
email (if (some? (:extract-email-callback provider))
|
email (get-email info)]
|
||||||
((:extract-email-callback provider) emails-res)
|
|
||||||
(get-email info))]
|
|
||||||
{:backend (:name provider)
|
{:backend (:name provider)
|
||||||
:email email
|
:email email
|
||||||
:fullname (or (get-name info) email)
|
:fullname (or (get-name info) email)
|
||||||
|
@ -133,10 +337,10 @@
|
||||||
:info info))
|
:info info))
|
||||||
info)]
|
info)]
|
||||||
|
|
||||||
(-> (p/all [(retrieve) (retrieve-emails)])
|
(-> (retrieve)
|
||||||
(p/then' validate-response)
|
(p/then validate-response)
|
||||||
(p/then' process-response)
|
(p/then process-response)
|
||||||
(p/then' validate-info))))
|
(p/then validate-info))))
|
||||||
|
|
||||||
(s/def ::backend ::us/not-empty-string)
|
(s/def ::backend ::us/not-empty-string)
|
||||||
(s/def ::email ::us/not-empty-string)
|
(s/def ::email ::us/not-empty-string)
|
||||||
|
@ -195,8 +399,6 @@
|
||||||
(p/then' validate-oidc)
|
(p/then' validate-oidc)
|
||||||
(p/then' (partial post-process state))))))
|
(p/then' (partial post-process state))))))
|
||||||
|
|
||||||
;; --- HTTP HANDLERS
|
|
||||||
|
|
||||||
(defn- retrieve-profile
|
(defn- retrieve-profile
|
||||||
[{:keys [pool executor] :as cfg} info]
|
[{:keys [pool executor] :as cfg} info]
|
||||||
(px/with-dispatch executor
|
(px/with-dispatch executor
|
||||||
|
@ -256,8 +458,7 @@
|
||||||
(redirect-response uri))))
|
(redirect-response uri))))
|
||||||
|
|
||||||
(defn- auth-handler
|
(defn- auth-handler
|
||||||
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
|
[{:keys [tokens] :as cfg} {:keys [params] :as request}]
|
||||||
(try
|
|
||||||
(let [props (audit/extract-utm-params params)
|
(let [props (audit/extract-utm-params params)
|
||||||
state (tokens :generate
|
state (tokens :generate
|
||||||
{:iss :oauth
|
{:iss :oauth
|
||||||
|
@ -265,12 +466,10 @@
|
||||||
:props props
|
:props props
|
||||||
:exp (dt/in-future "15m")})
|
:exp (dt/in-future "15m")})
|
||||||
uri (build-auth-uri cfg state)]
|
uri (build-auth-uri cfg state)]
|
||||||
(respond (yrs/response 200 {:redirect-uri uri})))
|
(yrs/response 200 {:redirect-uri uri})))
|
||||||
(catch Throwable cause
|
|
||||||
(raise cause))))
|
|
||||||
|
|
||||||
(defn- callback-handler
|
(defn- callback-handler
|
||||||
[cfg request respond _]
|
[cfg request]
|
||||||
(letfn [(process-request []
|
(letfn [(process-request []
|
||||||
(p/let [info (retrieve-info cfg request)
|
(p/let [info (retrieve-info cfg request)
|
||||||
profile (retrieve-profile cfg info)]
|
profile (retrieve-profile cfg info)]
|
||||||
|
@ -278,182 +477,62 @@
|
||||||
|
|
||||||
(handle-error [cause]
|
(handle-error [cause]
|
||||||
(l/error :hint "error on oauth process" :cause cause)
|
(l/error :hint "error on oauth process" :cause cause)
|
||||||
(respond (generate-error-redirect cfg cause)))]
|
(generate-error-redirect cfg cause))]
|
||||||
|
|
||||||
(-> (process-request)
|
(-> (process-request)
|
||||||
(p/then respond)
|
|
||||||
(p/catch handle-error))))
|
(p/catch handle-error))))
|
||||||
|
|
||||||
;; --- INIT
|
(def provider-lookup
|
||||||
|
{:compile
|
||||||
(declare initialize)
|
(fn [& _]
|
||||||
|
(fn [handler]
|
||||||
|
(fn [{:keys [providers] :as cfg} request]
|
||||||
|
(let [provider (some-> request :path-params :provider keyword)]
|
||||||
|
(if-let [provider (get providers provider)]
|
||||||
|
(handler (assoc cfg :provider provider) request)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :provider-not-configured
|
||||||
|
:provider provider
|
||||||
|
:hint "provider not configured"))))))})
|
||||||
|
|
||||||
(s/def ::public-uri ::us/not-empty-string)
|
(s/def ::public-uri ::us/not-empty-string)
|
||||||
|
(s/def ::http-client fn?)
|
||||||
(s/def ::session map?)
|
(s/def ::session map?)
|
||||||
(s/def ::tokens fn?)
|
(s/def ::tokens fn?)
|
||||||
(s/def ::rpc map?)
|
(s/def ::providers map?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
(defmethod ig/pre-init-spec ::routes
|
||||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
|
[_]
|
||||||
|
(s/keys :req-un [::public-uri
|
||||||
|
::session
|
||||||
|
::tokens
|
||||||
|
::http-client
|
||||||
|
::providers
|
||||||
|
::db/pool
|
||||||
|
::wrk/executor]))
|
||||||
|
|
||||||
(defn wrap-handler
|
(defmethod ig/init-key ::routes
|
||||||
[cfg handler]
|
[_ {:keys [executor session] :as cfg}]
|
||||||
(fn [request respond raise]
|
(let [cfg (update cfg :provider d/without-nils)]
|
||||||
(let [provider (get-in request [:path-params :provider])
|
["" {:middleware [[(:middleware session)]
|
||||||
provider (get-in @cfg [:providers provider])]
|
[hmw/with-promise-async executor]
|
||||||
(if provider
|
[hmw/with-config cfg]
|
||||||
(handler (assoc @cfg :provider provider)
|
[provider-lookup]
|
||||||
request
|
]}
|
||||||
respond
|
;; We maintain the both URI prefixes for backward compatibility.
|
||||||
raise)
|
|
||||||
(raise
|
|
||||||
(ex/error
|
|
||||||
:type :not-found
|
|
||||||
:provider provider
|
|
||||||
:hint "provider not configured"))))))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
["/auth/oauth"
|
||||||
[_ cfg]
|
["/:provider"
|
||||||
(let [cfg (initialize cfg)]
|
{:handler auth-handler
|
||||||
{:handler (wrap-handler cfg auth-handler)
|
:allowed-methods #{:post}}]
|
||||||
:callback-handler (wrap-handler cfg callback-handler)}))
|
["/:provider/callback"
|
||||||
|
{:handler callback-handler
|
||||||
|
:allowed-methods #{:get}}]]
|
||||||
|
|
||||||
(defn- discover-oidc-config
|
["/auth/oidc"
|
||||||
[{:keys [http-client]} {:keys [base-uri] :as opts}]
|
["/:provider"
|
||||||
|
{:handler auth-handler
|
||||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
:allowed-methods #{:post}}]
|
||||||
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))]
|
["/:provider/callback"
|
||||||
(cond
|
{:handler callback-handler
|
||||||
(ex/exception? response)
|
:allowed-methods #{:get}}]]]))
|
||||||
(do
|
|
||||||
(l/warn :hint "unable to discover oidc configuration"
|
|
||||||
:discover-uri (str discovery-uri)
|
|
||||||
:cause response)
|
|
||||||
nil)
|
|
||||||
|
|
||||||
(= 200 (:status response))
|
|
||||||
(let [data (json/read (:body response))]
|
|
||||||
{:token-uri (get data :token_endpoint)
|
|
||||||
:auth-uri (get data :authorization_endpoint)
|
|
||||||
:user-uri (get data :userinfo_endpoint)})
|
|
||||||
|
|
||||||
:else
|
|
||||||
(do
|
|
||||||
(l/warn :hint "unable to discover OIDC configuration"
|
|
||||||
:uri (str discovery-uri)
|
|
||||||
:response-status-code (:status response))
|
|
||||||
nil))))
|
|
||||||
|
|
||||||
(defn- obfuscate-string
|
|
||||||
[s]
|
|
||||||
(if (< (count s) 10)
|
|
||||||
(apply str (take (count s) (repeat "*")))
|
|
||||||
(str (subs s 0 5)
|
|
||||||
(apply str (take (- (count s) 5) (repeat "*"))))))
|
|
||||||
|
|
||||||
(defn- initialize-oidc-provider
|
|
||||||
[cfg]
|
|
||||||
(let [opts {:base-uri (cf/get :oidc-base-uri)
|
|
||||||
:client-id (cf/get :oidc-client-id)
|
|
||||||
:client-secret (cf/get :oidc-client-secret)
|
|
||||||
:token-uri (cf/get :oidc-token-uri)
|
|
||||||
:auth-uri (cf/get :oidc-auth-uri)
|
|
||||||
:user-uri (cf/get :oidc-user-uri)
|
|
||||||
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
|
|
||||||
:roles-attr (cf/get :oidc-roles-attr)
|
|
||||||
:roles (cf/get :oidc-roles)
|
|
||||||
:name "oidc"}]
|
|
||||||
|
|
||||||
(if (and (string? (:base-uri opts))
|
|
||||||
(string? (:client-id opts))
|
|
||||||
(string? (:client-secret opts)))
|
|
||||||
(do
|
|
||||||
(l/debug :hint "initialize oidc provider" :name "generic-oidc"
|
|
||||||
:opts (update opts :client-secret obfuscate-string))
|
|
||||||
(if (and (string? (:token-uri opts))
|
|
||||||
(string? (:user-uri opts))
|
|
||||||
(string? (:auth-uri opts)))
|
|
||||||
(do
|
|
||||||
(l/debug :hint "initialized with user provided configuration")
|
|
||||||
(assoc-in cfg [:providers "oidc"] opts))
|
|
||||||
(do
|
|
||||||
(l/debug :hint "trying to discover oidc provider configuration using BASE_URI")
|
|
||||||
(if-let [opts' (discover-oidc-config cfg opts)]
|
|
||||||
(do
|
|
||||||
(l/debug :hint "discovered opts" :additional-opts opts')
|
|
||||||
(assoc-in cfg [:providers "oidc"] (merge opts opts')))
|
|
||||||
|
|
||||||
cfg))))
|
|
||||||
cfg)))
|
|
||||||
|
|
||||||
(defn- initialize-google-provider
|
|
||||||
[cfg]
|
|
||||||
(let [opts {:client-id (cf/get :google-client-id)
|
|
||||||
:client-secret (cf/get :google-client-secret)
|
|
||||||
:scopes #{"openid" "email" "profile"}
|
|
||||||
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
|
|
||||||
:token-uri "https://oauth2.googleapis.com/token"
|
|
||||||
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
|
|
||||||
:name "google"}]
|
|
||||||
(if (and (string? (:client-id opts))
|
|
||||||
(string? (:client-secret opts)))
|
|
||||||
(do
|
|
||||||
(l/info :action "initialize" :provider "google"
|
|
||||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
|
||||||
(assoc-in cfg [:providers "google"] opts))
|
|
||||||
cfg)))
|
|
||||||
|
|
||||||
(defn extract-github-email
|
|
||||||
[response]
|
|
||||||
(let [emails (json/read (:body response))
|
|
||||||
primary-email (->> emails
|
|
||||||
(filter #(:primary %))
|
|
||||||
first)]
|
|
||||||
(:email primary-email)))
|
|
||||||
|
|
||||||
(defn- initialize-github-provider
|
|
||||||
[cfg]
|
|
||||||
(let [opts {:client-id (cf/get :github-client-id)
|
|
||||||
:client-secret (cf/get :github-client-secret)
|
|
||||||
:scopes #{"read:user" "user:email"}
|
|
||||||
:auth-uri "https://github.com/login/oauth/authorize"
|
|
||||||
:token-uri "https://github.com/login/oauth/access_token"
|
|
||||||
:emails-uri "https://api.github.com/user/emails"
|
|
||||||
:extract-email-callback extract-github-email
|
|
||||||
:user-uri "https://api.github.com/user"
|
|
||||||
:name "github"}]
|
|
||||||
(if (and (string? (:client-id opts))
|
|
||||||
(string? (:client-secret opts)))
|
|
||||||
(do
|
|
||||||
(l/info :action "initialize" :provider "github"
|
|
||||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
|
||||||
(assoc-in cfg [:providers "github"] opts))
|
|
||||||
cfg)))
|
|
||||||
|
|
||||||
(defn- initialize-gitlab-provider
|
|
||||||
[cfg]
|
|
||||||
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
|
|
||||||
opts {:base-uri base
|
|
||||||
:client-id (cf/get :gitlab-client-id)
|
|
||||||
:client-secret (cf/get :gitlab-client-secret)
|
|
||||||
:scopes #{"openid" "profile" "email"}
|
|
||||||
:auth-uri (str base "/oauth/authorize")
|
|
||||||
:token-uri (str base "/oauth/token")
|
|
||||||
:user-uri (str base "/oauth/userinfo")
|
|
||||||
:name "gitlab"}]
|
|
||||||
(if (and (string? (:client-id opts))
|
|
||||||
(string? (:client-secret opts)))
|
|
||||||
(do
|
|
||||||
(l/info :action "initialize" :provider "gitlab"
|
|
||||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
|
||||||
(assoc-in cfg [:providers "gitlab"] opts))
|
|
||||||
cfg)))
|
|
||||||
|
|
||||||
(defn- initialize
|
|
||||||
[cfg]
|
|
||||||
(let [cfg (agent cfg :error-mode :continue)]
|
|
||||||
(send-off cfg initialize-google-provider)
|
|
||||||
(send-off cfg initialize-gitlab-provider)
|
|
||||||
(send-off cfg initialize-github-provider)
|
|
||||||
(send-off cfg initialize-oidc-provider)
|
|
||||||
cfg))
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.main :as main]
|
[app.main :as main]
|
||||||
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
|
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
|
@ -54,13 +55,13 @@
|
||||||
:type :password}))]
|
:type :password}))]
|
||||||
(try
|
(try
|
||||||
(db/with-atomic [conn (:app.db/pool system)]
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
(->> (profile/create-profile conn
|
(->> (cmd.auth/create-profile conn
|
||||||
{:fullname fullname
|
{:fullname fullname
|
||||||
:email email
|
:email email
|
||||||
:password password
|
:password password
|
||||||
:is-active true
|
:is-active true
|
||||||
:is-demo false})
|
:is-demo false})
|
||||||
(profile/create-profile-relations conn)))
|
(cmd.auth/create-profile-relations conn)))
|
||||||
|
|
||||||
(when (pos? (:verbosity options))
|
(when (pos? (:verbosity options))
|
||||||
(println "User created successfully."))
|
(println "User created successfully."))
|
||||||
|
|
|
@ -79,7 +79,6 @@
|
||||||
:ldap-attrs-username "uid"
|
:ldap-attrs-username "uid"
|
||||||
:ldap-attrs-email "mail"
|
:ldap-attrs-email "mail"
|
||||||
:ldap-attrs-fullname "cn"
|
:ldap-attrs-fullname "cn"
|
||||||
:ldap-attrs-photo "jpegPhoto"
|
|
||||||
|
|
||||||
;; a server prop key where initial project is stored.
|
;; a server prop key where initial project is stored.
|
||||||
:initial-project-skey "initial-project"})
|
:initial-project-skey "initial-project"})
|
||||||
|
@ -149,7 +148,6 @@
|
||||||
(s/def ::initial-project-skey ::us/string)
|
(s/def ::initial-project-skey ::us/string)
|
||||||
(s/def ::ldap-attrs-email ::us/string)
|
(s/def ::ldap-attrs-email ::us/string)
|
||||||
(s/def ::ldap-attrs-fullname ::us/string)
|
(s/def ::ldap-attrs-fullname ::us/string)
|
||||||
(s/def ::ldap-attrs-photo ::us/string)
|
|
||||||
(s/def ::ldap-attrs-username ::us/string)
|
(s/def ::ldap-attrs-username ::us/string)
|
||||||
(s/def ::ldap-base-dn ::us/string)
|
(s/def ::ldap-base-dn ::us/string)
|
||||||
(s/def ::ldap-bind-dn ::us/string)
|
(s/def ::ldap-bind-dn ::us/string)
|
||||||
|
@ -256,7 +254,6 @@
|
||||||
::initial-project-skey
|
::initial-project-skey
|
||||||
::ldap-attrs-email
|
::ldap-attrs-email
|
||||||
::ldap-attrs-fullname
|
::ldap-attrs-fullname
|
||||||
::ldap-attrs-photo
|
|
||||||
::ldap-attrs-username
|
::ldap-attrs-username
|
||||||
::ldap-base-dn
|
::ldap-base-dn
|
||||||
::ldap-bind-dn
|
::ldap-bind-dn
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
[app.http.doc :as doc]
|
|
||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.middleware :as middleware]
|
[app.http.middleware :as middleware]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
|
@ -67,8 +66,10 @@
|
||||||
:xnio/worker-threads (:worker-threads cfg)
|
:xnio/worker-threads (:worker-threads cfg)
|
||||||
:xnio/dispatch (:executor cfg)
|
:xnio/dispatch (:executor cfg)
|
||||||
:ring/async true}
|
:ring/async true}
|
||||||
|
|
||||||
handler (if (some? router)
|
handler (if (some? router)
|
||||||
(wrap-router router)
|
(wrap-router router)
|
||||||
|
|
||||||
handler)
|
handler)
|
||||||
server (yt/server handler (d/without-nils options))]
|
server (yt/server handler (d/without-nils options))]
|
||||||
(assoc cfg :server (yt/start! server))))
|
(assoc cfg :server (yt/start! server))))
|
||||||
|
@ -113,7 +114,6 @@
|
||||||
;; HTTP ROUTER
|
;; HTTP ROUTER
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(s/def ::rpc map?)
|
|
||||||
(s/def ::oauth map?)
|
(s/def ::oauth map?)
|
||||||
(s/def ::storage map?)
|
(s/def ::storage map?)
|
||||||
(s/def ::assets map?)
|
(s/def ::assets map?)
|
||||||
|
@ -122,15 +122,27 @@
|
||||||
(s/def ::audit-handler fn?)
|
(s/def ::audit-handler fn?)
|
||||||
(s/def ::awsns-handler fn?)
|
(s/def ::awsns-handler fn?)
|
||||||
(s/def ::session map?)
|
(s/def ::session map?)
|
||||||
(s/def ::debug-routes vector?)
|
(s/def ::rpc-routes (s/nilable vector?))
|
||||||
|
(s/def ::debug-routes (s/nilable vector?))
|
||||||
|
(s/def ::oidc-routes (s/nilable vector?))
|
||||||
|
(s/def ::doc-routes (s/nilable vector?))
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::router [_]
|
(defmethod ig/pre-init-spec ::router [_]
|
||||||
(s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets
|
(s/keys :req-un [::mtx/metrics
|
||||||
::session ::feedback ::awsns-handler ::debug-routes
|
::ws
|
||||||
::audit-handler]))
|
::storage
|
||||||
|
::assets
|
||||||
|
::session
|
||||||
|
::feedback
|
||||||
|
::awsns-handler
|
||||||
|
::debug-routes
|
||||||
|
::oidc-routes
|
||||||
|
::audit-handler
|
||||||
|
::rpc-routes
|
||||||
|
::doc-routes]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::router
|
(defmethod ig/init-key ::router
|
||||||
[_ {:keys [ws session rpc oauth metrics assets feedback debug-routes] :as cfg}]
|
[_ {:keys [ws session metrics assets feedback] :as cfg}]
|
||||||
(rr/router
|
(rr/router
|
||||||
[["" {:middleware [[middleware/server-timing]
|
[["" {:middleware [[middleware/server-timing]
|
||||||
[middleware/format-response]
|
[middleware/format-response]
|
||||||
|
@ -145,7 +157,7 @@
|
||||||
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
|
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
|
||||||
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
|
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
|
||||||
|
|
||||||
debug-routes
|
(:debug-routes cfg)
|
||||||
|
|
||||||
["/webhooks"
|
["/webhooks"
|
||||||
["/sns" {:handler (:awsns-handler cfg)
|
["/sns" {:handler (:awsns-handler cfg)
|
||||||
|
@ -156,22 +168,12 @@
|
||||||
:allowed-methods #{:get}}]
|
:allowed-methods #{:get}}]
|
||||||
|
|
||||||
["/api" {:middleware [[middleware/cors]
|
["/api" {:middleware [[middleware/cors]
|
||||||
(:middleware session)]}
|
[(:middleware session)]]}
|
||||||
["/_doc" {:handler (doc/handler rpc)
|
|
||||||
:allowed-methods #{:get}}]
|
|
||||||
["/feedback" {:handler feedback
|
|
||||||
:allowed-methods #{:post}}]
|
|
||||||
|
|
||||||
["/auth/oauth/:provider" {:handler (:handler oauth)
|
|
||||||
:allowed-methods #{:post}}]
|
|
||||||
["/auth/oauth/:provider/callback" {:handler (:callback-handler oauth)
|
|
||||||
:allowed-methods #{:get}}]
|
|
||||||
|
|
||||||
["/audit/events" {:handler (:audit-handler cfg)
|
["/audit/events" {:handler (:audit-handler cfg)
|
||||||
:allowed-methods #{:post}}]
|
:allowed-methods #{:post}}]
|
||||||
|
["/feedback" {:handler feedback
|
||||||
|
:allowed-methods #{:post}}]
|
||||||
|
(:doc-routes cfg)
|
||||||
|
(:oidc-routes cfg)
|
||||||
|
(:rpc-routes cfg)]]]))
|
||||||
|
|
||||||
["/rpc"
|
|
||||||
["/command/:command" {:handler (:command-handler rpc)}]
|
|
||||||
["/query/:type" {:handler (:query-handler rpc)}]
|
|
||||||
["/mutation/:type" {:handler (:mutation-handler rpc)
|
|
||||||
:allowed-methods #{:post}}]]]]]))
|
|
||||||
|
|
|
@ -9,14 +9,16 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.template :as tmpl]
|
[app.util.template :as tmpl]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]
|
||||||
[pretty-spec.core :as ps]
|
[pretty-spec.core :as ps]
|
||||||
[yetti.response :as yrs]))
|
[yetti.response :as yrs]))
|
||||||
|
|
||||||
(defn get-spec-str
|
(defn- get-spec-str
|
||||||
[k]
|
[k]
|
||||||
(with-out-str
|
(with-out-str
|
||||||
(ps/pprint (s/form k)
|
(ps/pprint (s/form k)
|
||||||
|
@ -24,8 +26,8 @@
|
||||||
"clojure.core.specs.alpha" "score"
|
"clojure.core.specs.alpha" "score"
|
||||||
"clojure.core" nil}})))
|
"clojure.core" nil}})))
|
||||||
|
|
||||||
(defn prepare-context
|
(defn- prepare-context
|
||||||
[rpc]
|
[methods]
|
||||||
(letfn [(gen-doc [type [name f]]
|
(letfn [(gen-doc [type [name f]]
|
||||||
(let [mdata (meta f)]
|
(let [mdata (meta f)]
|
||||||
;; (prn name mdata)
|
;; (prn name mdata)
|
||||||
|
@ -38,22 +40,32 @@
|
||||||
{:command-methods
|
{:command-methods
|
||||||
(into []
|
(into []
|
||||||
(map (partial gen-doc :command))
|
(map (partial gen-doc :command))
|
||||||
(->> rpc :methods :command (sort-by first)))
|
(->> methods :commands (sort-by first)))
|
||||||
:query-methods
|
:query-methods
|
||||||
(into []
|
(into []
|
||||||
(map (partial gen-doc :query))
|
(map (partial gen-doc :query))
|
||||||
(->> rpc :methods :query (sort-by first)))
|
(->> methods :queries (sort-by first)))
|
||||||
:mutation-methods
|
:mutation-methods
|
||||||
(into []
|
(into []
|
||||||
(map (partial gen-doc :mutation))
|
(map (partial gen-doc :mutation))
|
||||||
(->> rpc :methods :mutation (sort-by first)))}))
|
(->> methods :mutations (sort-by first)))}))
|
||||||
|
|
||||||
(defn handler
|
(defn- handler
|
||||||
[rpc]
|
[methods]
|
||||||
(let [context (prepare-context rpc)]
|
|
||||||
(if (contains? cf/flags :backend-api-doc)
|
(if (contains? cf/flags :backend-api-doc)
|
||||||
|
(let [context (prepare-context methods)]
|
||||||
(fn [_ respond _]
|
(fn [_ respond _]
|
||||||
(respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
|
(respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
|
||||||
(tmpl/render context)))))
|
(tmpl/render context))))))
|
||||||
(fn [_ respond _]
|
(fn [_ respond _]
|
||||||
(respond (yrs/response 404))))))
|
(respond (yrs/response 404)))))
|
||||||
|
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
|
(s/keys :req-un [::rpc/methods]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::routes
|
||||||
|
[_ {:keys [methods] :as cfg}]
|
||||||
|
["/_doc" {:handler (handler methods)
|
||||||
|
:allowed-methods #{:get}}])
|
||||||
|
|
||||||
|
|
|
@ -143,13 +143,11 @@
|
||||||
|
|
||||||
(defn handle
|
(defn handle
|
||||||
[cause request]
|
[cause request]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(or (instance? java.util.concurrent.CompletionException cause)
|
(or (instance? java.util.concurrent.CompletionException cause)
|
||||||
(instance? java.util.concurrent.ExecutionException cause))
|
(instance? java.util.concurrent.ExecutionException cause))
|
||||||
(handle-exception (.getCause ^Throwable cause) request)
|
(handle-exception (.getCause ^Throwable cause) request)
|
||||||
|
|
||||||
|
|
||||||
(ex/wrapped? cause)
|
(ex/wrapped? cause)
|
||||||
(let [context (meta cause)
|
(let [context (meta cause)
|
||||||
cause (deref cause)]
|
cause (deref cause)]
|
||||||
|
|
|
@ -201,6 +201,7 @@
|
||||||
(fn [handler executor]
|
(fn [handler executor]
|
||||||
(fn [request respond raise]
|
(fn [request respond raise]
|
||||||
(-> (px/submit! executor #(handler request))
|
(-> (px/submit! executor #(handler request))
|
||||||
|
(p/bind p/wrap)
|
||||||
(p/then respond)
|
(p/then respond)
|
||||||
(p/catch raise)))))})
|
(p/catch raise)))))})
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.main
|
(ns app.main
|
||||||
(:require
|
(:require
|
||||||
|
[app.auth.oidc]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
@ -90,6 +91,9 @@
|
||||||
:app.http/session
|
:app.http/session
|
||||||
{:store (ig/ref :app.http.session/store)}
|
{:store (ig/ref :app.http.session/store)}
|
||||||
|
|
||||||
|
:app.http.doc/routes
|
||||||
|
{:methods (ig/ref :app.rpc/methods)}
|
||||||
|
|
||||||
:app.http.session/store
|
:app.http.session/store
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
|
@ -123,20 +127,81 @@
|
||||||
:max-body-size (cf/get :http-server-max-body-size)
|
:max-body-size (cf/get :http-server-max-body-size)
|
||||||
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
|
||||||
|
|
||||||
|
:app.auth.ldap/provider
|
||||||
|
{:host (cf/get :ldap-host)
|
||||||
|
:port (cf/get :ldap-port)
|
||||||
|
:ssl (cf/get :ldap-ssl)
|
||||||
|
:tls (cf/get :ldap-starttls)
|
||||||
|
:query (cf/get :ldap-user-query)
|
||||||
|
:attrs-email (cf/get :ldap-attrs-email)
|
||||||
|
:attrs-fullname (cf/get :ldap-attrs-fullname)
|
||||||
|
:attrs-username (cf/get :ldap-attrs-username)
|
||||||
|
:base-dn (cf/get :ldap-base-dn)
|
||||||
|
:bind-dn (cf/get :ldap-bind-dn)
|
||||||
|
:bind-password (cf/get :ldap-bind-password)
|
||||||
|
:enabled? (contains? cf/flags :login-with-ldap)}
|
||||||
|
|
||||||
|
:app.auth.oidc/google-provider
|
||||||
|
{:enabled? (contains? cf/flags :login-with-google)
|
||||||
|
:client-id (cf/get :google-client-id)
|
||||||
|
:client-secret (cf/get :google-client-secret)}
|
||||||
|
|
||||||
|
:app.auth.oidc/github-provider
|
||||||
|
{:enabled? (contains? cf/flags :login-with-github)
|
||||||
|
:http-client (ig/ref :app.http/client)
|
||||||
|
:client-id (cf/get :github-client-id)
|
||||||
|
:client-secret (cf/get :github-client-secret)}
|
||||||
|
|
||||||
|
:app.auth.oidc/gitlab-provider
|
||||||
|
{:enabled? (contains? cf/flags :login-with-gitlab)
|
||||||
|
:base-uri (cf/get :gitlab-base-uri "https://gitlab.com")
|
||||||
|
:client-id (cf/get :gitlab-client-id)
|
||||||
|
:client-secret (cf/get :gitlab-client-secret)}
|
||||||
|
|
||||||
|
:app.auth.oidc/generic-provider
|
||||||
|
{:enabled? (contains? cf/flags :login-with-oidc)
|
||||||
|
:http-client (ig/ref :app.http/client)
|
||||||
|
|
||||||
|
:client-id (cf/get :oidc-client-id)
|
||||||
|
:client-secret (cf/get :oidc-client-secret)
|
||||||
|
|
||||||
|
:base-uri (cf/get :oidc-base-uri)
|
||||||
|
|
||||||
|
:token-uri (cf/get :oidc-token-uri)
|
||||||
|
:auth-uri (cf/get :oidc-auth-uri)
|
||||||
|
:user-uri (cf/get :oidc-user-uri)
|
||||||
|
|
||||||
|
:scopes (cf/get :oidc-scopes)
|
||||||
|
:roles-attr (cf/get :oidc-roles-attr)
|
||||||
|
:roles (cf/get :oidc-roles)}
|
||||||
|
|
||||||
|
:app.auth.oidc/routes
|
||||||
|
{:providers {:google (ig/ref :app.auth.oidc/google-provider)
|
||||||
|
:github (ig/ref :app.auth.oidc/github-provider)
|
||||||
|
:gitlab (ig/ref :app.auth.oidc/gitlab-provider)
|
||||||
|
:oidc (ig/ref :app.auth.oidc/generic-provider)}
|
||||||
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
|
:http-client (ig/ref :app.http/client)
|
||||||
|
:pool (ig/ref :app.db/pool)
|
||||||
|
:session (ig/ref :app.http/session)
|
||||||
|
:public-uri (cf/get :public-uri)
|
||||||
|
:executor (ig/ref [::default :app.worker/executor])}
|
||||||
|
|
||||||
:app.http/router
|
:app.http/router
|
||||||
{:assets (ig/ref :app.http.assets/handlers)
|
{:assets (ig/ref :app.http.assets/handlers)
|
||||||
:feedback (ig/ref :app.http.feedback/handler)
|
:feedback (ig/ref :app.http.feedback/handler)
|
||||||
:session (ig/ref :app.http/session)
|
:session (ig/ref :app.http/session)
|
||||||
:awsns-handler (ig/ref :app.http.awsns/handler)
|
:awsns-handler (ig/ref :app.http.awsns/handler)
|
||||||
:oauth (ig/ref :app.http.oauth/handler)
|
|
||||||
:debug-routes (ig/ref :app.http.debug/routes)
|
:debug-routes (ig/ref :app.http.debug/routes)
|
||||||
|
:oidc-routes (ig/ref :app.auth.oidc/routes)
|
||||||
:ws (ig/ref :app.http.websocket/handler)
|
:ws (ig/ref :app.http.websocket/handler)
|
||||||
:metrics (ig/ref :app.metrics/metrics)
|
:metrics (ig/ref :app.metrics/metrics)
|
||||||
:public-uri (cf/get :public-uri)
|
:public-uri (cf/get :public-uri)
|
||||||
:storage (ig/ref :app.storage/storage)
|
:storage (ig/ref :app.storage/storage)
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
:audit-handler (ig/ref :app.loggers.audit/http-handler)
|
:audit-handler (ig/ref :app.loggers.audit/http-handler)
|
||||||
:rpc (ig/ref :app.rpc/rpc)
|
:rpc-routes (ig/ref :app.rpc/routes)
|
||||||
|
:doc-routes (ig/ref :app.http.doc/routes)
|
||||||
:executor (ig/ref [::default :app.worker/executor])}
|
:executor (ig/ref [::default :app.worker/executor])}
|
||||||
|
|
||||||
:app.http.debug/routes
|
:app.http.debug/routes
|
||||||
|
@ -162,17 +227,7 @@
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:executor (ig/ref [::default :app.worker/executor])}
|
:executor (ig/ref [::default :app.worker/executor])}
|
||||||
|
|
||||||
:app.http.oauth/handler
|
:app.rpc/methods
|
||||||
{:rpc (ig/ref :app.rpc/rpc)
|
|
||||||
:session (ig/ref :app.http/session)
|
|
||||||
:pool (ig/ref :app.db/pool)
|
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
|
||||||
:audit (ig/ref :app.loggers.audit/collector)
|
|
||||||
:executor (ig/ref [::default :app.worker/executor])
|
|
||||||
:http-client (ig/ref :app.http/client)
|
|
||||||
:public-uri (cf/get :public-uri)}
|
|
||||||
|
|
||||||
:app.rpc/rpc
|
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
:session (ig/ref :app.http/session)
|
:session (ig/ref :app.http/session)
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
|
@ -181,9 +236,13 @@
|
||||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
:msgbus (ig/ref :app.msgbus/msgbus)
|
||||||
:public-uri (cf/get :public-uri)
|
:public-uri (cf/get :public-uri)
|
||||||
:audit (ig/ref :app.loggers.audit/collector)
|
:audit (ig/ref :app.loggers.audit/collector)
|
||||||
|
:ldap (ig/ref :app.auth.ldap/provider)
|
||||||
:http-client (ig/ref :app.http/client)
|
:http-client (ig/ref :app.http/client)
|
||||||
:executors (ig/ref :app.worker/executors)}
|
:executors (ig/ref :app.worker/executors)}
|
||||||
|
|
||||||
|
:app.rpc/routes
|
||||||
|
{:methods (ig/ref :app.rpc/methods)}
|
||||||
|
|
||||||
:app.worker/worker
|
:app.worker/worker
|
||||||
{:executor (ig/ref [::worker :app.worker/executor])
|
{:executor (ig/ref [::worker :app.worker/executor])
|
||||||
:tasks (ig/ref :app.worker/registry)
|
:tasks (ig/ref :app.worker/registry)
|
||||||
|
|
|
@ -223,15 +223,13 @@
|
||||||
(defn- resolve-mutation-methods
|
(defn- resolve-mutation-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
|
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
|
||||||
(->> (sv/scan-ns 'app.rpc.mutations.demo
|
(->> (sv/scan-ns 'app.rpc.mutations.media
|
||||||
'app.rpc.mutations.media
|
|
||||||
'app.rpc.mutations.profile
|
'app.rpc.mutations.profile
|
||||||
'app.rpc.mutations.files
|
'app.rpc.mutations.files
|
||||||
'app.rpc.mutations.comments
|
'app.rpc.mutations.comments
|
||||||
'app.rpc.mutations.projects
|
'app.rpc.mutations.projects
|
||||||
'app.rpc.mutations.teams
|
'app.rpc.mutations.teams
|
||||||
'app.rpc.mutations.management
|
'app.rpc.mutations.management
|
||||||
'app.rpc.mutations.ldap
|
|
||||||
'app.rpc.mutations.fonts
|
'app.rpc.mutations.fonts
|
||||||
'app.rpc.mutations.share-link
|
'app.rpc.mutations.share-link
|
||||||
'app.rpc.mutations.verify-token)
|
'app.rpc.mutations.verify-token)
|
||||||
|
@ -241,26 +239,65 @@
|
||||||
(defn- resolve-command-methods
|
(defn- resolve-command-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
||||||
(->> (sv/scan-ns 'app.rpc.commands.binfile)
|
(->> (sv/scan-ns 'app.rpc.commands.binfile
|
||||||
|
'app.rpc.commands.auth
|
||||||
|
'app.rpc.commands.ldap
|
||||||
|
'app.rpc.commands.demo)
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
(s/def ::storage some?)
|
|
||||||
(s/def ::session map?)
|
|
||||||
(s/def ::tokens fn?)
|
|
||||||
(s/def ::audit (s/nilable fn?))
|
(s/def ::audit (s/nilable fn?))
|
||||||
(s/def ::executors (s/map-of keyword? ::wrk/executor))
|
(s/def ::executors (s/map-of keyword? ::wrk/executor))
|
||||||
|
(s/def ::executors map?)
|
||||||
|
(s/def ::http-client fn?)
|
||||||
|
(s/def ::ldap (s/nilable map?))
|
||||||
|
(s/def ::msgbus fn?)
|
||||||
|
(s/def ::public-uri ::us/not-empty-string)
|
||||||
|
(s/def ::session map?)
|
||||||
|
(s/def ::storage some?)
|
||||||
|
(s/def ::tokens fn?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::rpc [_]
|
(defmethod ig/pre-init-spec ::methods [_]
|
||||||
(s/keys :req-un [::storage ::session ::tokens ::audit
|
(s/keys :req-un [::storage
|
||||||
::executors ::mtx/metrics ::db/pool]))
|
::session
|
||||||
|
::tokens
|
||||||
|
::audit
|
||||||
|
::executors
|
||||||
|
::public-uri
|
||||||
|
::msgbus
|
||||||
|
::http-client
|
||||||
|
::mtx/metrics
|
||||||
|
::db/pool
|
||||||
|
::ldap]))
|
||||||
|
|
||||||
(defmethod ig/init-key ::rpc
|
(defmethod ig/init-key ::methods
|
||||||
[_ cfg]
|
[_ cfg]
|
||||||
(let [mq (resolve-query-methods cfg)
|
{:mutations (resolve-mutation-methods cfg)
|
||||||
mm (resolve-mutation-methods cfg)
|
:queries (resolve-query-methods cfg)
|
||||||
cm (resolve-command-methods cfg)]
|
:commands (resolve-command-methods cfg)})
|
||||||
{:methods {:query mq :mutation mm :command cm}
|
|
||||||
:command-handler (partial rpc-command-handler cm)
|
(s/def ::mutations
|
||||||
:query-handler (partial rpc-query-handler mq)
|
(s/map-of keyword? fn?))
|
||||||
:mutation-handler (partial rpc-mutation-handler mm)}))
|
|
||||||
|
(s/def ::queries
|
||||||
|
(s/map-of keyword? fn?))
|
||||||
|
|
||||||
|
(s/def ::commands
|
||||||
|
(s/map-of keyword? fn?))
|
||||||
|
|
||||||
|
(s/def ::methods
|
||||||
|
(s/keys :req-un [::mutations
|
||||||
|
::queries
|
||||||
|
::commands]))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::routes [_]
|
||||||
|
(s/keys :req-un [::methods]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::routes
|
||||||
|
[_ {:keys [methods] :as cfg}]
|
||||||
|
[["/rpc"
|
||||||
|
["/command/:command" {:handler (partial rpc-command-handler (:commands methods))}]
|
||||||
|
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
|
||||||
|
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
|
||||||
|
:allowed-methods #{:post}}]]])
|
||||||
|
|
||||||
|
|
416
backend/src/app/rpc/commands/auth.clj
Normal file
416
backend/src/app/rpc/commands/auth.clj
Normal file
|
@ -0,0 +1,416 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.rpc.commands.auth
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.emails :as eml]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.rpc.mutations.teams :as teams]
|
||||||
|
[app.rpc.queries.profile :as profile]
|
||||||
|
[app.rpc.rlimit :as rlimit]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[buddy.hashers :as hashers]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(s/def ::email ::us/email)
|
||||||
|
(s/def ::fullname ::us/not-empty-string)
|
||||||
|
(s/def ::lang ::us/string)
|
||||||
|
(s/def ::path ::us/string)
|
||||||
|
(s/def ::profile-id ::us/uuid)
|
||||||
|
(s/def ::password ::us/not-empty-string)
|
||||||
|
(s/def ::old-password ::us/not-empty-string)
|
||||||
|
(s/def ::theme ::us/string)
|
||||||
|
(s/def ::invitation-token ::us/not-empty-string)
|
||||||
|
(s/def ::token ::us/not-empty-string)
|
||||||
|
|
||||||
|
;; ---- HELPERS
|
||||||
|
|
||||||
|
(defn derive-password
|
||||||
|
[password]
|
||||||
|
(hashers/derive password
|
||||||
|
{:alg :argon2id
|
||||||
|
:memory 16384
|
||||||
|
:iterations 20
|
||||||
|
:parallelism 2}))
|
||||||
|
|
||||||
|
(defn verify-password
|
||||||
|
[attempt password]
|
||||||
|
(try
|
||||||
|
(hashers/verify attempt password)
|
||||||
|
(catch Exception _e
|
||||||
|
{:update false
|
||||||
|
:valid false})))
|
||||||
|
|
||||||
|
(defn email-domain-in-whitelist?
|
||||||
|
"Returns true if email's domain is in the given whitelist or if
|
||||||
|
given whitelist is an empty string."
|
||||||
|
[domains email]
|
||||||
|
(if (or (empty? domains)
|
||||||
|
(nil? domains))
|
||||||
|
true
|
||||||
|
(let [[_ candidate] (-> (str/lower email)
|
||||||
|
(str/split #"@" 2))]
|
||||||
|
(contains? domains candidate))))
|
||||||
|
|
||||||
|
(def ^:private sql:profile-existence
|
||||||
|
"select exists (select * from profile
|
||||||
|
where email = ?
|
||||||
|
and deleted_at is null) as val")
|
||||||
|
|
||||||
|
(defn check-profile-existence!
|
||||||
|
[conn {:keys [email] :as params}]
|
||||||
|
(let [email (str/lower email)
|
||||||
|
result (db/exec-one! conn [sql:profile-existence email])]
|
||||||
|
(when (:val result)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-already-exists))
|
||||||
|
params))
|
||||||
|
|
||||||
|
;; ---- COMMAND: login with password
|
||||||
|
|
||||||
|
(defn login-with-password
|
||||||
|
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
|
||||||
|
|
||||||
|
(when-not (contains? cf/flags :login)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :login-disabled
|
||||||
|
:hint "login is disabled in this instance"))
|
||||||
|
|
||||||
|
(letfn [(check-password [profile password]
|
||||||
|
(when (= (:password profile) "!")
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :account-without-password
|
||||||
|
:hint "the current account does not have password"))
|
||||||
|
(:valid (verify-password password (:password profile))))
|
||||||
|
|
||||||
|
(validate-profile [profile]
|
||||||
|
(when-not (:is-active profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :wrong-credentials))
|
||||||
|
(when-not profile
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :wrong-credentials))
|
||||||
|
(when-not (check-password profile password)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :wrong-credentials))
|
||||||
|
profile)]
|
||||||
|
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
||||||
|
(validate-profile)
|
||||||
|
(profile/strip-private-attrs)
|
||||||
|
(profile/populate-additional-data conn)
|
||||||
|
(profile/decode-profile-row))
|
||||||
|
|
||||||
|
invitation (when-let [token (:invitation-token params)]
|
||||||
|
(tokens :verify {:token token :iss :team-invitation}))
|
||||||
|
|
||||||
|
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
|
||||||
|
;; invitation because invitations matches exactly; and user can't loging with other email and
|
||||||
|
;; accept invitation with other email
|
||||||
|
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
||||||
|
{:invitation-token (:invitation-token params)}
|
||||||
|
profile)]
|
||||||
|
|
||||||
|
(with-meta response
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})))))
|
||||||
|
|
||||||
|
(s/def ::login-with-password
|
||||||
|
(s/keys :req-un [::email ::password]
|
||||||
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(sv/defmethod ::login-with-password
|
||||||
|
"Performs authentication using penpot password."
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[cfg params]
|
||||||
|
(login-with-password cfg params))
|
||||||
|
|
||||||
|
;; ---- COMMAND: Logout
|
||||||
|
|
||||||
|
(s/def ::logout
|
||||||
|
(s/keys :opt-un [::profile-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::logout
|
||||||
|
"Clears the authentication cookie and logout the current session."
|
||||||
|
{:auth false}
|
||||||
|
[{:keys [session] :as cfg} _]
|
||||||
|
(with-meta {}
|
||||||
|
{:transform-response (:delete session)}))
|
||||||
|
|
||||||
|
;; ---- COMMAND: Recover Profile
|
||||||
|
|
||||||
|
(defn recover-profile
|
||||||
|
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
|
||||||
|
(letfn [(validate-token [token]
|
||||||
|
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
|
||||||
|
(:profile-id tdata)))
|
||||||
|
|
||||||
|
(update-password [conn profile-id]
|
||||||
|
(let [pwd (derive-password password)]
|
||||||
|
(db/update! conn :profile {:password pwd} {:id profile-id})))]
|
||||||
|
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(->> (validate-token token)
|
||||||
|
(update-password conn))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(s/def ::token ::us/not-empty-string)
|
||||||
|
(s/def ::recover-profile
|
||||||
|
(s/keys :req-un [::token ::password]))
|
||||||
|
|
||||||
|
(sv/defmethod ::recover-profile
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[cfg params]
|
||||||
|
(recover-profile cfg params))
|
||||||
|
|
||||||
|
;; ---- COMMAND: Prepare Register
|
||||||
|
|
||||||
|
(defn prepare-register
|
||||||
|
[{:keys [pool tokens] :as cfg} params]
|
||||||
|
(when-not (contains? cf/flags :registration)
|
||||||
|
(if-not (contains? params :invitation-token)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :registration-disabled)
|
||||||
|
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
|
||||||
|
(when-not (= (:email params) (:member-email invitation))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :email-does-not-match-invitation
|
||||||
|
:hint "email should match the invitation")))))
|
||||||
|
|
||||||
|
(when-let [domains (cf/get :registration-domain-whitelist)]
|
||||||
|
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-domain-is-not-allowed)))
|
||||||
|
|
||||||
|
;; Don't allow proceed in preparing registration if the profile is
|
||||||
|
;; already reported as spammer.
|
||||||
|
(when (eml/has-bounce-reports? pool (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email has one or many bounces reported"))
|
||||||
|
|
||||||
|
(check-profile-existence! pool params)
|
||||||
|
|
||||||
|
(when (= (str/lower (:email params))
|
||||||
|
(str/lower (:password params)))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-as-password
|
||||||
|
:hint "you can't use your email as password"))
|
||||||
|
|
||||||
|
(let [params {:email (:email params)
|
||||||
|
:password (:password params)
|
||||||
|
:invitation-token (:invitation-token params)
|
||||||
|
:backend "penpot"
|
||||||
|
:iss :prepared-register
|
||||||
|
:exp (dt/in-future "48h")}
|
||||||
|
|
||||||
|
token (tokens :generate params)]
|
||||||
|
(with-meta {:token token}
|
||||||
|
{::audit/profile-id uuid/zero})))
|
||||||
|
|
||||||
|
(s/def ::prepare-register-profile
|
||||||
|
(s/keys :req-un [::email ::password]
|
||||||
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||||
|
[cfg params]
|
||||||
|
(prepare-register cfg params))
|
||||||
|
|
||||||
|
;; ---- COMMAND: Register Profile
|
||||||
|
|
||||||
|
(defn create-profile
|
||||||
|
"Create the profile entry on the database with limited input filling
|
||||||
|
all the other fields with defaults."
|
||||||
|
[conn params]
|
||||||
|
(let [id (or (:id params) (uuid/next))
|
||||||
|
|
||||||
|
props (-> (audit/extract-utm-params params)
|
||||||
|
(merge (:props params))
|
||||||
|
(db/tjson))
|
||||||
|
|
||||||
|
password (if-let [password (:password params)]
|
||||||
|
(derive-password password)
|
||||||
|
"!")
|
||||||
|
|
||||||
|
locale (:locale params)
|
||||||
|
locale (when (and (string? locale) (not (str/blank? locale)))
|
||||||
|
locale)
|
||||||
|
|
||||||
|
backend (:backend params "penpot")
|
||||||
|
is-demo (:is-demo params false)
|
||||||
|
is-muted (:is-muted params false)
|
||||||
|
is-active (:is-active params false)
|
||||||
|
email (str/lower (:email params))
|
||||||
|
|
||||||
|
params {:id id
|
||||||
|
:fullname (:fullname params)
|
||||||
|
:email email
|
||||||
|
:auth-backend backend
|
||||||
|
:lang locale
|
||||||
|
:password password
|
||||||
|
:deleted-at (:deleted-at params)
|
||||||
|
:props props
|
||||||
|
:is-active is-active
|
||||||
|
:is-muted is-muted
|
||||||
|
:is-demo is-demo}]
|
||||||
|
(try
|
||||||
|
(-> (db/insert! conn :profile params)
|
||||||
|
(profile/decode-profile-row))
|
||||||
|
(catch org.postgresql.util.PSQLException e
|
||||||
|
(let [state (.getSQLState e)]
|
||||||
|
(if (not= state "23505")
|
||||||
|
(throw e)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-already-exists
|
||||||
|
:cause e)))))))
|
||||||
|
|
||||||
|
(defn create-profile-relations
|
||||||
|
[conn profile]
|
||||||
|
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||||
|
:name "Default"
|
||||||
|
:is-default true})]
|
||||||
|
(-> profile
|
||||||
|
(profile/strip-private-attrs)
|
||||||
|
(assoc :default-team-id (:id team))
|
||||||
|
(assoc :default-project-id (:default-project-id team)))))
|
||||||
|
|
||||||
|
(defn register-profile
|
||||||
|
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
|
||||||
|
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
||||||
|
params (merge params claims)]
|
||||||
|
(check-profile-existence! conn params)
|
||||||
|
(let [is-active (or (:is-active params)
|
||||||
|
(contains? cf/flags :insecure-register))
|
||||||
|
profile (->> (assoc params :is-active is-active)
|
||||||
|
(create-profile conn)
|
||||||
|
(create-profile-relations conn)
|
||||||
|
(profile/decode-profile-row))
|
||||||
|
invitation (when-let [token (:invitation-token params)]
|
||||||
|
(tokens :verify {:token token :iss :team-invitation}))]
|
||||||
|
(cond
|
||||||
|
;; If invitation token comes in params, this is because the user comes from team-invitation process;
|
||||||
|
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
|
||||||
|
;; session as logged). This happens only if the invitation email matches with the register email.
|
||||||
|
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||||
|
(let [claims (assoc invitation :member-id (:id profile))
|
||||||
|
token (tokens :generate claims)
|
||||||
|
resp {:invitation-token token}]
|
||||||
|
(with-meta resp
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
|
;; If auth backend is different from "penpot" means user is
|
||||||
|
;; registering using third party auth mechanism; in this case
|
||||||
|
;; we need to mark this session as logged.
|
||||||
|
(not= "penpot" (:auth-backend profile))
|
||||||
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})
|
||||||
|
|
||||||
|
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||||
|
;; to sign in the user directly, without email verification.
|
||||||
|
(true? is-active)
|
||||||
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)})
|
||||||
|
|
||||||
|
;; In all other cases, send a verification email.
|
||||||
|
:else
|
||||||
|
(let [vtoken (tokens :generate
|
||||||
|
{:iss :verify-email
|
||||||
|
:exp (dt/in-future "48h")
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email (:email profile)})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/register
|
||||||
|
:public-uri (:public-uri cfg)
|
||||||
|
:to (:email profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:token vtoken
|
||||||
|
:extra-data ptoken})
|
||||||
|
|
||||||
|
(with-meta profile
|
||||||
|
{::audit/replace-props (audit/profile->props profile)
|
||||||
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
|
(s/def ::register-profile
|
||||||
|
(s/keys :req-un [::token ::fullname]))
|
||||||
|
|
||||||
|
(sv/defmethod ::register-profile
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[{:keys [pool] :as cfg} params]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(-> (assoc cfg :conn conn)
|
||||||
|
(register-profile params))))
|
||||||
|
|
||||||
|
;; ---- COMMAND: Request Profile Recovery
|
||||||
|
|
||||||
|
(defn request-profile-recovery
|
||||||
|
[{:keys [pool tokens] :as cfg} {:keys [email] :as params}]
|
||||||
|
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
||||||
|
(let [token (tokens :generate
|
||||||
|
{:iss :password-recovery
|
||||||
|
:exp (dt/in-future "15m")
|
||||||
|
:profile-id id})]
|
||||||
|
(assoc profile :token token)))
|
||||||
|
|
||||||
|
(send-email-notification [conn profile]
|
||||||
|
(let [ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/password-recovery
|
||||||
|
:public-uri (:public-uri cfg)
|
||||||
|
:to (:email profile)
|
||||||
|
:token (:token profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:extra-data ptoken})
|
||||||
|
nil))]
|
||||||
|
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
||||||
|
(when-not (eml/allow-send-emails? conn profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-is-muted
|
||||||
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
||||||
|
|
||||||
|
(when-not (:is-active profile)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :profile-not-verified
|
||||||
|
:hint "the user need to validate profile before recover password"))
|
||||||
|
|
||||||
|
(when (eml/has-bounce-reports? conn (:email profile))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||||
|
|
||||||
|
(->> profile
|
||||||
|
(create-recovery-token)
|
||||||
|
(send-email-notification conn))))))
|
||||||
|
|
||||||
|
(s/def ::request-profile-recovery
|
||||||
|
(s/keys :req-un [::email]))
|
||||||
|
|
||||||
|
(sv/defmethod ::request-profile-recovery {:auth false}
|
||||||
|
[cfg params]
|
||||||
|
(request-profile-recovery cfg params))
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
;;
|
;;
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.rpc.mutations.demo
|
(ns app.rpc.commands.demo
|
||||||
"A demo specific mutations."
|
"A demo specific mutations."
|
||||||
(:require
|
(:require
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
|
@ -45,8 +45,8 @@
|
||||||
:hint "Demo users are disabled by config."))
|
:hint "Demo users are disabled by config."))
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(->> (#'profile/create-profile conn params)
|
(->> (cmd.auth/create-profile conn params)
|
||||||
(#'profile/create-profile-relations conn))
|
(cmd.auth/create-profile-relations conn))
|
||||||
|
|
||||||
(with-meta {:email email
|
(with-meta {:email email
|
||||||
:password password}
|
:password password}
|
75
backend/src/app/rpc/commands/ldap.clj
Normal file
75
backend/src/app/rpc/commands/ldap.clj
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.rpc.commands.ldap
|
||||||
|
(:require
|
||||||
|
[app.auth.ldap :as ldap]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as-alias audit]
|
||||||
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
|
[app.rpc.queries.profile :as profile]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
;; --- COMMAND: login-with-ldap
|
||||||
|
|
||||||
|
(declare login-or-register)
|
||||||
|
|
||||||
|
(s/def ::email ::us/email)
|
||||||
|
(s/def ::password ::us/string)
|
||||||
|
(s/def ::invitation-token ::us/string)
|
||||||
|
|
||||||
|
(s/def ::login-with-ldap
|
||||||
|
(s/keys :req-un [::email ::password]
|
||||||
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(sv/defmethod ::login-with-ldap {:auth false}
|
||||||
|
[{:keys [session tokens ldap] :as cfg} params]
|
||||||
|
(when-not ldap
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :ldap-not-initialized
|
||||||
|
:hide "ldap auth provider is not initialized"))
|
||||||
|
|
||||||
|
(let [info (ldap/authenticate ldap params)]
|
||||||
|
(when-not info
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :wrong-credentials))
|
||||||
|
|
||||||
|
(let [profile (login-or-register cfg info)]
|
||||||
|
(if-let [token (:invitation-token params)]
|
||||||
|
;; If invitation token comes in params, this is because the
|
||||||
|
;; user comes from team-invitation process; in this case,
|
||||||
|
;; regenerate token and send back to the user a new invitation
|
||||||
|
;; token (and mark current session as logged).
|
||||||
|
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||||
|
claims (assoc claims
|
||||||
|
:member-id (:id profile)
|
||||||
|
:member-email (:email profile))
|
||||||
|
token (tokens :generate claims)]
|
||||||
|
(with-meta {:invitation-token token}
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
|
(with-meta profile
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)})))))
|
||||||
|
|
||||||
|
(defn- login-or-register
|
||||||
|
[{:keys [pool] :as cfg} info]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(or (some->> (:email info)
|
||||||
|
(profile/retrieve-profile-data-by-email conn)
|
||||||
|
(profile/populate-additional-data conn)
|
||||||
|
(profile/decode-profile-row))
|
||||||
|
(->> (assoc info :is-active true :is-demo false)
|
||||||
|
(cmd.auth/create-profile conn)
|
||||||
|
(cmd.auth/create-profile-relations conn)
|
||||||
|
(profile/strip-private-attrs)))))
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) UXBOX Labs SL
|
|
||||||
|
|
||||||
(ns app.rpc.mutations.ldap
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.config :as cfg]
|
|
||||||
[app.db :as db]
|
|
||||||
[app.loggers.audit :as audit]
|
|
||||||
[app.rpc.mutations.profile :as profile-m]
|
|
||||||
[app.rpc.queries.profile :as profile-q]
|
|
||||||
[app.util.services :as sv]
|
|
||||||
[clj-ldap.client :as ldap]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[clojure.string]))
|
|
||||||
|
|
||||||
|
|
||||||
(s/def ::fullname ::us/not-empty-string)
|
|
||||||
(s/def ::email ::us/email)
|
|
||||||
(s/def ::backend ::us/not-empty-string)
|
|
||||||
|
|
||||||
(s/def ::info-data
|
|
||||||
(s/keys :req-un [::fullname ::email ::backend]))
|
|
||||||
|
|
||||||
(defn connect
|
|
||||||
^java.lang.AutoCloseable
|
|
||||||
[]
|
|
||||||
(let [params {:ssl? (cfg/get :ldap-ssl)
|
|
||||||
:startTLS? (cfg/get :ldap-starttls)
|
|
||||||
:bind-dn (cfg/get :ldap-bind-dn)
|
|
||||||
:password (cfg/get :ldap-bind-password)
|
|
||||||
:host {:address (cfg/get :ldap-host)
|
|
||||||
:port (cfg/get :ldap-port)}}]
|
|
||||||
(try
|
|
||||||
(ldap/connect params)
|
|
||||||
(catch Exception e
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :ldap-disabled
|
|
||||||
:hint "ldap disabled or unable to connect"
|
|
||||||
:cause e)))))
|
|
||||||
|
|
||||||
;; --- Mutation: login-with-ldap
|
|
||||||
|
|
||||||
(declare authenticate)
|
|
||||||
(declare login-or-register)
|
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
|
||||||
(s/def ::password ::us/string)
|
|
||||||
(s/def ::invitation-token ::us/string)
|
|
||||||
|
|
||||||
(s/def ::login-with-ldap
|
|
||||||
(s/keys :req-un [::email ::password]
|
|
||||||
:opt-un [::invitation-token]))
|
|
||||||
|
|
||||||
(sv/defmethod ::login-with-ldap {:auth false}
|
|
||||||
[{:keys [pool session tokens] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [info (authenticate params)
|
|
||||||
cfg (assoc cfg :conn conn)]
|
|
||||||
|
|
||||||
(when-not info
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :wrong-credentials))
|
|
||||||
|
|
||||||
(when-not (s/valid? ::info-data info)
|
|
||||||
(let [explain (s/explain-str ::info-data info)]
|
|
||||||
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :wrong-ldap-response
|
|
||||||
:reason explain)))
|
|
||||||
|
|
||||||
(let [profile (login-or-register cfg {:email (:email info)
|
|
||||||
:backend (:backend info)
|
|
||||||
:fullname (:fullname info)})]
|
|
||||||
(if-let [token (:invitation-token params)]
|
|
||||||
;; If invitation token comes in params, this is because the
|
|
||||||
;; user comes from team-invitation process; in this case,
|
|
||||||
;; regenerate token and send back to the user a new invitation
|
|
||||||
;; token (and mark current session as logged).
|
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)]
|
|
||||||
(with-meta {:invitation-token token}
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
(with-meta profile
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)}))))))
|
|
||||||
|
|
||||||
(defn- replace-several [s & {:as replacements}]
|
|
||||||
(reduce-kv clojure.string/replace s replacements))
|
|
||||||
|
|
||||||
(defn- get-ldap-user
|
|
||||||
[cpool {:keys [email] :as params}]
|
|
||||||
(let [query (-> (cfg/get :ldap-user-query)
|
|
||||||
(replace-several ":username" email))
|
|
||||||
|
|
||||||
attrs [(cfg/get :ldap-attrs-username)
|
|
||||||
(cfg/get :ldap-attrs-email)
|
|
||||||
(cfg/get :ldap-attrs-photo)
|
|
||||||
(cfg/get :ldap-attrs-fullname)]
|
|
||||||
|
|
||||||
base-dn (cfg/get :ldap-base-dn)
|
|
||||||
params {:filter query
|
|
||||||
:sizelimit 1
|
|
||||||
:attributes attrs}]
|
|
||||||
(first (ldap/search cpool base-dn params))))
|
|
||||||
|
|
||||||
(defn- authenticate
|
|
||||||
[{:keys [password email] :as params}]
|
|
||||||
(with-open [conn (connect)]
|
|
||||||
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
|
||||||
(when (ldap/bind? conn dn password)
|
|
||||||
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
|
||||||
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
|
||||||
:email email
|
|
||||||
:backend "ldap"}))))
|
|
||||||
|
|
||||||
(defn- login-or-register
|
|
||||||
[{:keys [conn] :as cfg} info]
|
|
||||||
(or (some->> (:email info)
|
|
||||||
(profile-q/retrieve-profile-data-by-email conn)
|
|
||||||
(profile-q/populate-additional-data conn)
|
|
||||||
(profile-q/decode-profile-row))
|
|
||||||
(let [params (-> info
|
|
||||||
(assoc :is-active true)
|
|
||||||
(assoc :is-demo false))]
|
|
||||||
(->> params
|
|
||||||
(profile-m/create-profile conn)
|
|
||||||
(profile-m/create-profile-relations conn)
|
|
||||||
(profile-q/strip-private-attrs)))))
|
|
|
@ -9,19 +9,18 @@
|
||||||
[app.common.data :as d]
|
[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.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.emails :as eml]
|
[app.emails :as eml]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.rpc.mutations.teams :as teams]
|
[app.rpc.mutations.teams :as teams]
|
||||||
[app.rpc.queries.profile :as profile]
|
[app.rpc.queries.profile :as profile]
|
||||||
[app.rpc.rlimit :as rlimit]
|
[app.rpc.rlimit :as rlimit]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[buddy.hashers :as hashers]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[promesa.core :as p]
|
[promesa.core :as p]
|
||||||
|
@ -37,310 +36,6 @@
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
(s/def ::old-password ::us/not-empty-string)
|
(s/def ::old-password ::us/not-empty-string)
|
||||||
(s/def ::theme ::us/string)
|
(s/def ::theme ::us/string)
|
||||||
(s/def ::invitation-token ::us/not-empty-string)
|
|
||||||
|
|
||||||
(declare check-profile-existence!)
|
|
||||||
(declare create-profile)
|
|
||||||
(declare create-profile-relations)
|
|
||||||
(declare register-profile)
|
|
||||||
|
|
||||||
(defn email-domain-in-whitelist?
|
|
||||||
"Returns true if email's domain is in the given whitelist or if
|
|
||||||
given whitelist is an empty string."
|
|
||||||
[domains email]
|
|
||||||
(if (or (empty? domains)
|
|
||||||
(nil? domains))
|
|
||||||
true
|
|
||||||
(let [[_ candidate] (-> (str/lower email)
|
|
||||||
(str/split #"@" 2))]
|
|
||||||
(contains? domains candidate))))
|
|
||||||
|
|
||||||
(def ^:private sql:profile-existence
|
|
||||||
"select exists (select * from profile
|
|
||||||
where email = ?
|
|
||||||
and deleted_at is null) as val")
|
|
||||||
|
|
||||||
(defn check-profile-existence!
|
|
||||||
[conn {:keys [email] :as params}]
|
|
||||||
(let [email (str/lower email)
|
|
||||||
result (db/exec-one! conn [sql:profile-existence email])]
|
|
||||||
(when (:val result)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-already-exists))
|
|
||||||
params))
|
|
||||||
|
|
||||||
(defn derive-password
|
|
||||||
[password]
|
|
||||||
(hashers/derive password
|
|
||||||
{:alg :argon2id
|
|
||||||
:memory 16384
|
|
||||||
:iterations 20
|
|
||||||
:parallelism 2}))
|
|
||||||
|
|
||||||
(defn verify-password
|
|
||||||
[attempt password]
|
|
||||||
(try
|
|
||||||
(hashers/verify attempt password)
|
|
||||||
(catch Exception _e
|
|
||||||
{:update false
|
|
||||||
:valid false})))
|
|
||||||
|
|
||||||
(defn decode-profile-row
|
|
||||||
[{:keys [props] :as profile}]
|
|
||||||
(cond-> profile
|
|
||||||
(db/pgobject? props "jsonb")
|
|
||||||
(assoc :props (db/decode-transit-pgobject props))))
|
|
||||||
|
|
||||||
;; --- MUTATION: Prepare Register
|
|
||||||
|
|
||||||
(s/def ::prepare-register-profile
|
|
||||||
(s/keys :req-un [::email ::password]
|
|
||||||
:opt-un [::invitation-token]))
|
|
||||||
|
|
||||||
(sv/defmethod ::prepare-register-profile {:auth false}
|
|
||||||
[{:keys [pool tokens] :as cfg} params]
|
|
||||||
(when-not (contains? cf/flags :registration)
|
|
||||||
(if-not (contains? params :invitation-token)
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :registration-disabled)
|
|
||||||
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
|
|
||||||
(when-not (= (:email params) (:member-email invitation))
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :email-does-not-match-invitation
|
|
||||||
:hint "email should match the invitation")))))
|
|
||||||
|
|
||||||
(when-let [domains (cf/get :registration-domain-whitelist)]
|
|
||||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-domain-is-not-allowed)))
|
|
||||||
|
|
||||||
;; Don't allow proceed in preparing registration if the profile is
|
|
||||||
;; already reported as spammer.
|
|
||||||
(when (eml/has-bounce-reports? pool (:email params))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-has-permanent-bounces
|
|
||||||
:hint "looks like the email has one or many bounces reported"))
|
|
||||||
|
|
||||||
(check-profile-existence! pool params)
|
|
||||||
|
|
||||||
(when (= (str/lower (:email params))
|
|
||||||
(str/lower (:password params)))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-as-password
|
|
||||||
:hint "you can't use your email as password"))
|
|
||||||
|
|
||||||
(let [params {:email (:email params)
|
|
||||||
:password (:password params)
|
|
||||||
:invitation-token (:invitation-token params)
|
|
||||||
:backend "penpot"
|
|
||||||
:iss :prepared-register
|
|
||||||
:exp (dt/in-future "48h")}
|
|
||||||
|
|
||||||
token (tokens :generate params)]
|
|
||||||
(with-meta {:token token}
|
|
||||||
{::audit/profile-id uuid/zero})))
|
|
||||||
|
|
||||||
;; --- MUTATION: Register Profile
|
|
||||||
|
|
||||||
(s/def ::token ::us/not-empty-string)
|
|
||||||
(s/def ::register-profile
|
|
||||||
(s/keys :req-un [::token ::fullname]))
|
|
||||||
|
|
||||||
(sv/defmethod ::register-profile
|
|
||||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
|
||||||
[{:keys [pool] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(-> (assoc cfg :conn conn)
|
|
||||||
(register-profile params))))
|
|
||||||
|
|
||||||
(defn register-profile
|
|
||||||
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
|
|
||||||
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
|
||||||
params (merge params claims)]
|
|
||||||
(check-profile-existence! conn params)
|
|
||||||
(let [is-active (or (:is-active params)
|
|
||||||
(contains? cf/flags :insecure-register))
|
|
||||||
profile (->> (assoc params :is-active is-active)
|
|
||||||
(create-profile conn)
|
|
||||||
(create-profile-relations conn)
|
|
||||||
(decode-profile-row))
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
|
||||||
(tokens :verify {:token token :iss :team-invitation}))]
|
|
||||||
(cond
|
|
||||||
;; If invitation token comes in params, this is because the user comes from team-invitation process;
|
|
||||||
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
|
|
||||||
;; session as logged). This happens only if the invitation email matches with the register email.
|
|
||||||
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
|
||||||
(let [claims (assoc invitation :member-id (:id profile))
|
|
||||||
token (tokens :generate claims)
|
|
||||||
resp {:invitation-token token}]
|
|
||||||
(with-meta resp
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/replace-props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
;; If auth backend is different from "penpot" means user is
|
|
||||||
;; registering using third party auth mechanism; in this case
|
|
||||||
;; we need to mark this session as logged.
|
|
||||||
(not= "penpot" (:auth-backend profile))
|
|
||||||
(with-meta (profile/strip-private-attrs profile)
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/replace-props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)})
|
|
||||||
|
|
||||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
|
||||||
;; to sign in the user directly, without email verification.
|
|
||||||
(true? is-active)
|
|
||||||
(with-meta (profile/strip-private-attrs profile)
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/replace-props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)})
|
|
||||||
|
|
||||||
;; In all other cases, send a verification email.
|
|
||||||
:else
|
|
||||||
(let [vtoken (tokens :generate
|
|
||||||
{:iss :verify-email
|
|
||||||
:exp (dt/in-future "48h")
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:email (:email profile)})
|
|
||||||
ptoken (tokens :generate-predefined
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)})]
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/register
|
|
||||||
:public-uri (:public-uri cfg)
|
|
||||||
:to (:email profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:token vtoken
|
|
||||||
:extra-data ptoken})
|
|
||||||
|
|
||||||
(with-meta profile
|
|
||||||
{::audit/replace-props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)}))))))
|
|
||||||
|
|
||||||
(defn create-profile
|
|
||||||
"Create the profile entry on the database with limited input filling
|
|
||||||
all the other fields with defaults."
|
|
||||||
[conn params]
|
|
||||||
(let [id (or (:id params) (uuid/next))
|
|
||||||
|
|
||||||
props (-> (audit/extract-utm-params params)
|
|
||||||
(merge (:props params))
|
|
||||||
(db/tjson))
|
|
||||||
|
|
||||||
password (if-let [password (:password params)]
|
|
||||||
(derive-password password)
|
|
||||||
"!")
|
|
||||||
|
|
||||||
locale (:locale params)
|
|
||||||
locale (when (and (string? locale) (not (str/blank? locale)))
|
|
||||||
locale)
|
|
||||||
|
|
||||||
backend (:backend params "penpot")
|
|
||||||
is-demo (:is-demo params false)
|
|
||||||
is-muted (:is-muted params false)
|
|
||||||
is-active (:is-active params false)
|
|
||||||
email (str/lower (:email params))
|
|
||||||
|
|
||||||
params {:id id
|
|
||||||
:fullname (:fullname params)
|
|
||||||
:email email
|
|
||||||
:auth-backend backend
|
|
||||||
:lang locale
|
|
||||||
:password password
|
|
||||||
:deleted-at (:deleted-at params)
|
|
||||||
:props props
|
|
||||||
:is-active is-active
|
|
||||||
:is-muted is-muted
|
|
||||||
:is-demo is-demo}]
|
|
||||||
(try
|
|
||||||
(-> (db/insert! conn :profile params)
|
|
||||||
(decode-profile-row))
|
|
||||||
(catch org.postgresql.util.PSQLException e
|
|
||||||
(let [state (.getSQLState e)]
|
|
||||||
(if (not= state "23505")
|
|
||||||
(throw e)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-already-exists
|
|
||||||
:cause e)))))))
|
|
||||||
|
|
||||||
(defn create-profile-relations
|
|
||||||
[conn profile]
|
|
||||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
|
||||||
:name "Default"
|
|
||||||
:is-default true})]
|
|
||||||
(-> profile
|
|
||||||
(profile/strip-private-attrs)
|
|
||||||
(assoc :default-team-id (:id team))
|
|
||||||
(assoc :default-project-id (:default-project-id team)))))
|
|
||||||
|
|
||||||
;; --- MUTATION: Login
|
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
|
||||||
(s/def ::scope ::us/string)
|
|
||||||
|
|
||||||
(s/def ::login
|
|
||||||
(s/keys :req-un [::email ::password]
|
|
||||||
:opt-un [::scope ::invitation-token]))
|
|
||||||
|
|
||||||
(sv/defmethod ::login
|
|
||||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
|
||||||
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
|
|
||||||
|
|
||||||
(when-not (contains? cf/flags :login)
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :login-disabled
|
|
||||||
:hint "login is disabled in this instance"))
|
|
||||||
|
|
||||||
(letfn [(check-password [profile password]
|
|
||||||
(when (= (:password profile) "!")
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :account-without-password))
|
|
||||||
(:valid (verify-password password (:password profile))))
|
|
||||||
|
|
||||||
(validate-profile [profile]
|
|
||||||
(when-not (:is-active profile)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :wrong-credentials))
|
|
||||||
(when-not profile
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :wrong-credentials))
|
|
||||||
(when-not (check-password profile password)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :wrong-credentials))
|
|
||||||
profile)]
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
|
||||||
(validate-profile)
|
|
||||||
(profile/strip-private-attrs)
|
|
||||||
(profile/populate-additional-data conn)
|
|
||||||
(decode-profile-row))
|
|
||||||
|
|
||||||
invitation (when-let [token (:invitation-token params)]
|
|
||||||
(tokens :verify {:token token :iss :team-invitation}))
|
|
||||||
|
|
||||||
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
|
|
||||||
;; invitation because invitations matches exactly; and user can't loging with other email and
|
|
||||||
;; accept invitation with other email
|
|
||||||
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
|
||||||
{:invitation-token (:invitation-token params)}
|
|
||||||
profile)]
|
|
||||||
|
|
||||||
(with-meta response
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)})))))
|
|
||||||
|
|
||||||
;; --- MUTATION: Logout
|
|
||||||
|
|
||||||
(s/def ::logout
|
|
||||||
(s/keys :opt-un [::profile-id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::logout {:auth false}
|
|
||||||
[{:keys [session] :as cfg} _]
|
|
||||||
(with-meta {}
|
|
||||||
{:transform-response (:delete session)}))
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile (own)
|
;; --- MUTATION: Update Profile (own)
|
||||||
|
|
||||||
|
@ -414,7 +109,7 @@
|
||||||
(defn- validate-password!
|
(defn- validate-password!
|
||||||
[conn {:keys [profile-id old-password] :as params}]
|
[conn {:keys [profile-id old-password] :as params}]
|
||||||
(let [profile (db/get-by-id conn :profile profile-id)]
|
(let [profile (db/get-by-id conn :profile profile-id)]
|
||||||
(when-not (:valid (verify-password old-password (:password profile)))
|
(when-not (:valid (cmd.auth/verify-password old-password (:password profile)))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :old-password-not-match))
|
:code :old-password-not-match))
|
||||||
profile))
|
profile))
|
||||||
|
@ -422,7 +117,7 @@
|
||||||
(defn update-profile-password!
|
(defn update-profile-password!
|
||||||
[conn {:keys [id password] :as profile}]
|
[conn {:keys [id password] :as profile}]
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
{:password (derive-password password)}
|
{:password (cmd.auth/derive-password password)}
|
||||||
{:id id}))
|
{:id id}))
|
||||||
|
|
||||||
;; --- MUTATION: Update Photo
|
;; --- MUTATION: Update Photo
|
||||||
|
@ -481,7 +176,7 @@
|
||||||
(defn- change-email-immediately
|
(defn- change-email-immediately
|
||||||
[{:keys [conn]} {:keys [profile email] :as params}]
|
[{:keys [conn]} {:keys [profile email] :as params}]
|
||||||
(when (not= email (:email profile))
|
(when (not= email (:email profile))
|
||||||
(check-profile-existence! conn params))
|
(cmd.auth/check-profile-existence! conn params))
|
||||||
(db/update! conn :profile
|
(db/update! conn :profile
|
||||||
{:email email}
|
{:email email}
|
||||||
{:id (:id profile)})
|
{:id (:id profile)})
|
||||||
|
@ -499,7 +194,7 @@
|
||||||
:profile-id (:id profile)})]
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
(when (not= email (:email profile))
|
(when (not= email (:email profile))
|
||||||
(check-profile-existence! conn params))
|
(cmd.auth/check-profile-existence! conn params))
|
||||||
|
|
||||||
(when-not (eml/allow-send-emails? conn profile)
|
(when-not (eml/allow-send-emails? conn profile)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
|
@ -526,76 +221,6 @@
|
||||||
[conn id]
|
[conn id]
|
||||||
(db/get-by-id conn :profile id {:for-update true}))
|
(db/get-by-id conn :profile id {:for-update true}))
|
||||||
|
|
||||||
;; --- MUTATION: Request Profile Recovery
|
|
||||||
|
|
||||||
(s/def ::request-profile-recovery
|
|
||||||
(s/keys :req-un [::email]))
|
|
||||||
|
|
||||||
(sv/defmethod ::request-profile-recovery {:auth false}
|
|
||||||
[{:keys [pool tokens] :as cfg} {:keys [email] :as params}]
|
|
||||||
(letfn [(create-recovery-token [{:keys [id] :as profile}]
|
|
||||||
(let [token (tokens :generate
|
|
||||||
{:iss :password-recovery
|
|
||||||
:exp (dt/in-future "15m")
|
|
||||||
:profile-id id})]
|
|
||||||
(assoc profile :token token)))
|
|
||||||
|
|
||||||
(send-email-notification [conn profile]
|
|
||||||
(let [ptoken (tokens :generate-predefined
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)})]
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/password-recovery
|
|
||||||
:public-uri (:public-uri cfg)
|
|
||||||
:to (:email profile)
|
|
||||||
:token (:token profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:extra-data ptoken})
|
|
||||||
nil))]
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
|
|
||||||
(when-not (eml/allow-send-emails? conn profile)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :profile-is-muted
|
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
|
|
||||||
|
|
||||||
(when-not (:is-active profile)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :profile-not-verified
|
|
||||||
:hint "the user need to validate profile before recover password"))
|
|
||||||
|
|
||||||
(when (eml/has-bounce-reports? conn (:email profile))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-has-permanent-bounces
|
|
||||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
|
||||||
|
|
||||||
(->> profile
|
|
||||||
(create-recovery-token)
|
|
||||||
(send-email-notification conn))))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- MUTATION: Recover Profile
|
|
||||||
|
|
||||||
(s/def ::token ::us/not-empty-string)
|
|
||||||
(s/def ::recover-profile
|
|
||||||
(s/keys :req-un [::token ::password]))
|
|
||||||
|
|
||||||
(sv/defmethod ::recover-profile
|
|
||||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
|
||||||
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
|
|
||||||
(letfn [(validate-token [token]
|
|
||||||
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
|
|
||||||
(:profile-id tdata)))
|
|
||||||
|
|
||||||
(update-password [conn profile-id]
|
|
||||||
(let [pwd (derive-password password)]
|
|
||||||
(db/update! conn :profile {:password pwd} {:id profile-id})))]
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(->> (validate-token token)
|
|
||||||
(update-password conn))
|
|
||||||
nil)))
|
|
||||||
|
|
||||||
;; --- MUTATION: Update Profile Props
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
|
@ -668,3 +293,61 @@
|
||||||
:code :owner-teams-with-people
|
:code :owner-teams-with-people
|
||||||
:hint "The user need to transfer ownership of owned teams."
|
:hint "The user need to transfer ownership of owned teams."
|
||||||
:context {:teams (mapv :team-id rows)}))))
|
:context {:teams (mapv :team-id rows)}))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x)
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;; --- MUTATION: Login
|
||||||
|
|
||||||
|
(s/def ::login ::cmd.auth/login-with-password)
|
||||||
|
|
||||||
|
(sv/defmethod ::login
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[cfg params]
|
||||||
|
(cmd.auth/login-with-password cfg params))
|
||||||
|
|
||||||
|
;; --- MUTATION: Logout
|
||||||
|
|
||||||
|
(s/def ::logout ::cmd.auth/logout)
|
||||||
|
|
||||||
|
(sv/defmethod ::logout {:auth false}
|
||||||
|
[{:keys [session] :as cfg} _]
|
||||||
|
(with-meta {}
|
||||||
|
{:transform-response (:delete session)}))
|
||||||
|
|
||||||
|
;; --- MUTATION: Recover Profile
|
||||||
|
|
||||||
|
(s/def ::recover-profile ::cmd.auth/recover-profile)
|
||||||
|
|
||||||
|
(sv/defmethod ::recover-profile
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[cfg params]
|
||||||
|
(cmd.auth/recover-profile cfg params))
|
||||||
|
|
||||||
|
;; --- MUTATION: Prepare Register
|
||||||
|
|
||||||
|
(s/def ::prepare-register-profile ::cmd.auth/prepare-register-profile)
|
||||||
|
|
||||||
|
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||||
|
[cfg params]
|
||||||
|
(cmd.auth/prepare-register cfg params))
|
||||||
|
|
||||||
|
;; --- MUTATION: Register Profile
|
||||||
|
|
||||||
|
(s/def ::register-profile ::cmd.auth/register-profile)
|
||||||
|
|
||||||
|
(sv/defmethod ::register-profile
|
||||||
|
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||||
|
[{:keys [pool] :as cfg} params]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(-> (assoc cfg :conn conn)
|
||||||
|
(cmd.auth/register-profile params))))
|
||||||
|
|
||||||
|
;; --- MUTATION: Request Profile Recovery
|
||||||
|
|
||||||
|
(s/def ::request-profile-recovery ::cmd.auth/request-profile-recovery)
|
||||||
|
|
||||||
|
(sv/defmethod ::request-profile-recovery {:auth false}
|
||||||
|
[cfg params]
|
||||||
|
(cmd.auth/request-profile-recovery cfg params))
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.rpc.mutations.profile :refer [derive-password]]
|
[app.rpc.commands.auth :refer [derive-password]]
|
||||||
[app.main :refer [system]]))
|
[app.main :refer [system]]))
|
||||||
|
|
||||||
(defn reset-passwords
|
(defn reset-passwords
|
||||||
|
|
|
@ -46,7 +46,13 @@
|
||||||
(t/is (sto/storage-object? mobj1))
|
(t/is (sto/storage-object? mobj1))
|
||||||
(t/is (sto/storage-object? mobj2))
|
(t/is (sto/storage-object? mobj2))
|
||||||
(t/is (= 122785 (:size mobj1)))
|
(t/is (= 122785 (:size mobj1)))
|
||||||
(t/is (= 3303 (:size mobj2)))))
|
|
||||||
|
;; This is because in ubuntu 21.04 generates different
|
||||||
|
;; thumbnail that in ubuntu 22.04. This hack should be removed
|
||||||
|
;; when we all use the ubuntu 22.04 devenv image.
|
||||||
|
(t/is (or
|
||||||
|
(= 3302 (:size mobj2))
|
||||||
|
(= 3303 (:size mobj2))))))
|
||||||
))
|
))
|
||||||
|
|
||||||
(t/deftest media-object-upload
|
(t/deftest media-object-upload
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
|
[app.rpc.commands.auth :as cauth]
|
||||||
[app.test-helpers :as th]
|
[app.test-helpers :as th]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
|
@ -27,11 +28,10 @@
|
||||||
;; Test with wrong credentials
|
;; Test with wrong credentials
|
||||||
(t/deftest profile-login-failed-1
|
(t/deftest profile-login-failed-1
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
data {::th/type :login
|
data {::th/type :login-with-password
|
||||||
:email "profile1.test@nodomain.com"
|
:email "profile1.test@nodomain.com"
|
||||||
:password "foobar"
|
:password "foobar"}
|
||||||
:scope "foobar"}
|
out (th/command! data)]
|
||||||
out (th/mutation! data)]
|
|
||||||
|
|
||||||
#_(th/print-result! out)
|
#_(th/print-result! out)
|
||||||
(let [error (:error out)]
|
(let [error (:error out)]
|
||||||
|
@ -42,11 +42,10 @@
|
||||||
;; Test with good credentials but profile not activated.
|
;; Test with good credentials but profile not activated.
|
||||||
(t/deftest profile-login-failed-2
|
(t/deftest profile-login-failed-2
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
data {::th/type :login
|
data {::th/type :login-with-password
|
||||||
:email "profile1.test@nodomain.com"
|
:email "profile1.test@nodomain.com"
|
||||||
:password "123123"
|
:password "123123"}
|
||||||
:scope "foobar"}
|
out (th/command! data)]
|
||||||
out (th/mutation! data)]
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(let [error (:error out)]
|
(let [error (:error out)]
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-info? error))
|
||||||
|
@ -58,8 +57,7 @@
|
||||||
(let [profile (th/create-profile* 1 {:is-active true})
|
(let [profile (th/create-profile* 1 {:is-active true})
|
||||||
data {::th/type :login
|
data {::th/type :login
|
||||||
:email "profile1.test@nodomain.com"
|
:email "profile1.test@nodomain.com"
|
||||||
:password "123123"
|
:password "123123"}
|
||||||
:scope "foobar"}
|
|
||||||
out (th/mutation! data)]
|
out (th/mutation! data)]
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
|
@ -161,11 +159,11 @@
|
||||||
(t/deftest registration-domain-whitelist
|
(t/deftest registration-domain-whitelist
|
||||||
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
|
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
|
||||||
(t/testing "allowed email domain"
|
(t/testing "allowed email domain"
|
||||||
(t/is (true? (profile/email-domain-in-whitelist? whitelist "username@ya.ru")))
|
(t/is (true? (cauth/email-domain-in-whitelist? whitelist "username@ya.ru")))
|
||||||
(t/is (true? (profile/email-domain-in-whitelist? #{} "username@somedomain.com"))))
|
(t/is (true? (cauth/email-domain-in-whitelist? #{} "username@somedomain.com"))))
|
||||||
|
|
||||||
(t/testing "not allowed email domain"
|
(t/testing "not allowed email domain"
|
||||||
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
(t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
||||||
|
|
||||||
(t/deftest prepare-register-and-register-profile
|
(t/deftest prepare-register-and-register-profile
|
||||||
(let [data {::th/type :prepare-register-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
|
|
|
@ -9,14 +9,15 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.flags :as flags]
|
[app.common.flags :as flags]
|
||||||
[app.common.pages :as cp]
|
[app.common.pages :as cp]
|
||||||
|
[app.common.pprint :as pp]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.common.pprint :as pp]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.main :as main]
|
[app.main :as main]
|
||||||
[app.media]
|
[app.media]
|
||||||
[app.migrations]
|
[app.migrations]
|
||||||
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
[app.rpc.mutations.files :as files]
|
[app.rpc.mutations.files :as files]
|
||||||
[app.rpc.mutations.profile :as profile]
|
[app.rpc.mutations.profile :as profile]
|
||||||
[app.rpc.mutations.projects :as projects]
|
[app.rpc.mutations.projects :as projects]
|
||||||
|
@ -31,8 +32,8 @@
|
||||||
[expound.alpha :as expound]
|
[expound.alpha :as expound]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[mockery.core :as mk]
|
[mockery.core :as mk]
|
||||||
[yetti.request :as yrq]
|
[promesa.core :as p]
|
||||||
[promesa.core :as p])
|
[yetti.request :as yrq])
|
||||||
(:import org.postgresql.ds.PGSimpleDataSource))
|
(:import org.postgresql.ds.PGSimpleDataSource))
|
||||||
|
|
||||||
(def ^:dynamic *system* nil)
|
(def ^:dynamic *system* nil)
|
||||||
|
@ -59,10 +60,12 @@
|
||||||
:app.http/router
|
:app.http/router
|
||||||
:app.http.awsns/handler
|
:app.http.awsns/handler
|
||||||
:app.http.session/updater
|
:app.http.session/updater
|
||||||
:app.http.oauth/google
|
:app.auth.oidc/google-provider
|
||||||
:app.http.oauth/gitlab
|
:app.auth.oidc/gitlab-provider
|
||||||
:app.http.oauth/github
|
:app.auth.oidc/github-provider
|
||||||
:app.http.oauth/all
|
:app.auth.oidc/generic-provider
|
||||||
|
:app.auth.oidc/routes
|
||||||
|
;; :app.auth.ldap/provider
|
||||||
:app.worker/executors-monitor
|
:app.worker/executors-monitor
|
||||||
:app.http.oauth/handler
|
:app.http.oauth/handler
|
||||||
:app.notifications/handler
|
:app.notifications/handler
|
||||||
|
@ -81,9 +84,9 @@
|
||||||
(try
|
(try
|
||||||
(binding [*system* system
|
(binding [*system* system
|
||||||
*pool* (:app.db/pool system)]
|
*pool* (:app.db/pool system)]
|
||||||
(mk/with-mocks [mock1 {:target 'app.rpc.mutations.profile/derive-password
|
(mk/with-mocks [mock1 {:target 'app.rpc.commands.auth/derive-password
|
||||||
:return identity}
|
:return identity}
|
||||||
mock2 {:target 'app.rpc.mutations.profile/verify-password
|
mock2 {:target 'app.rpc.commands.auth/verify-password
|
||||||
:return (fn [a b] {:valid (= a b)})}]
|
:return (fn [a b] {:valid (= a b)})}]
|
||||||
(next)))
|
(next)))
|
||||||
(finally
|
(finally
|
||||||
|
@ -140,8 +143,8 @@
|
||||||
:is-demo false}
|
:is-demo false}
|
||||||
params)]
|
params)]
|
||||||
(->> params
|
(->> params
|
||||||
(#'profile/create-profile conn)
|
(cmd.auth/create-profile conn)
|
||||||
(#'profile/create-profile-relations conn)))))
|
(cmd.auth/create-profile-relations conn)))))
|
||||||
|
|
||||||
(defn create-project*
|
(defn create-project*
|
||||||
([i params] (create-project* *pool* i params))
|
([i params] (create-project* *pool* i params))
|
||||||
|
@ -267,17 +270,21 @@
|
||||||
{:error (handle-error e#)
|
{:error (handle-error e#)
|
||||||
:result nil})))
|
:result nil})))
|
||||||
|
|
||||||
|
(defn command!
|
||||||
|
[{:keys [::type] :as data}]
|
||||||
|
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])]
|
||||||
|
;; (app.common.pprint/pprint (:app.rpc/methods *system*))
|
||||||
|
(try-on! (method-fn (dissoc data ::type)))))
|
||||||
|
|
||||||
(defn mutation!
|
(defn mutation!
|
||||||
[{:keys [::type] :as data}]
|
[{:keys [::type] :as data}]
|
||||||
(let [method-fn (get-in *system* [:app.rpc/rpc :methods :mutation type])]
|
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
|
||||||
(try-on!
|
(try-on! (method-fn (dissoc data ::type)))))
|
||||||
(method-fn (dissoc data ::type)))))
|
|
||||||
|
|
||||||
(defn query!
|
(defn query!
|
||||||
[{:keys [::type] :as data}]
|
[{:keys [::type] :as data}]
|
||||||
(let [method-fn (get-in *system* [:app.rpc/rpc :methods :query type])]
|
(let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
|
||||||
(try-on!
|
(try-on! (method-fn (dissoc data ::type)))))
|
||||||
(method-fn (dissoc data ::type)))))
|
|
||||||
|
|
||||||
;; --- UTILS
|
;; --- UTILS
|
||||||
|
|
||||||
|
|
|
@ -97,4 +97,3 @@ PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com
|
||||||
# PENPOT_LDAP_ATTRS_USERNAME=uid
|
# PENPOT_LDAP_ATTRS_USERNAME=uid
|
||||||
# PENPOT_LDAP_ATTRS_EMAIL=mail
|
# PENPOT_LDAP_ATTRS_EMAIL=mail
|
||||||
# PENPOT_LDAP_ATTRS_FULLNAME=cn
|
# PENPOT_LDAP_ATTRS_FULLNAME=cn
|
||||||
# PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
|
|
||||||
|
|
|
@ -4,69 +4,10 @@ log() {
|
||||||
echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*"
|
echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
## App Frontend config
|
## App Frontend config
|
||||||
#########################################
|
#########################################
|
||||||
|
|
||||||
update_google_client_id() {
|
|
||||||
if [ -n "$PENPOT_GOOGLE_CLIENT_ID" ]; then
|
|
||||||
log "Updating Google Client Id: $PENPOT_GOOGLE_CLIENT_ID"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotGoogleClientID = \".*\";|var penpotGoogleClientID = \"$PENPOT_GOOGLE_CLIENT_ID\";|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update_gitlab_client_id() {
|
|
||||||
if [ -n "$PENPOT_GITLAB_CLIENT_ID" ]; then
|
|
||||||
log "Updating GitLab Client Id: $PENPOT_GITLAB_CLIENT_ID"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotGitlabClientID = \".*\";|var penpotGitlabClientID = \"$PENPOT_GITLAB_CLIENT_ID\";|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update_github_client_id() {
|
|
||||||
if [ -n "$PENPOT_GITHUB_CLIENT_ID" ]; then
|
|
||||||
log "Updating GitHub Client Id: $PENPOT_GITHUB_CLIENT_ID"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotGithubClientID = \".*\";|var penpotGithubClientID = \"$PENPOT_GITHUB_CLIENT_ID\";|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_oidc_client_id() {
|
|
||||||
if [ -n "$PENPOT_OIDC_CLIENT_ID" ]; then
|
|
||||||
log "Updating Oidc Client Id: $PENPOT_OIDC_CLIENT_ID"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotOIDCClientID = \".*\";|var penpotOIDCClientID = \"$PENPOT_OIDC_CLIENT_ID\";|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPRECATED
|
|
||||||
update_login_with_ldap() {
|
|
||||||
if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then
|
|
||||||
log "Updating Login with LDAP: $PENPOT_LOGIN_WITH_LDAP"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotLoginWithLDAP = .*;|var penpotLoginWithLDAP = $PENPOT_LOGIN_WITH_LDAP;|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPRECATED
|
|
||||||
update_registration_enabled() {
|
|
||||||
if [ -n "$PENPOT_REGISTRATION_ENABLED" ]; then
|
|
||||||
log "Updating Registration Enabled: $PENPOT_REGISTRATION_ENABLED"
|
|
||||||
sed -i \
|
|
||||||
-e "s|^//var penpotRegistrationEnabled = .*;|var penpotRegistrationEnabled = $PENPOT_REGISTRATION_ENABLED;|g" \
|
|
||||||
"$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_flags() {
|
update_flags() {
|
||||||
if [ -n "$PENPOT_FLAGS" ]; then
|
if [ -n "$PENPOT_FLAGS" ]; then
|
||||||
sed -i \
|
sed -i \
|
||||||
|
@ -75,11 +16,5 @@ update_flags() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
update_google_client_id /var/www/app/js/config.js
|
|
||||||
update_gitlab_client_id /var/www/app/js/config.js
|
|
||||||
update_github_client_id /var/www/app/js/config.js
|
|
||||||
update_oidc_client_id /var/www/app/js/config.js
|
|
||||||
update_login_with_ldap /var/www/app/js/config.js
|
|
||||||
update_registration_enabled /var/www/app/js/config.js
|
|
||||||
update_flags /var/www/app/js/config.js
|
update_flags /var/www/app/js/config.js
|
||||||
exec "$@";
|
exec "$@";
|
||||||
|
|
|
@ -80,10 +80,6 @@
|
||||||
(def default-theme "default")
|
(def default-theme "default")
|
||||||
(def default-language "en")
|
(def default-language "en")
|
||||||
|
|
||||||
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
|
|
||||||
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
|
|
||||||
(def github-client-id (obj/get global "penpotGithubClientID" nil))
|
|
||||||
(def oidc-client-id (obj/get global "penpotOIDCClientID" nil))
|
|
||||||
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||||
(def translations (obj/get global "penpotTranslations"))
|
(def translations (obj/get global "penpotTranslations"))
|
||||||
(def themes (obj/get global "penpotThemes"))
|
(def themes (obj/get global "penpotThemes"))
|
||||||
|
@ -100,14 +96,6 @@
|
||||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
|
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
|
||||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
|
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
|
||||||
|
|
||||||
;; maintain for backward compatibility
|
|
||||||
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
|
|
||||||
registration (obj/get global "penpotRegistrationEnabled" true)]
|
|
||||||
(when login-with-ldap
|
|
||||||
(swap! flags conj :login-with-ldap))
|
|
||||||
(when (false? registration)
|
|
||||||
(swap! flags disj :registration)))
|
|
||||||
|
|
||||||
(defn get-public-uri
|
(defn get-public-uri
|
||||||
[]
|
[]
|
||||||
(let [uri (u/uri (or (obj/get global "penpotPublicURI")
|
(let [uri (u/uri (or (obj/get global "penpotPublicURI")
|
||||||
|
|
|
@ -145,7 +145,7 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(when (= status "ended")
|
(when (= status "ended")
|
||||||
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id})
|
(->> (rp/command! :export {:cmd :get-resource :blob? true :id resource-id})
|
||||||
(rx/delay 500)
|
(rx/delay 500)
|
||||||
(rx/map #(dom/trigger-download filename %)))))))
|
(rx/map #(dom/trigger-download filename %)))))))
|
||||||
|
|
||||||
|
@ -165,9 +165,9 @@
|
||||||
:wait true}]
|
:wait true}]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of ::dwp/force-persist)
|
(rx/of ::dwp/force-persist)
|
||||||
(->> (rp/query! :exporter params)
|
(->> (rp/command! :export params)
|
||||||
(rx/mapcat (fn [{:keys [id filename]}]
|
(rx/mapcat (fn [{:keys [id filename]}]
|
||||||
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id})
|
(->> (rp/command! :export {:cmd :get-resource :blob? true :id id})
|
||||||
(rx/map (fn [data]
|
(rx/map (fn [data]
|
||||||
(dom/trigger-download filename data)
|
(dom/trigger-download filename data)
|
||||||
(clear-export-state uuid/zero))))))
|
(clear-export-state uuid/zero))))))
|
||||||
|
@ -213,7 +213,7 @@
|
||||||
|
|
||||||
;; Launch the exportation process and stores the resource id
|
;; Launch the exportation process and stores the resource id
|
||||||
;; locally.
|
;; locally.
|
||||||
(->> (rp/query! :exporter params)
|
(->> (rp/command! :export params)
|
||||||
(rx/map (fn [{:keys [id] :as resource}]
|
(rx/map (fn [{:keys [id] :as resource}]
|
||||||
(vreset! resource-id id)
|
(vreset! resource-id id)
|
||||||
(initialize-export-status exports cmd resource))))
|
(initialize-export-status exports cmd resource))))
|
||||||
|
|
|
@ -206,7 +206,7 @@
|
||||||
;; the returned profile is an NOT authenticated profile, we
|
;; the returned profile is an NOT authenticated profile, we
|
||||||
;; proceed to logout and show an error message.
|
;; proceed to logout and show an error message.
|
||||||
|
|
||||||
(->> (rp/mutation :login (d/without-nils params))
|
(->> (rp/command :login-with-password (d/without-nils params))
|
||||||
(rx/merge-map (fn [data]
|
(rx/merge-map (fn [data]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/of (fetch-profile))
|
(rx/of (fetch-profile))
|
||||||
|
@ -292,7 +292,7 @@
|
||||||
(ptk/reify ::logout
|
(ptk/reify ::logout
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(->> (rp/mutation :logout)
|
(->> (rp/command :logout)
|
||||||
(rx/delay-at-least 300)
|
(rx/delay-at-least 300)
|
||||||
(rx/catch (constantly (rx/of 1)))
|
(rx/catch (constantly (rx/of 1)))
|
||||||
(rx/map #(logged-out params)))))))
|
(rx/map #(logged-out params)))))))
|
||||||
|
@ -494,7 +494,7 @@
|
||||||
:or {on-error rx/throw
|
:or {on-error rx/throw
|
||||||
on-success identity}} (meta data)]
|
on-success identity}} (meta data)]
|
||||||
|
|
||||||
(->> (rp/mutation :request-profile-recovery data)
|
(->> (rp/command :request-profile-recovery data)
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
@ -513,7 +513,7 @@
|
||||||
(let [{:keys [on-error on-success]
|
(let [{:keys [on-error on-success]
|
||||||
:or {on-error rx/throw
|
:or {on-error rx/throw
|
||||||
on-success identity}} (meta data)]
|
on-success identity}} (meta data)]
|
||||||
(->> (rp/mutation :recover-profile data)
|
(->> (rp/command :recover-profile data)
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
@ -524,7 +524,7 @@
|
||||||
(ptk/reify ::create-demo-profile
|
(ptk/reify ::create-demo-profile
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ _ _]
|
(watch [_ _ _]
|
||||||
(->> (rp/mutation :create-demo-profile {})
|
(->> (rp/command :create-demo-profile {})
|
||||||
(rx/map login)))))
|
(rx/map login)))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,22 @@
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response)))
|
(rx/mapcat handle-response)))
|
||||||
|
|
||||||
|
(defn- send-command!
|
||||||
|
"A simple helper for a common case of sending and receiving transit
|
||||||
|
data to the penpot mutation api."
|
||||||
|
[id params]
|
||||||
|
(->> (http/send! {:method :post
|
||||||
|
:uri (u/join base-uri "api/rpc/command/" (name id))
|
||||||
|
:credentials "include"
|
||||||
|
:body (http/transit-data params)})
|
||||||
|
(rx/map http/conditional-decode-transit)
|
||||||
|
(rx/mapcat handle-response)))
|
||||||
|
|
||||||
(defn- dispatch [& args] (first args))
|
(defn- dispatch [& args] (first args))
|
||||||
|
|
||||||
(defmulti query dispatch)
|
(defmulti query dispatch)
|
||||||
(defmulti mutation dispatch)
|
(defmulti mutation dispatch)
|
||||||
|
(defmulti command dispatch)
|
||||||
|
|
||||||
(defmethod query :default
|
(defmethod query :default
|
||||||
[id params]
|
[id params]
|
||||||
|
@ -90,6 +102,10 @@
|
||||||
[id params]
|
[id params]
|
||||||
(send-mutation! id params))
|
(send-mutation! id params))
|
||||||
|
|
||||||
|
(defmethod command :default
|
||||||
|
[id params]
|
||||||
|
(send-command! id params))
|
||||||
|
|
||||||
(defn query!
|
(defn query!
|
||||||
([id] (query id {}))
|
([id] (query id {}))
|
||||||
([id params] (query id params)))
|
([id params] (query id params)))
|
||||||
|
@ -98,7 +114,11 @@
|
||||||
([id] (mutation id {}))
|
([id] (mutation id {}))
|
||||||
([id params] (mutation id params)))
|
([id params] (mutation id params)))
|
||||||
|
|
||||||
(defmethod mutation :login-with-oauth
|
(defn command!
|
||||||
|
([id] (command id {}))
|
||||||
|
([id params] (command id params)))
|
||||||
|
|
||||||
|
(defmethod command :login-with-oidc
|
||||||
[_ {:keys [provider] :as params}]
|
[_ {:keys [provider] :as params}]
|
||||||
(let [uri (u/join base-uri "api/auth/oauth/" (d/name provider))
|
(let [uri (u/join base-uri "api/auth/oauth/" (d/name provider))
|
||||||
params (dissoc params :provider)]
|
params (dissoc params :provider)]
|
||||||
|
@ -109,7 +129,7 @@
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response))))
|
(rx/mapcat handle-response))))
|
||||||
|
|
||||||
(defmethod mutation :send-feedback
|
(defmethod command :send-feedback
|
||||||
[_ params]
|
[_ params]
|
||||||
(->> (http/send! {:method :post
|
(->> (http/send! {:method :post
|
||||||
:uri (u/join base-uri "api/feedback")
|
:uri (u/join base-uri "api/feedback")
|
||||||
|
@ -128,7 +148,7 @@
|
||||||
(rx/map http/conditional-decode-transit)
|
(rx/map http/conditional-decode-transit)
|
||||||
(rx/mapcat handle-response)))
|
(rx/mapcat handle-response)))
|
||||||
|
|
||||||
(defmethod query :exporter
|
(defmethod command :export
|
||||||
[_ params]
|
[_ params]
|
||||||
(let [default {:wait false :blob? false}]
|
(let [default {:wait false :blob? false}]
|
||||||
(send-export (merge default params))))
|
(send-export (merge default params))))
|
||||||
|
|
|
@ -23,10 +23,11 @@
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
(def show-alt-login-buttons?
|
(def show-alt-login-buttons?
|
||||||
(or cf/google-client-id
|
(some (partial contains? @cf/flags)
|
||||||
cf/gitlab-client-id
|
[:login-with-google
|
||||||
cf/github-client-id
|
:login-with-github
|
||||||
cf/oidc-client-id))
|
:login-with-gitlab
|
||||||
|
:login-with-oidc]))
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
|
@ -36,19 +37,27 @@
|
||||||
(s/keys :req-un [::email ::password]
|
(s/keys :req-un [::email ::password]
|
||||||
:opt-un [::invitation-token]))
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
(defn- login-with-oauth
|
(defn- login-with-oidc
|
||||||
[event provider params]
|
[event provider params]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(->> (rp/mutation! :login-with-oauth (assoc params :provider provider))
|
(->> (rp/command! :login-with-oidc (assoc params :provider provider))
|
||||||
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
|
||||||
(.replace js/location redirect-uri)))))
|
(.replace js/location redirect-uri))
|
||||||
|
(fn [{:keys [type code] :as error}]
|
||||||
|
(cond
|
||||||
|
(and (= type :restriction)
|
||||||
|
(= code :provider-not-configured))
|
||||||
|
(st/emit! (dm/error (tr "errors.auth-provider-not-configured")))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(st/emit! (dm/error (tr "errors.generic"))))))))
|
||||||
|
|
||||||
(defn- login-with-ldap
|
(defn- login-with-ldap
|
||||||
[event params]
|
[event params]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [{:keys [on-error]} (meta params)]
|
(let [{:keys [on-error]} (meta params)]
|
||||||
(->> (rp/mutation! :login-with-ldap params)
|
(->> (rp/command! :login-with-ldap params)
|
||||||
(rx/subs (fn [profile]
|
(rx/subs (fn [profile]
|
||||||
(if-let [token (:invitation-token profile)]
|
(if-let [token (:invitation-token profile)]
|
||||||
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
|
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
|
||||||
|
@ -56,11 +65,15 @@
|
||||||
(fn [{:keys [type code] :as error}]
|
(fn [{:keys [type code] :as error}]
|
||||||
(cond
|
(cond
|
||||||
(and (= type :restriction)
|
(and (= type :restriction)
|
||||||
(= code :ldap-disabled))
|
(= code :ldap-not-initialized))
|
||||||
(st/emit! (dm/error (tr "errors.ldap-disabled")))
|
(st/emit! (dm/error (tr "errors.ldap-disabled")))
|
||||||
|
|
||||||
(fn? on-error)
|
(fn? on-error)
|
||||||
(on-error error)))))))
|
(on-error error)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(st/emit! (dm/error (tr "errors.generic")))))))))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc login-form
|
(mf/defc login-form
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
|
@ -134,35 +147,35 @@
|
||||||
(mf/defc login-buttons
|
(mf/defc login-buttons
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
[:div.auth-buttons
|
[:div.auth-buttons
|
||||||
(when cf/google-client-id
|
(when (contains? @cf/flags :login-with-google)
|
||||||
[:a.btn-primary.btn-large.btn-google-auth
|
[:a.btn-primary.btn-large.btn-google-auth
|
||||||
{:on-click #(login-with-oauth % :google params)}
|
{:on-click #(login-with-oidc % :google params)}
|
||||||
[:span.logo i/brand-google]
|
[:span.logo i/brand-google]
|
||||||
(tr "auth.login-with-google-submit")])
|
(tr "auth.login-with-google-submit")])
|
||||||
|
|
||||||
(when cf/github-client-id
|
(when (contains? @cf/flags :login-with-github)
|
||||||
[:a.btn-primary.btn-large.btn-github-auth
|
[:a.btn-primary.btn-large.btn-github-auth
|
||||||
{:on-click #(login-with-oauth % :github params)}
|
{:on-click #(login-with-oidc % :github params)}
|
||||||
[:span.logo i/brand-github]
|
[:span.logo i/brand-github]
|
||||||
(tr "auth.login-with-github-submit")])
|
(tr "auth.login-with-github-submit")])
|
||||||
|
|
||||||
(when cf/gitlab-client-id
|
(when (contains? @cf/flags :login-with-gitlab)
|
||||||
[:a.btn-primary.btn-large.btn-gitlab-auth
|
[:a.btn-primary.btn-large.btn-gitlab-auth
|
||||||
{:on-click #(login-with-oauth % :gitlab params)}
|
{:on-click #(login-with-oidc % :gitlab params)}
|
||||||
[:span.logo i/brand-gitlab]
|
[:span.logo i/brand-gitlab]
|
||||||
(tr "auth.login-with-gitlab-submit")])
|
(tr "auth.login-with-gitlab-submit")])
|
||||||
|
|
||||||
(when cf/oidc-client-id
|
(when (contains? @cf/flags :login-with-oidc)
|
||||||
[:a.btn-primary.btn-large.btn-github-auth
|
[:a.btn-primary.btn-large.btn-github-auth
|
||||||
{:on-click #(login-with-oauth % :oidc params)}
|
{:on-click #(login-with-oidc % :oidc params)}
|
||||||
[:span.logo i/brand-openid]
|
[:span.logo i/brand-openid]
|
||||||
(tr "auth.login-with-oidc-submit")])])
|
(tr "auth.login-with-oidc-submit")])])
|
||||||
|
|
||||||
(mf/defc login-button-oidc
|
(mf/defc login-button-oidc
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
(when cf/oidc-client-id
|
(when (contains? @cf/flags :login-with-oidc)
|
||||||
[:div.link-entry.link-oidc
|
[:div.link-entry.link-oidc
|
||||||
[:a {:on-click #(login-with-oauth % :oidc params)}
|
[:a {:on-click #(login-with-oidc % :oidc params)}
|
||||||
(tr "auth.login-with-oidc-submit")]]))
|
(tr "auth.login-with-oidc-submit")]]))
|
||||||
|
|
||||||
(mf/defc login-page
|
(mf/defc login-page
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
(fn [form _event]
|
(fn [form _event]
|
||||||
(reset! submitted? true)
|
(reset! submitted? true)
|
||||||
(let [cdata (:clean-data @form)]
|
(let [cdata (:clean-data @form)]
|
||||||
(->> (rp/mutation :prepare-register-profile cdata)
|
(->> (rp/command :prepare-register-profile cdata)
|
||||||
(rx/map #(merge % params))
|
(rx/map #(merge % params))
|
||||||
(rx/finalize #(reset! submitted? false))
|
(rx/finalize #(reset! submitted? false))
|
||||||
(rx/subs (partial handle-prepare-register-success form)
|
(rx/subs (partial handle-prepare-register-success form)
|
||||||
|
@ -207,7 +207,7 @@
|
||||||
(fn [form _event]
|
(fn [form _event]
|
||||||
(reset! submitted? true)
|
(reset! submitted? true)
|
||||||
(let [params (:clean-data @form)]
|
(let [params (:clean-data @form)]
|
||||||
(->> (rp/mutation :register-profile params)
|
(->> (rp/command :register-profile params)
|
||||||
(rx/finalize #(reset! submitted? false))
|
(rx/finalize #(reset! submitted? false))
|
||||||
(rx/subs (partial handle-register-success form)
|
(rx/subs (partial handle-register-success form)
|
||||||
(partial handle-register-error form))))))
|
(partial handle-register-error form))))))
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
(fn [form _]
|
(fn [form _]
|
||||||
(reset! loading true)
|
(reset! loading true)
|
||||||
(let [data (:clean-data @form)]
|
(let [data (:clean-data @form)]
|
||||||
(->> (rp/mutation! :send-feedback data)
|
(->> (rp/command! :send-feedback data)
|
||||||
(rx/subs on-succes on-error)))))]
|
(rx/subs on-succes on-error)))))]
|
||||||
|
|
||||||
[:& fm/form {:class "feedback-form"
|
[:& fm/form {:class "feedback-form"
|
||||||
|
|
|
@ -699,6 +699,10 @@ msgstr "This invite might be canceled or may be expired."
|
||||||
msgid "errors.ldap-disabled"
|
msgid "errors.ldap-disabled"
|
||||||
msgstr "LDAP authentication is disabled."
|
msgstr "LDAP authentication is disabled."
|
||||||
|
|
||||||
|
#: src/app/main/ui/auth/login.cljs
|
||||||
|
msgid "errors.auth-provider-not-configured"
|
||||||
|
msgstr "Authentication provider not configured."
|
||||||
|
|
||||||
msgid "errors.media-format-unsupported"
|
msgid "errors.media-format-unsupported"
|
||||||
msgstr "The image format is not supported (must be svg, jpg or png)."
|
msgstr "The image format is not supported (must be svg, jpg or png)."
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue