diff --git a/deps.edn b/deps.edn index de6a2e4..03c87a5 100644 --- a/deps.edn +++ b/deps.edn @@ -5,6 +5,7 @@ ; https://opensource.org/licenses/MIT. {:deps {io.swagger.parser.v3/swagger-parser {:mvn/version "2.1.25"}} + :paths ["resources" "src"] :aliases {:test {:extra-paths ["test"] :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"} metosin/malli {:mvn/version "0.17.0"}} diff --git a/resources/security-users.yml b/resources/security-users.yml new file mode 100644 index 0000000..01152b6 --- /dev/null +++ b/resources/security-users.yml @@ -0,0 +1,65 @@ +openapi: 3.1.0 +info: + title: Simple User Listing API + version: "1.0.0" + +paths: + /users: + get: + operationId: listUsers + security: + - sessionCookieAuth: ["read:user"] + - test: ["one:two"] + responses: + '200': + description: A list of user objects + '401': + description: Unauthorized + /users-single-scheme: + get: + operationId: listUsersSingle + security: + - sessionCookieAuth: ["read:user"] + responses: + '200': + description: OK + /users-no-scope: + get: + operationId: listUsersNoScope + security: + - sessionCookieAuth: [] + responses: + '200': + description: OK + /users-no-security: + get: + operationId: listUsersNoSecurity + responses: + '200': + description: OK + +components: + securitySchemes: + sessionCookieAuth: + type: apiKey + in: cookie + name: session + description: > + A session-based authentication scheme. The cookie must contain + a valid session ID that identifies the user. + schemas: + User: + type: object + required: + - id + properties: + id: + type: string + format: uuid + description: The unique identifier for a user + email: + type: string + description: Optional email address of the user + name: + type: string + description: Optional name of the user diff --git a/src/navi/impl.clj b/src/navi/impl.clj index c07cd17..713aec5 100644 --- a/src/navi/impl.clj +++ b/src/navi/impl.clj @@ -103,6 +103,17 @@ (cond-> {:content content} description (assoc :description description)))) +(defn security->vec-of-vecs + "Takes a list of SecurityRequirement objects and returns + a vector of [scheme scope-vector] pairs. + e.g. [[\"sessionCookieAuth\" [\"read:user\"]] + [\"test\" [\"one:two\"]]]" + [^java.util.List security-reqs] + (->> security-reqs + (mapcat seq) + (map (juxt key val)) + (into []))) + (defn operation->data "Converts a Java Operation to a map of parameters, responses, schemas and handler that conforms to reitit." @@ -121,10 +132,12 @@ (wrap-map :header) (wrap-map :cookie)) responses (-> (.getResponses op) - (update-kvs handle-response-key response->data))] + (update-kvs handle-response-key response->data)) + security (security->vec-of-vecs (.getSecurity op))] (cond-> {:handler (get handlers (.getOperationId op))} (seq schemas) (assoc :parameters schemas) - (seq responses) (assoc :responses responses))) + (seq responses) (assoc :responses responses) + (seq security) (assoc :security security))) (catch Exception e (throw (ex-info (str "Exception processing operation " (pr-str (.getOperationId op)) diff --git a/test/navi/impl_test.clj b/test/navi/impl_test.clj index 141bc7b..8239a43 100644 --- a/test/navi/impl_test.clj +++ b/test/navi/impl_test.clj @@ -7,7 +7,9 @@ (ns navi.impl-test (:require [clojure.test :refer [deftest is testing]] - [navi.impl :as i]) + [navi.core :as navi] + [navi.impl :as i] + [clojure.java.io :as io]) (:import [clojure.lang ExceptionInfo] [io.swagger.v3.oas.models Operation PathItem] @@ -153,4 +155,46 @@ (.setGet operation))] (is (= {:get {:handler "a handler" :parameters {:path [:map [:x int?]]}}} - (i/path-item->data path-item handlers)))))) \ No newline at end of file + (i/path-item->data path-item handlers)))))) + +(defn find-route [rts path method] + (some (fn [[p r]] + (when (= p path) + (get r method))) + rts)) + +(deftest security-requirements-test + (testing "Verifying security requirements from security-users.yml" + ;; A dummy map of operationId to handler (the actual function doesn't matter for this test). + (let [handlers {"listUsers" (constantly :ok) + "listUsersSingle" (constantly :ok) + "listUsersNoScope" (constantly :ok) + "listUsersNoSecurity" (constantly :ok)} + api-spec (slurp (io/resource "security-users.yml")) + routes (navi/routes-from api-spec handlers)] + + (testing "multiple security schemes" + (let [route (find-route routes "/users" :get)] + (is (some? route) "Should have found /users GET route") + (is (= [["sessionCookieAuth" ["read:user"]] + ["test" ["one:two"]]] + (:security route))))) + + (testing "single security scheme with scopes" + (let [route (find-route routes "/users-single-scheme" :get)] + (is (some? route) "Should have found /users-single-scheme GET route") + (is (= [["sessionCookieAuth" ["read:user"]]] + (:security route))))) + + (testing "single security scheme without scopes" + (let [route (find-route routes "/users-no-scope" :get)] + (is (some? route) "Should have found /users-no-scope GET route") + (is (= [["sessionCookieAuth" []]] + (:security route)) + "No scopes should yield an empty vector for that scheme"))) + + (testing "no security block" + (let [route (find-route routes "/users-no-security" :get)] + (is (some? route) "Should have found /users-no-security GET route") + (is (nil? (:security route)) + "Route with no security block should not have :security key"))))))