@@ -66,8 +66,10 @@ module Yoga.HTTP.API.Route.OpenAPI
6666
6767import Prelude
6868
69+import Data.Array as Array
6970import Data.Foldable (foldl)
7071import Data.Maybe (Maybe(..), maybe)
72+import Data.String as String
7173import Data.String.Regex (replace)
7274import Data.String.Regex.Flags (global)
7375import Data.String.Regex.Unsafe (unsafeRegex)
@@ -80,7 +82,7 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
8082import Prim.RowList as RL
8183import Type.Proxy (Proxy(..))
8284import Unsafe.Coerce (unsafeCoerce)
83−import Yoga.HTTP.API.Route.BearerToken (BearerToken)
85+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
8486import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
8587import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
8688import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasOperationMetadata, operationMetadata)
@@ -187,6 +189,54 @@ else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex Bea
187189else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
188190 renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
189191
192+-- Skip BasicAuth headers (they're handled as security schemes, not regular headers)
193+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name BasicAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name BasicAuth tail) headers where
194+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
195+
196+-- Skip BasicAuth wrapped in Description
197+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc BasicAuth) tail) headers where
198+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
199+
200+-- Skip BasicAuth wrapped in Example
201+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex BasicAuth) tail) headers where
202+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
203+
204+-- Skip BasicAuth wrapped in Deprecated
205+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BasicAuth) tail) headers where
206+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
207+
208+-- Skip ApiKeyHeader headers (they're handled as security schemes, not regular headers)
209+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name ApiKeyHeader tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name ApiKeyHeader tail) headers where
210+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
211+
212+-- Skip ApiKeyHeader wrapped in Description
213+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc ApiKeyHeader) tail) headers where
214+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
215+
216+-- Skip ApiKeyHeader wrapped in Example
217+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex ApiKeyHeader) tail) headers where
218+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
219+
220+-- Skip ApiKeyHeader wrapped in Deprecated
221+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated ApiKeyHeader) tail) headers where
222+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
223+
224+-- Skip DigestAuth headers (they're handled as security schemes, not regular headers)
225+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name DigestAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name DigestAuth tail) headers where
226+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
227+
228+-- Skip DigestAuth wrapped in Description
229+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc DigestAuth) tail) headers where
230+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
231+
232+-- Skip DigestAuth wrapped in Example
233+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex DigestAuth) tail) headers where
234+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
235+
236+-- Skip DigestAuth wrapped in Deprecated
237+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated DigestAuth) tail) headers where
238+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
239+
190240-- Required header (non-Maybe type)
191241else instance
192242 ( IsSymbol name
@@ -290,6 +340,106 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
290340 in
291341 [ bearerAuth ]
292342
343+-- Case: BasicAuth found
344+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name BasicAuth tail) where
345+ detectSecurityRL _ =
346+ let
347+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
348+ in
349+ [ basicAuth ]
350+
351+-- Case: BasicAuth wrapped in Description
352+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc BasicAuth) tail) where
353+ detectSecurityRL _ =
354+ let
355+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
356+ in
357+ [ basicAuth ]
358+
359+-- Case: BasicAuth wrapped in Example
360+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex BasicAuth) tail) where
361+ detectSecurityRL _ =
362+ let
363+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
364+ in
365+ [ basicAuth ]
366+
367+-- Case: BasicAuth wrapped in Deprecated
368+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated BasicAuth) tail) where
369+ detectSecurityRL _ =
370+ let
371+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
372+ in
373+ [ basicAuth ]
374+
375+-- Case: ApiKeyHeader found
376+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name ApiKeyHeader tail) where
377+ detectSecurityRL _ =
378+ let
379+ keyName = reflectSymbol (Proxy :: Proxy name)
380+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
381+ in
382+ [ apiKeyAuth ]
383+
384+-- Case: ApiKeyHeader wrapped in Description
385+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Description desc ApiKeyHeader) tail) where
386+ detectSecurityRL _ =
387+ let
388+ keyName = reflectSymbol (Proxy :: Proxy name)
389+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
390+ in
391+ [ apiKeyAuth ]
392+
393+-- Case: ApiKeyHeader wrapped in Example
394+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Example ex ApiKeyHeader) tail) where
395+ detectSecurityRL _ =
396+ let
397+ keyName = reflectSymbol (Proxy :: Proxy name)
398+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
399+ in
400+ [ apiKeyAuth ]
401+
402+-- Case: ApiKeyHeader wrapped in Deprecated
403+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Deprecated ApiKeyHeader) tail) where
404+ detectSecurityRL _ =
405+ let
406+ keyName = reflectSymbol (Proxy :: Proxy name)
407+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
408+ in
409+ [ apiKeyAuth ]
410+
411+-- Case: DigestAuth found
412+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name DigestAuth tail) where
413+ detectSecurityRL _ =
414+ let
415+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
416+ in
417+ [ digestAuth ]
418+
419+-- Case: DigestAuth wrapped in Description
420+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc DigestAuth) tail) where
421+ detectSecurityRL _ =
422+ let
423+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
424+ in
425+ [ digestAuth ]
426+
427+-- Case: DigestAuth wrapped in Example
428+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex DigestAuth) tail) where
429+ detectSecurityRL _ =
430+ let
431+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
432+ in
433+ [ digestAuth ]
434+
435+-- Case: DigestAuth wrapped in Deprecated
436+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated DigestAuth) tail) where
437+ detectSecurityRL _ =
438+ let
439+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
440+ in
441+ [ digestAuth ]
442+
293443-- Case: Non-security header, recurse
294444else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
295445 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
@@ -1237,32 +1387,60 @@ foreign import data OpenAPISpec :: Type
12371387instance WriteForeign OpenAPISpec where
12381388 writeImpl = unsafeCoerce
12391389
1240−-- | Detect if any operations use bearer token authentication
1390+-- | Detect which security schemes are used across all operations
12411391-- | and build the securitySchemes component
12421392buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
12431393buildSecuritySchemes ops =
12441394 let
1245− hasSecurity = ops # foldl checkOp false
1395+ securityKeys = ops # foldl collectSecurityKeys []
12461396 where
1247− checkOp acc entry =
1397+ collectSecurityKeys acc entry =
12481398 let
12491399 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
12501400 securityField = FObject.lookup "security" opObj
12511401 in
1252− acc || case securityField of
1253− Nothing -> false
1254− Just _ -> true
1255− in
1256− if hasSecurity then
1257− FObject.singleton "bearerAuth"
1258− ( unsafeCoerce
1402+ case securityField of
1403+ Nothing -> acc
1404+ Just secArray ->
1405+ let
1406+ secList = unsafeCoerce secArray :: Array (FObject.Object Foreign)
1407+ keys = secList >>= FObject.keys
1408+ in
1409+ acc <> keys
1410+
1411+ uniqueKeys = Array.nub securityKeys
1412+ schemes = uniqueKeys <#> \key ->
1413+ let
1414+ scheme = case key of
1415+ "bearerAuth" -> Just $ Tuple key $ unsafeCoerce
12591416 { type: "http"
12601417 , scheme: "bearer"
12611418 , bearerFormat: "JWT"
12621419 }
1263− )
1264− else
1265− FObject.empty
1420+ "basicAuth" -> Just $ Tuple key $ unsafeCoerce
1421+ { type: "http"
1422+ , scheme: "basic"
1423+ }
1424+ "digestAuth" -> Just $ Tuple key $ unsafeCoerce
1425+ { type: "http"
1426+ , scheme: "digest"
1427+ }
1428+ _ ->
1429+ -- API Key (header names ending with "ApiKey")
1430+ if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "ApiKey" then
1431+ let
1432+ headerName = String.take (String.length key - 6) key
1433+ in
1434+ Just $ Tuple key $ unsafeCoerce
1435+ { type: "apiKey"
1436+ , in: "header"
1437+ , name: headerName
1438+ }
1439+ else Nothing
1440+ in
1441+ scheme
1442+ in
1443+ FObject.fromFoldable $ Array.catMaybes schemes
12661444
12671445-- | Type for OpenAPI server object
12681446type ServerObject =