Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compojure routes with different middleware

(defroutes user-routes*
  (GET "-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "-endpoint2" [] ("USER ENDPOINT 1")))

(def user-routes
     (-> #'user-routes*
         (wrap-basic-authentication user-auth?)))

(defroutes admin-routes*
  (GET "-endpoint" [] ("ADMIN ENDPOINT")))


(def admin-routes
     (-> #'admin-routes*
         (wrap-basic-authentication admin-auth?)))

(defroutes main-routes
  (ANY "*" [] admin-routes)
  (ANY "*" [] user-routes)

This will run the incoming request first through admin-routes and then through user routes, applying the correct authentication in both cases. The main idea here is that your authentication function should return nil if the route is not accessible to the caller instead of throwing an error. This way admin-routes will return nil if a) the route actually does not match defined admin-routes or b) the user does not have the required authentication. If admin-routes returns nil, user-routes will be tried by compojure.

Hope this helps.

EDIT: I wrote a post about Compojure some time back, which you might find useful: https://vedang.me/techlog/2012-02-23-composability-and-compojure/


I stumbled on this issue, and it seems wrap-routes (compojure 1.3.2) solves elegantly:

(def app
  (handler/api
    (routes
      public-routes
      (-> user-routes
          (wrap-routes wrap-basic-authentication user-auth?)))))
      (-> admin-routes
          (wrap-routes wrap-basic-authentication admin-auth?)))))

This is a reasonable question, which I found surprisingly tricky when I ran into it myself.

I think what you want is this:

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" _
       (wrap-basic-authentication
        user-auth?
        (fn [req] (ring.util.response/response "USER ENDPOINT 1"))))

  (GET "/user-endpoint2" _
       (wrap-basic-authentication
        user-auth?
        (fn [req] (ring.util.response/response "USER ENDPOINT 1")))))

(defroutes admin-routes
  (GET "/admin-endpoint" _
       (wrap-basic-authentication
        admin-auth? (fn [req] (ring.util.response/response "ADMIN ENDPOINT")))))

(def app
  (handler/api
   (routes
    public-routes
    user-routes
    admin-routes)))

Two things to note: the authentication middleware is inside the routing form and the middleware calls an an anonymous function that is a genuine handler. Why?

  1. As you said, you need to apply authentication middleware after routing, or the request will never get routed to the authentication middleware! In other words, the routing needs to be on a middleware ring outside the authentication ring.

  2. If you use Compojure's routing forms like GET, and you are applying middleware in the body of the form, then the middleware function needs as its argument a genuine ring response handler (that is, a function that takes a request and returns a response), rather than something simpler like a string or a response map.

This is because, by definition, middleware functions like wrap-basic-authentication only take handlers as arguments, not bare strings or response maps or anything else.

So why is it so easy to miss this? The reason is that the Compojure routing operators like (GET [path args & body] ...) try to make things easy for you by being very flexible with what form you are allowed to pass in the body field. You can pass in a true handler function, or just a string, or a response map, or probably something else that hasn't occurred to me. It's all laid out in the render multi-method in the Compojure internals.

This flexibility disguises what the GET form is actually doing, so it's easy to get mixed up when you try to do something a bit different.

In my view, the problem with the leading answer by vedang is not a great idea in most cases. It essentially uses compojure machinery that's meant to answer the question "Does the route match the request?" (if not, return nil) to also answer the question "Does the request pass authentication?" This is problematic because usually you want requests that fail authentication to return proper responses with 401 status codes, as per the HTTP spec. In that answer, consider what would happen to valid user-authenticated requests if you added such an error response for failed admin-authentication to that example: all the valid user-authenticated request would fail and give errors at the admin routing layer.


I just found the following unrelated page that addresses the same issue:

http://compojureongae.posterous.com/using-the-app-engine-users-api-from-clojure

I didn't realise it's possible to use that type of syntax (which I have not yet tested):

(defroutes public-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT")))

(defroutes user-routes
  (GET "/user-endpoint1" [] ("USER ENDPOINT 1"))
  (GET "/user-endpoint2" [] ("USER ENDPOINT 1")))

(defroutes admin-routes
  (GET "/admin-endpoint" [] ("ADMIN ENDPOINT")))

(def app
  (handler/api
    (routes
      public-routes
      (ANY "/user*" []
        (-> user-routes
            (wrap-basic-authentication user-auth?)))
      (ANY "/admin*" []
        (-> admin-routes
            (wrap-basic-authentication admin-auth?))))))

Have you considered using Sandbar? It uses role-based authorisation, and lets you specify declaratively which roles are needed to access a particular resource. Check Sandbar's documentation for more information, but it could work something like this (note the reference to a fictitious my-auth-function, that's where you'd put your authentication code):

(def security-policy
     [#"/admin-endpoint.*"          :admin 
      #"/user-endpoint.*"           :user
      #"/public-endpoint.*"         :any])

(defroutes my-routes
  (GET "/public-endpoint" [] ("PUBLIC ENDPOINT"))
  (GET "/user-endpoint1"  [] ("USER ENDPOINT1"))
  (GET "/user-endpoint2"  [] ("USER ENDPOINT2"))
  (GET "/admin-endpoint"  [] ("ADMIN ENDPOINT"))

(def app
  (-> my-routes
      (with-security security-policy my-auth-function)
      wrap-stateful-session
      handler/api))