In my Compojure/Ring web application's handler, I need to serve one set of routes using the site-defaults middleware, and another separate set of routes using the api-defaults middleware. How can I do that?
The code below only serves one set of routes using the site-defaults middleware. What should I add to serve the second set of routes (api-routes) using the api-defaults middleware?
(web-experiment.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults
site-defaults
api-defaults]]
[web-experiment.views :refer :all]))
(defroutes app-routes
(GET "/" [] (index-page))
(GET "/about" [] (about-page))
(route/not-found "Not Found"))
(defroutes api-routes
(GET "/grapefruit" [:as {body :body}] (grapefruit-api body))
(GET "/factory" [:as {body :body}] (factory-api body))
(GET "/umbrella" [:as {body :body}] (umbrella-api body))
(route/not-found "Not Found"))
(def app
(wrap-defaults app-routes site-defaults))
;; TODO: Add api-routes. How to use api-defaults middleware to serve api-routes?
I've read these:
Serving app and api routes with different middleware using Ring and Compojure - Does not solve the problem because the solution presented does not work with the wrap-defaults middleware using the site-defaults configuration.
https://github.com/ring-clojure/ring-anti-forgery/pull/14 - Does not provide a clear solution (i.e. code snippet) to the problem I have.
You can just wrap some of your routes in one wrapper and some others in the other wrapper. You just need to organize your routes a bit differently.
Some time ago I wrote a demo what you can do with ring routes. You can find it at https://github.com/ska2342/ring-routes-demo/
In particular the part starting at line 70 of the demo should be interesting to you.
I'm not sure how this could be solved in Compojure, but you may wish to consider using Pedestal. This page provides a good introduction to the route matching process, which takes place before any interceptors are called (the Pedestal replacement for Ring middleware).
So you could define two different sets of middleware:
(def user-intc-chain [inject-connection auth-required (body-params/body-params)] )
(def api-intc-chain [inject-connection auth-required api-params-intc] )
and then define routes like:
["/echo" :get (conj user-intc-chain echo-intc) ] ; a "user" route
["/alive?" :get (conj api-intc-chain alive-intc) ] ; an "api" route
In addition to the built-in Pedestal features, I have a number of helper & convenience functions documented here: https://cloojure.github.io/doc/tupelo/tupelo.pedestal.html
after doing web development for ages and discovering Clojure a year ago, I want to combine these two things.
After starting with Compojure, I try to implement authentication by using a middleware which responds with a 403 code, telling the user to authenticate.
This is my code:
(defn authenticated? [req]
(not (nil? (get-in req [:session :usr]))))
(defn helloworld [req]
(html5
[:head
[:title "Hello"]]
[:body
[:p "lorem ipsum"]]))
(defn backend [req]
(html5
[:head
[:title "Backend"]]
[:body
[:p "authenticated"]]))
(defroutes all-routes
(GET "/" [] helloworld)
(context "/backend" []
(GET "/" [] backend)))
(defn wrap-auth [handler]
(fn [req]
(if (authenticated? req)
(handler req)
(-> (response "Nope")
(status 403)))))
(def app
(-> (handler/site all-routes)
(wrap-auth)
(wrap-defaults site-defaults)))
Here comes the funny part: If I run the code as shown above, Firefox breaks with the error message "File not found". Opening the debug toolbar, I see a 403 response and the content "Tm9wZQ==" which is base 64 decoded the "Nope" from my auth middleware function. When I put wrap-auth after wrap-defaults everything works fine.
I want to understand what's going on there. Can you help me?
It's really difficult to say what's going on under the hood. The wrap-defaults middleware brings lots of stuff, maybe 10 or more wrappers at once. You'd better to examine its source code and choose exactly what you need.
I may guess that, for some reason, the Ring server considers your response being a file, so that's why it encodes it into base64. Try to return a plain map with proper headers as follows:
{:status 403
:body "<h1>No access</h1>"
:headers {"Content-Type" "text/html"}}
How do I add webjars resources to lib-noir's app-handler?
I used to do this only using Ring like this:
(def app
(-> handler
(wrap-resource "public")
(wrap-resource "/META-INF/resources")
;;resources from webjars
))
Now I'm trying to figure out how to do this with lib-noir.
I tried this:
(def app (noir-middleware/app-handler [home-routes app-routes]
:ring-defaults {:static
{:resources
"/META-INF/resources"}}))
and it works, but I get a problem when posting forms after configuring this. The params are empty in the ring request now.
This seems to do it:
(defroutes app-routes
(route/resources "/")
(route/resources "/" {:root "META-INF/resources/"})
(route/not-found "Not Found"))
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.
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.