diff --git a/README.md b/README.md index f66dfafe..69ea86dc 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ Path-based routing: On match, route meta-data is returned and can interpreted by the application. -Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:handler` in the match. See [configuring routers](configuring-routers) for details. +Routers also support meta-data compilation enabling things like fast [Ring](https://github.com/ring-clojure/ring) or [Pedestal](http://pedestal.io/) -style handlers. Compilation results are found under `:handler` in the match. See [configuring routers](#configuring-routers) for details. ## Ring @@ -281,6 +281,59 @@ Nested middleware works too: Ring-router supports also 3-arity [Async Ring](https://www.booleanknot.com/blog/2016/07/15/asynchronous-ring.html), so it can be used on [Node.js](https://nodejs.org/en/) too. +### Meta-data based extensions + +The routing `Match` is injected into a request and can be extracted with `reitit.ring/get-match` helper. It can be used to build dynamic extensions to the system. + +A middleware to guard routes: + +```clj +(require '[clojure.set :as set]) + +(defn wrap-enforce-roles [handler] + (fn [{:keys [::roles] :as request}] + (let [required (some-> request (ring/get-match) :meta ::roles)] + (if (and (seq required) (not (set/intersection required roles))) + {:status 403, :body "forbidden"} + (handler request))))) +``` + +Mounted to an app via router meta-data (effecting all routes): + +```clj +(def handler (constantly {:status 200, :body "ok"})) + +(def app + (ring/ring-handler + (ring/router + [["/api" + ["/ping" handler] + ["/admin" {::roles #{:admin}} + ["/ping" handler]]]] + {:meta {:middleware [wrap-enforce-roles]}}))) +``` + +Anonymous access to public route: + +```clj +(app {:request-method :get, :uri "/api/ping"}) +; {:status 200, :body "ok"} +``` + +Anonymous access to guarded route: + +```clj +(app {:request-method :get, :uri "/api/admin/ping"}) +; {:status 403, :body "forbidden"} +``` + +Authorized access to guarded route: + +```clj +(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}}) +; {:status 200, :body "ok"} +``` + ## Validating route-tree **TODO** @@ -297,10 +350,6 @@ Ring-router supports also 3-arity [Async Ring](https://www.booleanknot.com/blog/ **TODO** -## Custom extensions - -**TODO** - ## Configuring Routers Routers can be configured via options to do things like custom coercion and compilatin of meta-data. These can be used to do things like [`clojure.spec`](https://clojure.org/about/spec) validation of meta-data and fast, compiled [Ring](https://github.com/ring-clojure/ring/wiki/Concepts) or [Pedestal](http://pedestal.io/) -style handlers. diff --git a/src/reitit/middleware.cljc b/src/reitit/middleware.cljc index 71af5d15..378c9974 100644 --- a/src/reitit/middleware.cljc +++ b/src/reitit/middleware.cljc @@ -1,5 +1,6 @@ (ns reitit.middleware - (:require [reitit.core :as reitit])) + (:require [meta-merge.core :refer [meta-merge]] + [reitit.core :as reitit])) (defprotocol ExpandMiddleware (expand-middleware [this])) @@ -40,5 +41,9 @@ (ensure-handler! path meta scope) ((compose-middleware middleware) handler))) -(defn router [data] - (reitit/router data {:compile compile-handler})) +(defn router + ([data] + (router data nil)) + ([data opts] + (let [opts (meta-merge {:compile compile-handler} opts)] + (reitit/router data opts)))) diff --git a/src/reitit/ring.cljc b/src/reitit/ring.cljc index 56c98c17..354d2f59 100644 --- a/src/reitit/ring.cljc +++ b/src/reitit/ring.cljc @@ -18,15 +18,18 @@ (fn ([request] (if-let [match (reitit/match-by-path router (:uri request))] - ((:handler match) request))) + ((:handler match) (assoc request ::match match)))) ([request respond raise] (if-let [match (reitit/match-by-path router (:uri request))] - ((:handler match) request respond raise)))) + ((:handler match) (assoc request ::match match) respond raise)))) {::router router})) (defn get-router [handler] (some-> handler meta ::router)) +(defn get-match [request] + (::match request)) + (defn coerce-handler [[path meta] {:keys [expand]}] [path (reduce (fn [acc method] @@ -53,6 +56,10 @@ (if-let [handler (resolved-handler (:request-method request))] (handler request respond raise)))))))) -(defn router [data] - (reitit/router data {:coerce coerce-handler - :compile compile-handler})) +(defn router + ([data] + (router data nil)) + ([data opts] + (let [opts (meta-merge {:coerce coerce-handler + :compile compile-handler} opts)] + (reitit/router data opts)))) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 12dddc68..bbd63e0b 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -1,7 +1,8 @@ (ns reitit.ring-test (:require [clojure.test :refer [deftest testing is]] [reitit.middleware :as middleware] - [reitit.ring :as ring]) + [reitit.ring :as ring] + [clojure.set :as set]) #?(:clj (:import (clojure.lang ExceptionInfo)))) @@ -122,3 +123,35 @@ (app {:uri "/api/users" :request-method :post} respond raise) (is (= {:status 200, :body [:api :users :post :ok :post :users :api]} @result))))))) + +(defn wrap-enforce-roles [handler] + (fn [{:keys [::roles] :as request}] + (let [required (some-> request (ring/get-match) :meta ::roles)] + (if (and (seq required) (not (set/intersection required roles))) + {:status 403, :body "forbidden"} + (handler request))))) + +(deftest enforcing-meta-data-rules-at-runtime-test + (let [handler (constantly {:status 200, :body "ok"}) + app (ring/ring-handler + (ring/router + [["/api" + ["/ping" handler] + ["/admin" {::roles #{:admin}} + ["/ping" handler]]]] + {:meta {:middleware [wrap-enforce-roles]}}))] + + (testing "public handler" + (is (= {:status 200, :body "ok"} + (app {:uri "/api/ping" :request-method :get})))) + + (testing "runtime-enforced handler" + (testing "without needed roles" + (is (= {:status 403 :body "forbidden"} + (app {:uri "/api/admin/ping" + :request-method :get})))) + (testing "with needed roles" + (is (= {:status 200, :body "ok"} + (app {:uri "/api/admin/ping" + :request-method :get + ::roles #{:admin}})))))))