Restricted routes

Restricting access to a specific web page or some functionality is a cornerstone of today's web apps. Without restricting resources or functionality, the applications that power the Internet would be in complete anarchy. Anybody could post as anybody on Twitter; Facebook would suddenly have 1 single user representing 1/7th of the world's population; YouTube would become even more dominated by cats. Hence, restricting access is a good thing – even if you wish YouTube was dominated (even more so) by cats.

Restricting routes in a Luminus application is a two pronged approach. First, we must mark a route as restricted. Secondly, we must define what governs access to the restricted route.

Restricting route access

There are two ways we can restrict route access in a Luminus app. The first is by using the noir.util.route/restricted macro on the route in question. For example, pretend we had the following route:

(GET "/just-for-you" [] (render-private-page))

That's something we've seen no fewer than a dozen times. In its current incarnation, anybody and everybody can view /just-for-you as it has no restrictions. It's wide open and public. It is the exact opposite of just-for-you. We can lock this down by applying the restricted macro, as seen here:

(require '[noir.util.route :refer [restricted]])
(GET "/just-for'you" [] (restricted (render-private-page)))

Alternatively, we can restrict access to multiple routes by using the def-restricted-routes macro. The def-restricted-routes macro is exactly like the defroutes macro we've been using in the various hipstr.routes.* namespaces, with the exception that it will mark all defined routes as restricted on our behalf. For example, instead of doing something like this:

(defroutes ;everything here will be public,
           ;unless marked otherwise
  (GET "/" [] (render-home-page))
  (GET "/protected [] (restricted (render-protected-page)))
  (GET "/another-public" [] (render-another-public-page)))

We can separate our protected from our public routes:

(defroutes ;everything here will be public
  (GET "/" [] (render-home-page))
  (GET "/another-public" [] (render-another-public-page)))
(def-restricted-routes ;these are all protected, obviously
  (GET "/protected [] (render-protected-page)))

There are pros and cons to each, but mostly from a route management perspective. If your application is small and you only have a handful of routes, or if all of your protected routes are under a specific URI, then it probably makes sense to just use the restricted macro directly. However, if you have dozens or hundreds of routes spread about several pages, then it may make sense to make use of the def-protected-routes macro; however, that also comes at the cost of doubling the number of routes you have to import in your handler. It's give and take. Frankly, I've never worked on a project that's grown quite so large wherein we didn't have to refactor our routes at least once, so whichever strategy you choose may change over time.

So we now know how to restrict routes, but how do we access them? How does Luminus know that the requester of the route is okay to access the route?

Accessing a restricted route

Rules governing restricted routes are subjective. Every application, and in fact many routes within the same application, may have different rules governing access. Luminus allows us to define each of these rules to varying layers of granularity by using the :access-rules key of the app-handler, which hipstr makes use of when defining hipstr.handler/app:

(def app (app-handler
  ;...snipped for brevity
  ;; add access rules here
  :access-rules []
  ;…snipped for brevity
  )

By default, the :access-rules are empty. An empty :access-rules is basically a skeleton key, unlocking every restricted door in the house. In our just-for-you example, the restricted macro won't actually do anything because we've yet to define an access rule. There are two ways we can define an access rule, either as a function, or as a map.

Access rule as a function

Access rule functions accept a request map and return a value. If the returned value is truthy, then access will be granted, otherwise the user will be redirected to /. With that in mind, we could create a simple and completely useless access rule such as the following:

:access-rules [#(-> (java.util.UUID/randomUUID) str keyword %)]

In the preceding code, we're generating a random UUID, getting its string representation, turning it into a keyword, and, finally, checking to see if that keyword is in the request map (which it won't be because UUIDs are, by definition, guaranteed to be universally unique). Since the access rule always returns false, and it's the only access rule we've defined, all requests to a route marked as restricted will redirect the user to /.

If we changed the rule to return true under certain circumstances, say, if a particular value is in the session, then the the request will be granted access to any restricted route.

Access rules as a function don't provide us with many options, as it's basically a one-ring-to-rule-them-all approach; we can't even define where to redirect the user if access is not granted. This may be fine if you're designing a REST API that requires an authentication token. However, for a web application with human interaction, it's not the best. For this reason, it's likely that you'll define access rules as a map.

Access rule as a map

Defining an access rule as a map opens the door for customization. While a rule as a map still requires us to write at least 1 rule function, we can also define different rules for different routes, redirect to different URIs, and define what to do in case of a failed attempt (instead of just redirecting the user – such as returning a 403 response in a REST API instead of a 302 Redirect). We can even have multiple rule functions for a single access rule and state whether we want any or all of the rules to pass in order for access to be granted.

The map equivalent of our access-rule-as-a-function example would look like the following:

:access-rules [{:rule
  #(-> (java.util.UUID/randomUUID) str keyword %)}]

This looks roughly the same, but with a bit more noise, thus calling into question why we would define it as such. However, we could do something like the following:

:access-rules [{:uri "/just-for-you"
                :rule #(->(java.utiul.UUID/randomUUID)                str keyword %)
                :redirect "/access-denied"}]

Ah, now we're getting somewhere. Instead of redirecting the user to / (the default), the preceding access rule will redirect the user to /access-denied because we defined a value for :redirect. Also, by specifying the :uri keyword, the rule will only be applied to that URI instead of all URIs. Alternatively, we can use :uris to define multiple URIs against which the :rule will be applied:

:access-rules [{:uris ["/just-for-you" "/and-maybe-you"]…}]

However, in the preceding case, the route will still always redirect the user to /access-denied because our rule will always return false. We can, however, provide multiple rules and specify if any of them match to grant access:

:access-rules [{:rules {:any [
                 #(->(java.utiul.UUID/randomUUID) str keyword %)
                 #(= (:username %) "TheDude")]}…}]

In the preceding example, the access rule will grant access if either the first UUID function returns true (which it never will,) or if the request map has the :username of TheDude. Alternatively, by using :all instead of :any, all of the rules would have to return truthy in order for the access rule to be satisfied.

Tip

Please note, dear reader, that this is just a trivial example to understand what we can do. Your access rules will likely (please?) be more complex and secure than something so easily spoofable. But you knew that already.

Finally, if we didn't want to return a 302 redirect to some URI on failure, we can use the :on-fail key to specify what to do as a response. For example, we could, instead, render a basic 403 Unauthorized page:

(require ‘[noir.util.response :as response])
:access-rules [{:on-fail (fn [req]
                 (response/status
                   (response/response "Unauthorized") 403))…}]

Defining access rules as maps provides granularity and flexibility for authorization within our application, while keeping the syntax relatively low. It's easy to get our head around it all, and simple to expand.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.42.128