@@ -66,8 +66,10 @@ module Yoga.HTTP.API.Route.OpenAPI
67import Prelude
67import Prelude
69+import Data.Array as Array
69import Data.Foldable (foldl)
70import Data.Foldable (foldl)
70import Data.Maybe (Maybe(..), maybe)
71import Data.Maybe (Maybe(..), maybe)
72+import Data.String as String
71import Data.String.Regex (replace)
73import Data.String.Regex (replace)
72import Data.String.Regex.Flags (global)
74import Data.String.Regex.Flags (global)
73import Data.String.Regex.Unsafe (unsafeRegex)
75import Data.String.Regex.Unsafe (unsafeRegex)
@@ -80,7 +82,7 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
80import Prim.RowList as RL
82import Prim.RowList as RL
81import Type.Proxy (Proxy(..))
83import Type.Proxy (Proxy(..))
82import Unsafe.Coerce (unsafeCoerce)
84import Unsafe.Coerce (unsafeCoerce)
83−import Yoga.HTTP.API.Route.BearerToken (BearerToken)
85+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
84import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
86import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
85import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
87import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
86import 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)
88import 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
187else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
189else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
188 renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
190 renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
190-- Required header (non-Maybe type)
240-- Required header (non-Maybe type)
191else instance
241else instance
192 ( IsSymbol name
242 ( IsSymbol name
@@ -290,6 +340,106 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
291 [ bearerAuth ]
341 [ bearerAuth ]
343+-- Case: BasicAuth found
344+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name BasicAuth tail) where
345+ detectSecurityRL _ =
347+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
351+-- Case: BasicAuth wrapped in Description
352+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc BasicAuth) tail) where
353+ detectSecurityRL _ =
355+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
359+-- Case: BasicAuth wrapped in Example
360+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex BasicAuth) tail) where
361+ detectSecurityRL _ =
363+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
367+-- Case: BasicAuth wrapped in Deprecated
368+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated BasicAuth) tail) where
369+ detectSecurityRL _ =
371+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
375+-- Case: ApiKeyHeader found
376+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name ApiKeyHeader tail) where
377+ detectSecurityRL _ =
379+ keyName = reflectSymbol (Proxy :: Proxy name)
380+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
384+-- Case: ApiKeyHeader wrapped in Description
385+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Description desc ApiKeyHeader) tail) where
386+ detectSecurityRL _ =
388+ keyName = reflectSymbol (Proxy :: Proxy name)
389+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
393+-- Case: ApiKeyHeader wrapped in Example
394+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Example ex ApiKeyHeader) tail) where
395+ detectSecurityRL _ =
397+ keyName = reflectSymbol (Proxy :: Proxy name)
398+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
402+-- Case: ApiKeyHeader wrapped in Deprecated
403+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Deprecated ApiKeyHeader) tail) where
404+ detectSecurityRL _ =
406+ keyName = reflectSymbol (Proxy :: Proxy name)
407+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
411+-- Case: DigestAuth found
412+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name DigestAuth tail) where
413+ detectSecurityRL _ =
415+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
419+-- Case: DigestAuth wrapped in Description
420+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc DigestAuth) tail) where
421+ detectSecurityRL _ =
423+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
427+-- Case: DigestAuth wrapped in Example
428+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex DigestAuth) tail) where
429+ detectSecurityRL _ =
431+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
435+-- Case: DigestAuth wrapped in Deprecated
436+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated DigestAuth) tail) where
437+ detectSecurityRL _ =
439+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
293-- Case: Non-security header, recurse
443-- Case: Non-security header, recurse
294else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
444else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
295 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
445 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
@@ -1237,32 +1387,60 @@ foreign import data OpenAPISpec :: Type
1237instance WriteForeign OpenAPISpec where
1387instance WriteForeign OpenAPISpec where
1238 writeImpl = unsafeCoerce
1388 writeImpl = unsafeCoerce
1240−-- | Detect if any operations use bearer token authentication
1390+-- | Detect which security schemes are used across all operations
1241-- | and build the securitySchemes component
1391-- | and build the securitySchemes component
1242buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
1392buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
1243buildSecuritySchemes ops =
1393buildSecuritySchemes ops =
1245− hasSecurity = ops # foldl checkOp false
1395+ securityKeys = ops # foldl collectSecurityKeys []
1247− checkOp acc entry =
1397+ collectSecurityKeys acc entry =
1249 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
1399 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
1250 securityField = FObject.lookup "security" opObj
1400 securityField = FObject.lookup "security" opObj
1252− acc || case securityField of
1402+ case securityField of
1253− Nothing -> false
1403+ Nothing -> acc
1254− Just _ -> true
1404+ Just secArray ->
1406+ secList = unsafeCoerce secArray :: Array (FObject.Object Foreign)
1407+ keys = secList >>= FObject.keys
1256− if hasSecurity then
1409+ acc <> keys
1257− FObject.singleton "bearerAuth"
1410+
1411+ uniqueKeys = Array.nub securityKeys
1412+ schemes = uniqueKeys <#> \key ->
1414+ scheme = case key of
1258− ( unsafeCoerce
1415+ "bearerAuth" -> Just $ Tuple key $ unsafeCoerce
1259 { type: "http"
1416 { type: "http"
1260 , scheme: "bearer"
1417 , scheme: "bearer"
1261 , bearerFormat: "JWT"
1418 , bearerFormat: "JWT"
1263− )
1420+ "basicAuth" -> Just $ Tuple key $ unsafeCoerce
1424+ "digestAuth" -> Just $ Tuple key $ unsafeCoerce
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
1432+ headerName = String.take (String.length key - 6) key
1434+ Just $ Tuple key $ unsafeCoerce
1264− else
1439+ else Nothing
1265− FObject.empty
1440+ in
1443+ FObject.fromFoldable $ Array.catMaybes schemes
1267-- | Type for OpenAPI server object
1445-- | Type for OpenAPI server object
1268type ServerObject =
1446type ServerObject =