-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Auth/Security in pathom #4
Comments
I am still iterating on a solution, but I currently am enjoying combining pedestal interceptors with pathom-connect/transform. (:require
[dv.pathom :as pa]
[io.pedestal.interceptor.helpers :as ih])
(defn auth-user-interceptor
"Takes a msg to set as the server error message if there is no user in the pathom environment."
([]
(auth-user-interceptor "You must be logged in to perform this action."))
([msg]
(fn auth-user-interceptor*
;;; the map "in" is the pedestal environment/context and contains:
;; :opts - The definition-time pathom resolver or mutation map of options.
;; :env - The pathom connect environment for the resolver or mutation passed by pathom at request-time.
;; :params - The params to the resolver or the mutation.
[{env :env :as in}]
(let [{:keys [current-user]} env]
(cond-> in (not current-user)
(pa/assoc-response {:server/error? true :server/error-msg msg}))))))
(pc/defmutation create-thing-mutation
[env props]
{::pc/transform (pa/interceptors->pathom-transform
[(ih/before (user/auth-user-interceptor "You must be logged in to create a thing."))])}
;; usual body of mutate here
) the current user is assoced into the pathom environment from a session, like in this example: I like that this is flexible enough to support arbitrary logic; for example, for resolvers you could add an "after" interceptor that inspects the return value of the resolver and checks that only certain keys are allowed given some account information or user role information that are passed as constructor arguments to the interceptor. It also lets the "body" of the mutation/resolver be a straight line of logic because validation and auth are taken care of in the interceptors. |
Here's my unsophisticated approach, inspired by some code from @dvingo. The basic strategy is to keep some permissions information available in First some helper functions: (defn server-error [msg]
{:server/message msg
:server/error? true})
;; assume just two simple security levels: :admin and :user. If you want something more
;; fine-grained, this is the function to change
(defn authorized
"Given a resolver's environment, say whether it is authorized for a given level"
[env level]
(let [{:keys [session/valid? user/admin?]} (get-in env [:ring/request :session])]
(or (nil? level)
(and (= level :admin) admin?)
(and (= level :user) valid?)))) The transform maker, which is used to make a transform for each security level: (defn make-auth-transform [level]
"Make a transform for a Pathom resolver that checks whether the user has sufficient
permissions for a given operation. mutate? is a bool that indicates whether this is
for a resolver or a mutation, and level is one of :admin or :user indicating required
permissions."
(fn auth-transform [{::pc/keys [mutate resolve] :as outer-env}]
(let [pathom-action (if mutate mutate resolve)
pathom-action-kwd (if mutate ::pc/mutate ::pc/resolve)]
(-> outer-env
(assoc
pathom-action-kwd
(fn [env params]
(if (authorized env level)
(pathom-action env params)
(server-error (str "Unauthorized pathom action: session "
(get-in env [:ring/request :session])
" does not satisfy authorization requirement "
level)))))))))
(def admin-required (make-auth-transform :admin))
(def user-required (make-auth-transform :user)) Now all you need to do is use (pc/defresolver my-resolver [_ _]
{::pc/output [...]
::pc/transform admin-required}
...) If the user has insufficient permissions, If you wanted to make permissions more sophisticated, you could straightforwardly just modify the |
Here is an approach that I think will work: make all resolvers that need a valid auth take an additional input (say :app/authed), and make a global resolver that outputs {:app/authed true} or {} based on the session data stored in env (like |
I'm implementing this:
(pc/defresolver authed? [env input]
{::private #{:app.user/authed?}
::pc/output [:app.user/authed?]}
{:app.user/authed? ...})
(pc/defresolver password [env input]
{::pc/input #{:app.user/email
:app.user/authed?}
::pc/output [:app.user/password]}
{:app.user/password ...}) (defn private-keys
"Returns a list of keys that should not be used as input on queries"
[env]
(set (mapcat ::private (vals (::pc/index-resolvers (::pc/indexes env))))))
(defn remove-input-idents
[env query idents]
(let [idents (set idents)]
(->> query
eql/query->ast
(eql/transduce-children (fn [{:keys [dispatch-key params]
:as node}]
(cond (contains? idents dispatch-key)
(throw (ex-info (str "You can't use '" dispatch-key "' as input")
{}))
;; I use placeholders to provide input, as in pathom3
(p/placeholder-key? env dispatch-key) (assoc node :params (apply dissoc params idents))
(contains? params :pathom/context) (assoc node :params (assoc params (apply dissoc (:pathom/context params) idents)))
:else node)))
eql/ast->query))) |
Related to #3
This thead is dedicated to anyone describe how handle security.
Some methods that I use:
1- Filter your query
given a session, you can build a set of attributes that the user can reach
2- Let it be.
you your datasource (REST, DB, GQL) has some auth method, let it handle auth for you
3- Env basead
Just check if your env is authorized to call this handler
Maybe it can be handled by
pc/transform
Maybe you can transform programmatically every resolve in your index
The text was updated successfully, but these errors were encountered: