From 7cd4c62216f9ccab303647df96329eac3de1a525 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Aug 2017 10:05:26 +0300 Subject: [PATCH 1/3] Match is injected into request --- src/reitit/middleware.cljc | 11 +++++++--- src/reitit/ring.cljc | 17 +++++++++----- test/cljc/reitit/ring_test.cljc | 39 +++++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 10 deletions(-) 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..3ef1a2e2 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)))) @@ -121,4 +122,38 @@ respond (partial reset! result), raise ::not-called] (app {:uri "/api/users" :request-method :post} respond raise) (is (= {:status 200, :body [:api :users :post :ok :post :users :api]} - @result))))))) + @result)))))) + + (testing "runtime extensions for meta-data" + (let [enforce-roles (fn [handler] + (fn [{:keys [::roles] :as request}] + (let [required (some-> request + (ring/get-match) + :meta + ::roles)] + (if (or (not (seq required)) + (set/intersection required roles)) + (handler request) + {:status 403 :body "forbidden"})))) + router (ring/router + [["/api" + ["/ping" handler] + ["/admin" {::roles #{:admin}} + ["/ping" handler]]]] + {:meta {:middleware [enforce-roles]}}) + app (ring/ring-handler router)] + + (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}})))))))) From 0de9e1b3c6610f95bf58082eef4be1a8c4903a67 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Aug 2017 10:16:25 +0300 Subject: [PATCH 2/3] Polish --- test/cljc/reitit/ring_test.cljc | 61 ++++++++++++++++----------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index 3ef1a2e2..ca85d30e 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -122,38 +122,35 @@ respond (partial reset! result), raise ::not-called] (app {:uri "/api/users" :request-method :post} respond raise) (is (= {:status 200, :body [:api :users :post :ok :post :users :api]} - @result)))))) + @result))))))) - (testing "runtime extensions for meta-data" - (let [enforce-roles (fn [handler] - (fn [{:keys [::roles] :as request}] - (let [required (some-> request - (ring/get-match) - :meta - ::roles)] - (if (or (not (seq required)) - (set/intersection required roles)) - (handler request) - {:status 403 :body "forbidden"})))) - router (ring/router - [["/api" - ["/ping" handler] - ["/admin" {::roles #{:admin}} - ["/ping" handler]]]] - {:meta {:middleware [enforce-roles]}}) - app (ring/ring-handler router)] +(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 "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}})))))))) + (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}}))))))) From 406b4c653f54c3a5f96aa3aa0c91b3b395f25eb7 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Tue, 15 Aug 2017 11:06:26 +0300 Subject: [PATCH 3/3] Update README --- README.md | 59 ++++++++++++++++++++++++++++++--- test/cljc/reitit/ring_test.cljc | 1 + 2 files changed, 55 insertions(+), 5 deletions(-) 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/test/cljc/reitit/ring_test.cljc b/test/cljc/reitit/ring_test.cljc index ca85d30e..bbd63e0b 100644 --- a/test/cljc/reitit/ring_test.cljc +++ b/test/cljc/reitit/ring_test.cljc @@ -130,6 +130,7 @@ (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