Skip to content

Commit

Permalink
Auto-mapping of Okta users and roles (#1293)
Browse files Browse the repository at this point in the history
* update config doc comment

* support event-context in okta cookie-based requests

* create local identity for okta user

* allow disabling of separate session cache

* Optional redis password for auth (#1290)

Co-authored-by: Muazzam Ali <muazzam_ali@live.com>

* add rbac for orders

* fix fn args

* logout redirect as a config

* make auto-rbac entries before registering auth-resolver

* support roles as okta-attribute

* update config spec

* handle auto-added groups attribute

* update notes on role-claim config

* support default-role assignment

---------

Co-authored-by: Muazzam Kazmi <118562685+muazzam0x48@users.noreply.github.com>
Co-authored-by: Muazzam Ali <muazzam_ali@live.com>
  • Loading branch information
3 people committed Apr 9, 2024
1 parent b1daa18 commit 6d79c3e
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 56 deletions.
22 changes: 19 additions & 3 deletions example/auth/order.fractl
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@
(entity
:Order
{:order_id {:type :String :guid true}
:customer_id :String
:email :Email
:order_qty :Int
:order_amount :Decimal
:tnc_agreement {:type :Boolean :default true}
:submission_date :Now})
:submission_date :Now
:rbac [{:roles ["order-users"] :allow [:create]}]})

(relationship
:UserOrder
{:meta {:between [:Fractl.Kernel.Identity/User :Acme/Order]}
:rbac [{:roles ["order-users"] :allow [:create]}]})

(dataflow
:CreateOrder
{:Order
{:order_id :CreateOrder.order_id
:order_qty :CreateOrder.order_qty
:order_amount :CreateOrder.order_amount}
:as :O}
{:UserOrder
{:User :CreateOrder.EventContext.User
:Order :O.order_id}}
:O)
6 changes: 2 additions & 4 deletions example/auth/src/auth/handler.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
[:form {:action "/api/Acme/Order" :method :post}
[:table
[:tr [:td "Order Id"] [:td [:input {:type "text" :id "order_id" :name "order_id"}]]]
[:tr [:td "Customer Id"] [:td [:input {:type "text" :id "customer_id" :name "customer_id"}]]]
[:tr [:td "Email"] [:td [:input {:type "text" :id "email" :name "email"}]]]
[:tr [:td "Qty"] [:td [:input {:type "text" :id "order_qty" :name "order_qty"}]]]
[:tr [:td "Amount"] [:td [:input {:type "text" :id "order_amount" :name "order_amount"}]]]
[:tr [:td] [:td]]
Expand Down Expand Up @@ -63,8 +61,8 @@
(POST "/api/Acme/Order" request (let [params (:params request)
attrs (assoc params "order_qty" (read-string (get params "order_qty"))
"order_amount" (read-string (get params "order_amount")))
inst {"Acme/Order" attrs}
url "http://localhost:8000/api/Acme/Order"
inst {"Acme/CreateOrder" attrs}
url "http://localhost:8000/api/Acme/CreateOrder"
result @(http/request {:method :post :url url
:headers {"Content-Type" "application/json"
"Cookie" @sid}
Expand Down
11 changes: 6 additions & 5 deletions src/fractl/auth.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@

(defn- setup-cache-resolver [config]
#?(:clj
(try
(let [resolver (cache/make :auth-cache config)]
(rr/override-resolver [:Fractl.Kernel.Identity/SessionCookie] resolver))
(catch Exception ex
(log/error ex)))))
(when config
(try
(let [resolver (cache/make :auth-cache config)]
(rr/override-resolver [:Fractl.Kernel.Identity/SessionCookie] resolver))
(catch Exception ex
(log/error ex))))))

(defn setup-resolver [config evaluator]
(let [resolver (authn/make :authentication config)
Expand Down
82 changes: 60 additions & 22 deletions src/fractl/auth/okta.clj
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,35 @@
(def ^:private tag :okta)

;; config required for okta/auth:
#_{:authentication
{:service :okta
:superuser-email "<email>"
:client-id "<client-id>"

:domain "<domain>"
:auth-server "<auth-server-name>"
:client-secret "<secret>"
:introspect <boolean> ; if true, call okta introspect api to verify the token, defaults to false
:scope "<string>" ; one or more scopes, separated by space
:authorize-redirect-url "<url>" ; redirect url setting for okta authorize
:client-url "<url>" ; url of the client app that's being authorized
}}
#_{:rbac-enabled true
:authentication {:service :okta
:superuser-email <email>
:domain <okta-domain>
:auth-server <okta-auth-server-name> ; or "default"
:client-id <okta-app-client-id>
:client-secret <okta-app-secret>
:scope "openid offline_access"
:introspect true ; if false token will be verified locally
:authorize-redirect-url "http://localhost:3000/auth/callback"
:client-url "http://localhost:3000/order"
:logout-redirect "http://localhost:3000/bye"

:role-claim :user-roles
;; "user-roles" is the custom attribute set for Okta users.
;; More than one role or group can be specified as a comma-delmited string
;; or as a vector of strings.
;; Okta groups can be automatically set as a role-claim attribute by following
;; these steps:
;; In Okta console, navigate to Security > API. Select your authorization server and go to the Claims tab.
;; Set these values: Name: groups, Include in: Access Token, Value type: Groups,
;; Filter: Select Matches regex and use .* as the value
;; Then, click Create. In this config set `:role-claim` to `:groups`.

:default-role "guest"

;; cache is optional
:cache {:host <REDIS_HOST>
:port <REDIS_PORT>}}}

(defmethod auth/make-client tag [_config]
_config)
Expand All @@ -51,9 +67,9 @@

(defn- verify-and-extract [{domain :domain auth-server :auth-server client-id :client-id} token]
(try
(jwt/verify-and-extract
(get-jwks-url domain auth-server client-id)
token)
(jwt/verify-and-extract
(get-jwks-url domain auth-server client-id)
token)
(catch Exception e
(log/warn e))))

Expand Down Expand Up @@ -229,6 +245,14 @@
{:status :redirect-found :location (first (make-authorize-url auth-config))}))
{:status :redirect-found :location (first (make-authorize-url auth-config))}))

(defn- cleanup-roles [roles default-role]
(let [roles (if (vector? roles)
(vec (filter #(not= "Everyone" %) roles))
roles)]
(if (seq roles)
roles
default-role)))

(defmethod auth/handle-auth-callback tag [{client-url :client-url args :args :as auth-config}]
(let [request (:request args)
current-sid (cookie-to-sid (get-in request [:headers "cookie"]))
Expand All @@ -239,6 +263,12 @@
auth-status (auth/verify-token auth-config [session-id result])
user (:sub auth-status)]
(log/debug (str "auth/handle-auth-callback returning session-cookie " session-id " to " client-url))
(when-not (sess/ensure-local-user
user
(cleanup-roles
(get auth-status (:role-claim auth-config))
(:default-role auth-config)))
(log/warn (str "failed to create local user for " user)))
(if (and user (sess/upsert-user-session user true)
((if current-sid
sess/session-cookie-replace
Expand All @@ -249,9 +279,11 @@
:set-cookie (str "sid=" session-id)}
{:error "failed to create session"})))

(defmethod auth/session-user tag [{req :request cookie :cookie :as all-stuff-map}]
(defmethod auth/session-user tag [{req :request cookie :cookie :as auth-config}]
(if cookie
(let [result (introspect all-stuff-map cookie)
(let [sid (auth/cookie-to-session-id auth-config cookie)
session-data (sess/lookup-session-cookie-user-data sid)
result (auth/verify-token auth-config [sid session-data])
user (:sub result)]
{:email user
:sub user
Expand All @@ -264,8 +296,12 @@
(defmethod auth/session-sub tag [req]
(auth/session-user req))

(defmethod auth/user-logout tag [{domain :domain auth-server :auth-server req :request cookie :cookie}]
(let [redirect-uri (http/url-encode "http://localhost:8000")
(defmethod auth/user-logout tag [{domain :domain
auth-server :auth-server
req :request
cookie :cookie
logout-redirect :logout-redirect}]
(let [redirect-uri (http/url-encode logout-redirect)
id-token (if cookie
(get-in (second cookie) [:authentication-result :id-token])
(jwt/remove-bearer (get (:headers req) "authorization")))
Expand Down Expand Up @@ -299,13 +335,15 @@
(u/throw-ex "auth/change-password not implemented for okta"))

(defmethod auth/create-role tag [_]
(u/throw-ex "auth/create-role not implemented for okta"))
(log/warn "auth/create-role not implemented for okta")
true)

(defmethod auth/delete-role tag [_]
(u/throw-ex "auth/delete-role not implemented for okta"))

(defmethod auth/add-user-to-role tag [_]
(u/throw-ex "auth/add-user-to-role not implemented for okta"))
(log/warn "auth/add-user-to-role not implemented for okta")
true)

(defmethod auth/remove-user-from-role tag [_]
(u/throw-ex "auth/remove-user-from-role not implemented for okta"))
6 changes: 3 additions & 3 deletions src/fractl/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@
(when (or (not (init-schema? config)) (store/init-all-schema store))
(let [resolved-config (run-initconfig config ev)
has-rbac (some #{:rbac} (keys ins))]
(register-resolvers! config ev)
(when (seq (:resolvers resolved-config))
(register-resolvers! resolved-config ev))
(if has-rbac
(lr/finalize-events ev)
(lr/reset-events!))
(register-resolvers! config ev)
(when (seq (:resolvers resolved-config))
(register-resolvers! resolved-config ev))
(when-let [f @on-init-fn]
(f)
(reset! on-init-fn nil))
Expand Down
41 changes: 24 additions & 17 deletions src/fractl/evaluator.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,30 @@
(recur (rest insts) env (concat result rs)))))
result)))

(defn- fire-post-events-for [tag insts]
(doseq [inst insts]
(when-let [[event-name r] (cn/fire-post-event eval-all-dataflows tag inst)]
(when-not (u/safe-ok-result r)
(log/warn r)
(u/throw-ex (str "internal event " event-name " failed.")))))
insts)

(defn fire-post-events [env]
(let [srcs (env/post-event-trigger-sources env)]
(reduce
(fn [env tag]
(if-let [insts (seq (tag srcs))]
(do (fire-post-events-for tag insts)
(def ^:dynamic internal-post-events false)

(defn- fire-post-events-for
([tag is-internal insts]
(binding [internal-post-events is-internal]
(doseq [inst insts]
(when-let [[event-name r] (cn/fire-post-event eval-all-dataflows tag inst)]
(when-not (u/safe-ok-result r)
(log/warn r)
(u/throw-ex (str "internal event " event-name " failed.")))))
insts))
([tag insts] (fire-post-events-for tag nil insts)))

(defn fire-post-events
([env is-internal]
(let [srcs (env/post-event-trigger-sources env)]
(reduce
(fn [env tag]
(if-let [insts (seq (tag srcs))]
(do (fire-post-events-for tag is-internal insts)
(env/assoc-rule-futures env (trigger-rules tag insts)))
env))
env [:create :update :delete])))
env [:create :update :delete])))
([env] (fire-post-events env nil)))

(defn- fire-post-event-for [tag inst]
(fire-post-events-for tag [inst]))
Expand All @@ -116,7 +123,7 @@
(gs/set-active-txn! txn)
(reset! txn-set true))
(try
(let [is-internal (internal-event? event-instance)
(let [is-internal (or (internal-event? event-instance) internal-post-events)
event-instance0 (if is-internal
(dissoc event-instance internal-event-key)
event-instance)
Expand All @@ -141,7 +148,7 @@
(if (and (map? r) (not= :ok (:status r)))
(throw (ex-info "eval failed" {:eval-result r}))
r)))
env0 (fire-post-events (:env result))]
env0 (fire-post-events (:env result) is-internal)]
(assoc result :env env0)))]
(interceptors/eval-intercept env0 event-instance continuation))
(finally (when @txn-set (gs/set-active-txn! nil)))))))
Expand Down
3 changes: 2 additions & 1 deletion src/fractl/http.clj
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@

(defn- assoc-event-context [request auth-config event-instance]
(if auth-config
(let [user (auth/session-user (assoc auth-config :request request))
(let [user (auth/session-user (assoc auth-config :request request
:cookie (get (:headers request) "cookie")))
event-instance (if (cn/an-instance? event-instance)
event-instance
(cn/make-instance event-instance))]
Expand Down
4 changes: 3 additions & 1 deletion src/fractl/resolver/redis.clj
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
(fn [resolver-name config]
(let [host (or (:host config) "localhost")
port (or (:port config) 6379)
conn (JedisPool. host port)
username (:username config)
password (:password config)
conn (JedisPool. host port username password)
handlers (map (fn [[k res]]
[k {:handler (partial (:handler res) conn config)}])
resolver-fns)]
Expand Down
46 changes: 46 additions & 0 deletions src/fractl/user_session.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,49 @@
{:Fractl.Kernel.Identity/Update_SessionCookie
{:Id (normalize-sid sid) :Data {:UserData user-data}}})
user-data)))

(defn- maybe-assign-roles [email user-roles]
(doseq [role (if (string? user-roles)
(s/split user-roles #",")
user-roles)]
(let [role-assignment (str role "-" email)]
(when-not (ev/safe-eval-internal
{:Fractl.Kernel.Rbac/Lookup_Role
{:Name role}})
(when-not (ev/safe-eval-internal
{:Fractl.Kernel.Rbac/Create_Role
{:Instance
{:Fractl.Kernel.Rbac/Role
{:Name role}}}})
(u/throw-ex (str "failed to create role " role))))
(when-not (ev/safe-eval-internal
{:Fractl.Kernel.Rbac/Lookup_RoleAssignment
{:Name role-assignment}})
(when-not (ev/safe-eval-internal
{:Fractl.Kernel.Rbac/Create_RoleAssignment
{:Instance
{:Fractl.Kernel.Rbac/RoleAssignment
{:Name role-assignment :Role role :Assignee email}}}})
(u/throw-ex (str "failed to assign role " role " to " email)))))))

(defn ensure-local-user [email user-roles]
#?(:clj
(try
(let [r (first
(ev/eval-internal
{:Fractl.Kernel.Identity/FindUser
{:Email email}}))]
(if (and (= :ok (:status r)) (seq (:result r)))
(first (:result r))
(let [r (first
(ev/eval-internal
{:Fractl.Kernel.Identity/Create_User
{:Instance
{:Fractl.Kernel.Identity/User
{:Email email}}}}))]
(when (= :ok (:status r))
(:result r)))))
(when user-roles
(maybe-assign-roles email user-roles))
(catch Exception ex
(log/error (str "ensure-local-user failed: " (.getMessage ex)))))))

0 comments on commit 6d79c3e

Please sign in to comment.