-
Notifications
You must be signed in to change notification settings - Fork 123
proposal: get user's roles based on current session #80
Comments
What you're describing are effectively ACLs, for which Friend doesn't directly provide any support (largely because the particulars are often so different from application to application that I've not yet thought it reasonable to add an implementation of them). You can certainly build this functionality by e.g. putting all of your authorization checks into one piece of middleware that fronts the HTTP resources that need this fine-grained access control. This middleware can then throw authorization failure exceptions (see I hope that helps? |
Yeah, that sounds like it makes sense. Here are the helper functions I came up with: (ns paddleguru.util.friend
"Helper utilities for the friend authorization and authentication
library, by cemerick."
(:refer-clojure :exclude (identity))
(:require [paddleguru.models.user :as user]
[cemerick.friend :as friend :refer [identity
current-authentication
throw-unauthorized]]))
;; ## Route Building Helpers
;;
;; The following functions make it easy to build routes with custom
;; friend validations.
(defn roles
"Returns an authorization function that checks if the authenticated
user has the specified roles. (This is the usual friend behavior.)"
[roles]
(fn [id]
(friend/authorized? roles id)))
(defn unauthorized!
"Throws the proper unauthorized! slingshot error if authentication
fails. This error is picked up upstream by friend's middleware."
[handler req]
(throw-unauthorized (identity req)
{::wrapped-handler handler}))
(defn wrap-authorize
"Custom ring middleware to help with friend authorization. Takes a
handler and a predicate of one argument (the request), and only
allows access through to the handler if the predicate returns
true. If the predicate fails, Friend will throw a validation error
internally.
the authorized? function takes the request as its argument. If you
want to get the identity out, use the `identity` function inside of
cemerick.friend, or current-authentication. Both are helpful."
[handler authorized?]
(fn [request]
(if (authorized? request)
(handler request)
(unauthorized! handler request))))
(defn wrap-authenticated
"Takes a handler and wraps it with a check that the current user is
authenticated."
[handler]
(fn [req]
(if (identity req)
(handler req)
(unauthorized! handler req))))
(defn super-admin
"Wraps the supplied handler (returned by the supplied no-arg func)
in a super-admin? authorization check."
[f]
(-> (constantly (f))
(wrap-authorize (roles #{::user/super-admin}))))
(def admins-only
"Friend predicate that returns true if the request is pegged to a regatta
admin, false otherwise."
(fn [req]
(user/regatta-admin-session?
(:params req)
(:username (friend/current-authentication req)))))
(defn admin-route
"Takes a handler and wraps it in an authorization layer that only
lets through requests from verified admins."
[handler]
(fn [req]
(wrap-authorize handler admins-only)))
;; ## Workflow Helpers
;;
;; Here are some nice functions for developing custom authentication
;; workflows using friend.
(defn on-success
"When a workflow is run, it returns either an authorized user
document OR a ring response noting that the auth failed. If a login
isn't actually happening, Friend authenticates the user via their
session.
`on-success` returns a new workflow that runs the old workflow, then
calls the supplied function if the workflow is applied
successfully."
[workflow f]
(fn [& args]
(when-let [res (apply workflow args)]
(when (friend/auth? res)
(f))
res))) |
And then here's my integration with Liberator. This extends liberator to allow for base behaviors in the map. Along with that I've got a base Friend resource that inserts the proper checks into the (ns paddleguru.util.liberator
"Helpful extensions to liberator."
(:require [compojure.route :as route]
[liberator.core :as l]
[liberator.representation :as rep]
[paddleguru.models.user :as user]
[paddleguru.util.friend :as friend]
[paddleguru.views.shared :as shared]))
;; ## Basic Helpers
;;
;; These could easily make their way back into liberator.
(defn flatten-resource
"Accepts a map (or a sequence, which gets turned into a map) of
resources; if the map contains the key :base, the kv pairs from THAT
map are merged in to the current map. If there are clashes, the new
replaces the old by default.
Combat this by supplying a :merge-with function in the map. This
binary function will be used to resolve clashes using
\"merge-with\"."
[kvs]
(let [m (if (map? kvs)
kvs
(apply hash-map kvs))
trim #(dissoc % :base :merge-with)]
(if-let [base (:base m)]
(let [combined (combine base)
trimmed (trim m)]
(if-let [merge-fn (:merge-with m)]
(merge-with merge-fn combined trimmed)
(merge combined trimmed)))
(trim m))))
(defn resource
"Functional version of defresource. Takes any number of kv pairs,
returns a resource function."
[& kvs]
(fn [request]
(l/run-resource request
(flatten-resource kvs))))
(defmacro defresource
"The same as liberator's defresource, except it allows for a base
resource and a merge-with function."
[name & kvs]
(if (vector? (first kvs))
(let [[args & kvs] kvs]
`(defn ~name [~@args]
(resource ~@kvs)))
`(defn ~name [req#]
(l/run-resource req# ~(flatten-resource kvs)))))
;; ## Friend Integration
(def guru-base
"Base for all guru resources."
(let [not-found (comp rep/ring-response
(route/not-found shared/status-404-page))]
{:handle-not-acceptable not-found
:handle-not-found not-found}))
(def friend-resource
"Base resource that will handle authentication via friend's
mechanisms. Provide an authorization function and you'll be good to
go."
{:base guru-base
:handle-unauthorized
(fn [req]
(friend/unauthorized! (-> req :resource :allowed?)
req))})
;; ## Base Resources
;;
;; These functions and vars provide base resources that make it easier
;; to define new liberator resources within the paddleguru codebase.
(defn friend-auth
"Returns a base resource that authenticates using the supplied
auth-fn. Authorization failure will trigger Friend's default
unauthorized response."
[auth-fn] {:base friend-resource
:authorized? auth-fn})
(defn with-base
"Merges the supplied base map into the supplied resource
map (knocking out any existing base)"
[base resource]
(assoc resource :base base))
(defn role-auth
"Returns a base resource that authenticates users against the
supplied set of roles."
[role-input]
{:base friend-resource
:authorized? (friend/roles role-input)})
(def regatta-admin-resource
"Base resource that guarantees the supplied resource will be
authenticated against regatta admins only."
(friend-auth friend/admins-only))
(def super-admin-resource
"Base resource that guarantees the supplied resource will be
authenticated against super admins only."
(role-auth #{::user/super-admin})) |
I don't see where you're actually going to the point of setting up ACLs (which, maybe you don't actually need?). In any case, these look like a sane bunch of helpers. Also, thanks for paving through what's necessary for liberator-friend interop. Looking forward to your friend-liberator library. :-D |
This is the spot: (def admins-only
"Friend predicate that returns true if the request is pegged to a regatta
admin, false otherwise."
(fn [req]
(user/regatta-admin-session?
(:params req)
(:username (friend/current-authentication req)))))
(defn admin-route
"Takes a handler and wraps it in an authorization layer that only
lets through requests from verified admins."
[handler]
(fn [req]
(wrap-authorize handler admins-only))) Determine based on the route and current auth if the user is visiting a regatta they're administering, and authorize (or authenticate) accordingly. |
I find myself in need of something similar. I want to have multiple projects, where each user can be a member or an admin of the project. May I ask how you implemented your |
Sure, it's nothing fancy... I just use my regatta title to look up the regatta in the DB and check some attributes. Here's a version using schema: (s/defn regatta-admin? :- s/Bool
[title :- regatta/Title
username :- (s/maybe UserName)]
(let [admin-set (set (:admins (regatta/get-regatta title)))]
(contains? admin-set username)))
(s/defn regatta-admin-session? :- s/Bool
"Returns true if the supplied params reference a regatta admin,
false otherwise."
([params]
(regatta-admin-session? params (logged-in-user)))
([params user :- UserName]
(regatta-admin? (:regatta-title params) user))) |
It's quite simple alright. I think I was over-thinking it. Thank you very much! |
Hey Chas,
I'm trying to figure out how to write an app (paddleguru.com) that defines roles based on the particular resource being requested. I might be the admin of race A, but not race B.
I think the easiest way to make this work would be to allow
:roles
to be a function of the request. Alternatively, with the current code, I can bind the request dynamically and access it from inside the no-arg function.Do you have a suggestion for a different way to tackle this case?
The text was updated successfully, but these errors were encountered: