Compojure routes with different middleware - clojure

I'm currently writing an API in Clojure using Compojure (and Ring and associated middleware).
I'm trying to apply different authentication code depending on the route. Consider the following code:
(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
(-> user-routes
(wrap-basic-authentication user-auth?)))))
(-> admin-routes
(wrap-basic-authentication admin-auth?)))))
This doesn't work as expected because wrap-basic-authentication indeed wraps routes so it gets tried regardless of the wrapped routes. Specifically, if the requests needs to be routed to admin-routes, user-auth? will still be tried (and fail).
I resorted to use context to root some routes under a common base
path but it's quite a constraint (the code below may not work it's simply to illustrate the idea):
(defroutes user-routes
(GET "-endpoint1" [] ("USER ENDPOINT 1"))
(GET "-endpoint2" [] ("USER ENDPOINT 1")))
(defroutes admin-routes
(GET "-endpoint" [] ("ADMIN ENDPOINT")))
(def app
(handler/api
(routes
public-routes
(context "/user" []
(-> user-routes
(wrap-basic-authentication user-auth?)))
(context "/admin" []
(-> admin-routes
(wrap-basic-authentication admin-auth?))))))
I'm wondering if I'm missing something or if there's any way at all to achieve what I want without constraint on my defroutes and without using a common base path (as ideally, there would be none).

(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?
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.
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))

I would shift how you end up handling the authentication in general to split apart the process of authenticating and filtering routes on authentication.
Rather than just having the admin-auth? and user-auth? return booleans or a user name, use it as more of an "access level" key which you can filter on on much more of a per-route level without the need to "reauthenticate" for different routes.
(defn auth [user pass]
(cond
(admin-auth? user pass) :admin
(user-auth? user pass) :user
true :unauthenticated))
You'll also want to consider an alternate to the existing basic authentication middleware for this path. As it's currently designed, it'll always return a {:status 401} if you don't provide credentials, so you'll need to take this into account and have it continue through instead.
The result of this is put in the :basic-authentication key in the request map, which you can then filter at the level you want.
The main "filtering" cases that come to mind are:
At a context level (like what you have in your answer), except you can just filter out requests that don't have the required :basic-authentication key
On a per route level, where you return a 401 response after a local check on how it's authenticated. Note that this is the only way you'll get a distinction between 404s and 401s unless you do the context level filtering on individual routes.
Different views for a page depending on the authentication level
The biggest thing to remember is that you have to continue feeding back nil for invalid routes unless the url being asked for needs authentication. You need to make sure you're not filtering out more than you want by returning a 401, which will cause ring to stop trying any other routes/handles.

Related

Unable to access form paramaters when using nested defroutes in Compojure

I'm unable to access form parameters from a POST request. I've tried every combination of middleware and config options I've seen in the docs, on SO, etc. (including the deprecated compojure/handler options) and I'm still unable to see the parameters. I'm sure I'm missing something very obvious, so any suggestions (no matter how slight) would be greatly appreciated.
Here's my latest attempt, wherein I try to use the site-defaults middleware and disable the anti-forgery/CSRF protection provided by default. (I know this is a bad idea.) However, when I try to view the page in question in a web browser, the browser tries to download the page, as if it were a file it wasn't capable of rendering. (Interestingly, the page is rendered as expected when using Curl.)
Here's the latest attempt:
(defroutes config-routes*
(POST "/config" request post-config-handler))
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
(middleware-defaults/wrap-defaults (assoc middleware-defaults/site-defaults :security {:anti-forgery false}))))
Previous attempt:
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
middleware-params/wrap-params))
UPDATE:
The parameters appear to be swallowed by the outer defroutes:
(defroutes app-routes
(ANY "*" [] api-routes)
(ANY "*" [] config-routes)
(route/not-found "Not Found"))
So, my question now becomes: How can I thread the parameters through to the nested defroutes?
My temporary solve is based on this solution, but Steffen Frank's is much simpler. I will try that and follow-up.
UPDATE 2:
In trying to implement the suggestions provided by both of the current answers, I'm running into a new issue: route matches are overeager. e.g. given the following, POSTs to /something fail with a 401 response because of the wrap-basic-authentication middleware in config-routes.
(defroutes api-routes*
(POST "/something" request post-somethings-handler))
(def api-routes
(-> #'api-routes*
(middleware-defaults/wrap-defaults middleware-defaults/api-defaults)
middleware-json/wrap-json-params
middleware-json/wrap-json-response))
(defroutes config-routes*
(GET "/config" request get-config-handler)
(POST "/config" request post-config-handler))
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
middleware-params/wrap-params))
(defroutes app-routes
config-routes
api-routes
(route/not-found "Not Found"))
(def app app-routes)
The issue is that when you define your routes in this way:
(defroutes app-routes
(ANY "*" [] api-routes)
(ANY "*" [] config-routes)
(route/not-found "Not Found"))
then any request will be matched by api-routes as long as it returns non-nil response. Thus api-routes does not swallow your request params but rather stealing the whole request.
Instead you should define your app-routes as (preferred solution):
(defroutes app-routes
api-routes
config-routes
(route/not-found "Not Found"))
or make sure that your api-routes returns nil for unmatched URL path (e.g. it shouldn't have not-found route defined).
Just a guess, but have you tried this:
(defroutes app-routes
api-routes
config-routes
(route/not-found "Not Found"))
You may find the following post useful. It talks about mixing api and app routes such that they don't interfere with each other and you avoid adding middleware from one to the toher etc. Serving app and api routes with different middleware using Ring and Compojure

Compojure: access basic-authentication vars in route processing

I am writing a web API using Compojure with basic-authentication middleware. The basic authentication part looks something like:
(defn authenticated? [id pass]
(and (= id "blah")
(= pass "blah"))
id and pass are passed in using the id:pass#website technique. My problem is, I would like to access this id and pass further on down where the routes are processed with the various GET, PUT, POST, etc. headings. But I can't figure out how to do this, and haven't had any luck googling around.
; i'd like to access id and pass here
(defroutes routes
(GET "/" [] {:status 200 :headers {} :body "hi!"})
...)
I presume the solution is for the above to somehow "add" id and pass to some set of variables that can be accessed where the routes are processed, but I have no idea how to add nor to access them.
Hopefully someone can point me in the right direction - thanks.
Assuming you are talking about https://github.com/remvee/ring-basic-authentication, the authenticated? fn can return a truthly value that will be added to the request in the :basic-authentication. Something like (untested):
(defn authenticated? [id pass]
(and (= id "gal")
(= pass "foo")
{:user id :passwd pass}))
(defroutes routes
(GET "/" {the-user :basic-authentication} {:status 200 :headers {} :body (str "hi! Mr. " (:user the-user) " your password is " (:passwd the-user))})
...)
The return of the authenticated? method is associated in request map referenced by the key :basic-authentication. Here goes an example with a route that returns the user. You could, however, return a map or any other object and access it through :basic-authentication key.
(defn authenticated? [user password] user)
(defroutes require-auth-routes
(GET "/return-user" request (.toString (:basic-authentication request)))
(def my-app
(routes
(-> require-auth-routes
(wrap-basic-authentication authenticated?)))

How can I intercept a Compojure request and execute it based on a test?

I have some routes.
(defroutes some-routes
(GET "one" [] one)
(GET "two" [] two))
(defroutes other-routes
(GET "three" [] three)
(GET "four" [] four))
(defroutes more-routes
(GET "five" [] five)
(GET "six" [] six))
(def all-routes
(routes app-routes
(-> some-routes session/wrap-session my-interceptor)
(-> more-routes session/wrap-session my-other-interceptor)
other-routes))
I want to intercept the some-routes but not other-routes and perform a test based on the request (checking that a key exists in the session and some other stuff). I have more than one of these. my-other-interceptor does the same kind of thing but different.
So I start with this:
(defn my-interceptor [handler]
(fn [request]
(prn (-> request :session :thing-key))
(let [thing (-> request :session :thing-key-id)]
(if (nil? thing)
(-> (response "Not authenticated"))
(handler request)))))
This will allow access to the handler if :thing-key is set in the session.
Unfortunately this doesn't play nicely with having more than one set of routes. This check should only apply to some-routes and not other-routes. But until we execute the handler we don't know if the route matches. And at that point the handler has already executed. I could rewrite it to execute handler and then only perform the check if the response is non-nil, but this means I've executed a handler before checking for auth.
I followed this example, which exhibits the problem:
(defn add-app-version-header [handler]
(fn [request]
(let [resp (handler request)
headers (:headers resp)]
(assoc resp :headers
(assoc headers "X-APP-INFO" "MyTerifficApp Version 0.0.1-Alpha")))))
How do I do this? What I want is:
a way of checking a response (and some other logic) before handling a request
that I can apply to a large-ish set of routes' handlers
that isn't applied to all routes in the app
I will have more than one such handler doing a different kind of check on the session
How should I go about doing this?
The way to have separate handlers or middlewares is to decompose your routes using compojure.core/routes and use your handler only where you need it.
In your case, if you put your other-routes first, your problem should be solved.
As in:
(def app-routes
(compojure.core/routes
other-routes
(-> some-routes
session/wrap-session
my-interceptor)))
Remember compojure routes are just ring handlers, you can always write a custom defroutes that calls your handler only if the route matches the request, this is the make route source code
(defn make-route
"Returns a function that will only call the handler if the method and Clout
route match the request."
[method route handler]
(if-method method
(if-route route
(fn [request]
(render (handler request) request)))))
That way, if you have more than one conditioned handler you don't need to rely on putting those routes at the end of the composition.
Notice that approach is in case you want to keep your route handling code clean.
(my-def-routes routes
(GET "/" request (show-all request))
If you don't wanna roll your own defroutes just call your interceptor inside:
(defroutes routes
(GET "/" request (interceptor request show-all))
(defn interceptor
[request handler]

Compojure - how to return 404 when required params are not provided?

Suppose I have this handler:
(defroutes routes
(DELETE "/books" [id] (delete-book id)))
What can I do to make this app return HTTP 404 when request does not contain ID?
Firstly, you could make the id a part of the URI, which seems nice and RESTful and would allow you to use the route syntax to impose your condition:
(GET ["/books/:id" :id #"[0-9]+"] [] ...)
If you do prefer to use a parameter, something like
(if-not id
(ring.util.response/not-found body-for-404)
...)
should work in the next Ring version, though this particular function has not been released yet (it simply returns {:status 404 :headers {} :body the-body} though).
Also,
(when id
...)
would result in the equivalent of a route match failure and the remaining routes would be tried; then you could use
(compojure.route/not-found body-for-404)
as the final route which would always match.
Finally, if you wish to apply filtering to a large group of Compojure handlers, you may wish to combine them into a single handler with Compojure's defroutes or routes (the latter is a function) and wrapping them in a piece of middleware:
(defn wrap-404 [handler]
(fn wrap-404 [request]
(when (-> request :params :id)
(handler request))))
You can then include the wrapped handler as an entry in routes / defroutes forms.

compojure defroutes - route sometimes not recognized

I have a clojure / compojure webapp with the following routes
(defroutes my-routes
(GET "/app/preview" request (my-preview-function request))
(ANY "*" request (str "ANY page <br>" (request :params))))
The preview GET request is made with a couple of parameters. I find this works most of the time but sometimes the /ebook/preview is not found and processing drops to the ANY route, in which case the output is similar to this,
ANY page
{:* "/app/preview", :section "50", :id "48"}
Can anyone suggest what might cause the /ebook/preview request to be skipped? It is definitely a GET request being made; the HTML does not have a POST for the /app/preview URL and to be doubly sure I added a POST route for /app/preview and that was not being hit.
JAR versions:
Clojure 1.2
compojure-0.6.2
ring-core-0.3.7
jetty-6.1.14
ring-jetty-adapter-0.3.1
ring-servlet-0.3.1jar
servlet-api-2.5-6.1.14
Routes are wrapped as follows
(require '[compojure.handler :as handler])
(defn wrap-charset [handler charset]
(fn [request]
(if-let [response (handler request)]
(if-let [content-type (get-in response [:headers "Content-Type"])]
(if (.contains content-type "charset")
response
(assoc-in response
[:headers "Content-Type"]
(str content-type "; charset=" charset)))
response))))
(def app (-> my-routes
handler/site
wrap-stateful-session
(wrap-charset "utf-8")
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
If you're trying to figure out what request is causing the problems, stop throwing away the request map with (request :params) and just have a look at request. That will give you a map with all the information Compojure has; you can inspect it, and pass it back into your routes later to observe what happens (after you make some changes, say).
If
(my-preview-function request)
returns nil, then the routing will try the next route. Take a look at (source GET) and see how it matches (or doesn't) your route.