on master help sign in
Added security schemes (#59)

* Security schemes * Added simple test

commitb76f48dc5a23dfed08b163829a90bee33d990490 authoriko <ilyakooo0@gmail.com> dateTue 25 May 2021, 13:01 (5 years ago) parente0ffc45
16 files changed+594−52
openapi-diff.cabal+2 src/OpenAPI/Checker/Behavior.hs+8−3 src/OpenAPI/Checker/Orphans.hs+16−1 src/OpenAPI/Checker/References.hs+1 src/OpenAPI/Checker/Subtree.hs+1 src/OpenAPI/Checker/Subtree/Deriving.hs+6 src/OpenAPI/Checker/Validate/MediaTypeObject.hs+41−31 src/OpenAPI/Checker/Validate/OAuth2Flows.hs+132 src/OpenAPI/Checker/Validate/Operation.hs+17−4 src/OpenAPI/Checker/Validate/SecurityRequirement.hs+93−8 src/OpenAPI/Checker/Validate/SecurityScheme.hs+43 stack.yaml+1−1 stack.yaml.lock+4−4 test/golden/common/security-scheme/a.yaml+110 test/golden/common/security-scheme/b.yaml+110 test/golden/common/security-scheme/trace-tree.yaml+9
openapi-diff.cabal+2
@@ -134,6 +134,8 @@ library
134134 , OpenAPI.Checker.Validate.Header
135135 , OpenAPI.Checker.Validate.Link
136136 , OpenAPI.Checker.Common
137+ , OpenAPI.Checker.Validate.SecurityScheme
138+ , OpenAPI.Checker.Validate.OAuth2Flows
137139
138140executable openapi-diff
139141 import: common-options
src/OpenAPI/Checker/Behavior.hs+8−3
@@ -4,7 +4,8 @@ module OpenAPI.Checker.Behavior
44 , Issuable (..)
55 , Behavior
66 , AnIssue (..)
7− ) where
7+ )
8+where
89
910import Data.Aeson
1011import Data.Kind
@@ -16,6 +17,7 @@ data BehaviorLevel
1617 = APILevel
1718 | ServerLevel
1819 | SecurityRequirementLevel
20+ | SecuritySchemeLevel
1921 | PathLevel
2022 | OperationLevel
2123 | PathFragmentLevel
@@ -28,8 +30,10 @@ data BehaviorLevel
2830 | TypedSchemaLevel
2931 | LinkLevel
3032
31−class (Ord (Behave a b), Show (Behave a b))
32− => Behavable (a :: BehaviorLevel) (b :: BehaviorLevel) where
33+class
34+ (Ord (Behave a b), Show (Behave a b)) =>
35+ Behavable (a :: BehaviorLevel) (b :: BehaviorLevel)
36+ where
3337 data Behave a b
3438
3539class (Ord (Issue l), Show (Issue l)) => Issuable (l :: BehaviorLevel) where
@@ -48,6 +52,7 @@ data AnIssue (l :: BehaviorLevel) where
4852 AnIssue :: Issuable l => Issue l -> AnIssue l
4953
5054deriving stock instance Eq (AnIssue l)
55+
5156deriving stock instance Ord (AnIssue l)
5257
5358instance ToJSON (AnIssue l) where
src/OpenAPI/Checker/Orphans.hs+16−1
@@ -3,25 +3,40 @@
33module OpenAPI.Checker.Orphans () where
44
55import Control.Comonad.Env
6−import Data.OpenApi
76import qualified Data.HashMap.Strict.InsOrd as IOHM
7+import Data.OpenApi
88
99deriving newtype instance Ord Reference
1010
1111deriving stock instance Ord a => Ord (Referenced a)
12+
1213deriving stock instance Ord Schema
14+
1315deriving stock instance Ord AdditionalProperties
16+
1417deriving stock instance Ord Discriminator
18+
1519deriving stock instance Ord Xml
20+
1621deriving stock instance Ord OpenApiType
22+
1723deriving stock instance Ord Style
24+
1825deriving stock instance Ord OpenApiItems
26+
1927deriving stock instance Ord ParamLocation
2028
29+deriving stock instance Ord HttpSchemeType
30+
31+deriving stock instance Ord ApiKeyParams
32+
33+deriving stock instance Ord ApiKeyLocation
2134
2235instance (Ord k, Ord v) => Ord (IOHM.InsOrdHashMap k v) where
2336 compare xs ys = compare (IOHM.toList xs) (IOHM.toList ys)
2437
2538deriving stock instance (Eq e, Eq (w a)) => Eq (EnvT e w a)
39+
2640deriving stock instance (Ord e, Ord (w a)) => Ord (EnvT e w a)
41+
2742deriving stock instance (Show e, Show (w a)) => Show (EnvT e w a)
src/OpenAPI/Checker/References.hs+1
@@ -3,6 +3,7 @@
33module OpenAPI.Checker.References
44 ( Step (..)
55 , dereference
6+ , Typeable
67 )
78where
89
src/OpenAPI/Checker/Subtree.hs+1
@@ -39,6 +39,7 @@ module OpenAPI.Checker.Subtree
3939 , ask
4040 , local
4141 , step
42+ , Typeable
4243 )
4344where
4445
src/OpenAPI/Checker/Subtree/Deriving.hsnew file+6
@@ -0,0 +1,6 @@
1+module OpenAPI.Checker.Subtree.Deriving (EqSubtree (..)) where
2+
3+import Control.Monad
4+import OpenAPI.Checker.Subtree
5+
6+newtype EqSubtree t = EqSubtree t
src/OpenAPI/Checker/Validate/MediaTypeObject.hs+41−31
@@ -1,9 +1,10 @@
11{-# OPTIONS_GHC -Wno-orphans #-}
22
33module OpenAPI.Checker.Validate.MediaTypeObject
4− ( Issue(..)
5− , Behave(..)
6− ) where
4+ ( Issue (..)
5+ , Behave (..)
6+ )
7+where
78
89import Control.Lens
910import Data.Foldable as F
@@ -15,18 +16,18 @@ import Data.Text (Text)
1516import Network.HTTP.Media (MediaType, mainType, subType)
1617import OpenAPI.Checker.Behavior
1718import OpenAPI.Checker.Subtree
19+import OpenAPI.Checker.Validate.Header ()
1820import OpenAPI.Checker.Validate.Products
1921import OpenAPI.Checker.Validate.Schema ()
20−import OpenAPI.Checker.Validate.Header ()
21−
2222
2323tracedSchema :: Traced MediaTypeObject -> Maybe (Traced (Referenced Schema))
2424tracedSchema mto = _mediaTypeObjectSchema (extract mto) <&> traced (ask mto >>> step MediaTypeSchema)
2525
2626-- FIXME: This should be done through 'MediaTypeEncodingMapping'
2727tracedEncoding :: Traced MediaTypeObject -> InsOrdHashMap Text (Traced Encoding)
28−tracedEncoding mto = IOHM.mapWithKey (\k -> traced (ask mto >>> step (MediaTypeParamEncoding k)))
29− $ _mediaTypeObjectEncoding $ extract mto
28+tracedEncoding mto =
29+ IOHM.mapWithKey (\k -> traced (ask mto >>> step (MediaTypeParamEncoding k))) $
30+ _mediaTypeObjectEncoding $ extract mto
3031
3132instance Issuable 'PayloadLevel where
3233 data Issue 'PayloadLevel
@@ -47,43 +48,52 @@ instance Behavable 'PayloadLevel 'SchemaLevel where
4748
4849instance Subtree MediaTypeObject where
4950 type SubtreeLevel MediaTypeObject = 'PayloadLevel
50− type CheckEnv MediaTypeObject =
51− '[ MediaType
52− , ProdCons (Traced (Definitions Schema))
53− , ProdCons (Traced (Definitions Header))
54− ]
51+ type
52+ CheckEnv MediaTypeObject =
53+ '[ MediaType
54+ , ProdCons (Traced (Definitions Schema))
55+ , ProdCons (Traced (Definitions Header))
56+ ]
5557 checkStructuralCompatibility env pc = do
5658 structuralMaybe env $ tracedSchema <$> pc
5759 structuralEq $ fmap _mediaTypeObjectExample <$> pc
5860 iohmStructural env $ stepTraced MediaTypeEncodingMapping . fmap _mediaTypeObjectEncoding <$> pc
5961 pure ()
6062 checkSemanticCompatibility env beh prodCons@(ProdCons p c) = do
61− if | "multipart" == mainType mediaType -> checkEncoding
62− | "application" == mainType mediaType &&
63− "x-www-form-urlencoded" == subType mediaType -> checkEncoding
64− | otherwise -> pure ()
63+ if
64+ | "multipart" == mainType mediaType -> checkEncoding
65+ | "application" == mainType mediaType
66+ && "x-www-form-urlencoded" == subType mediaType ->
67+ checkEncoding
68+ | otherwise -> pure ()
6569 -- If consumer requires schema then producer must produce compatible
6670 -- request
6771 for_ (tracedSchema c) $ \consRef ->
68− case tracedSchema p of
69− Nothing -> issueAt beh MediaTypeSchemaRequired
70− Just prodRef -> checkCompatibility env (beh >>> step PayloadSchema)
71− $ ProdCons prodRef consRef
72+ case tracedSchema p of
73+ Nothing -> issueAt beh MediaTypeSchemaRequired
74+ Just prodRef ->
75+ checkCompatibility env (beh >>> step PayloadSchema) $
76+ ProdCons prodRef consRef
7277 pure ()
7378 where
7479 mediaType = getH @MediaType env
7580 checkEncoding =
76− let
77− -- Parameters of the media type are product-like entities
78− getEncoding mt = M.fromList
79− $ (IOHM.toList $ tracedEncoding mt) <&> \(k, enc) ->
80− ( k
81− , ProductLike
82− { productValue = enc
83− , required = True } )
84− encProdCons = getEncoding <$> prodCons
85− in checkProducts beh MediaEncodingMissing
86− (const $ checkCompatibility env beh) encProdCons
81+ let -- Parameters of the media type are product-like entities
82+ getEncoding mt =
83+ M.fromList $
84+ (IOHM.toList $ tracedEncoding mt) <&> \(k, enc) ->
85+ ( k
86+ , ProductLike
87+ { productValue = enc
88+ , required = True
89+ }
90+ )
91+ encProdCons = getEncoding <$> prodCons
92+ in checkProducts
93+ beh
94+ MediaEncodingMissing
95+ (const $ checkCompatibility env beh)
96+ encProdCons
8797
8898instance Subtree Encoding where
8999 type SubtreeLevel Encoding = 'PayloadLevel
src/OpenAPI/Checker/Validate/OAuth2Flows.hsnew file+132
@@ -0,0 +1,132 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.OAuth2Flows
4+ ( Step (..)
5+ , Issue (..)
6+ , Behave (..)
7+ )
8+where
9+
10+import Control.Monad
11+import Data.Function
12+import qualified Data.HashMap.Strict.InsOrd as IOHM
13+import Data.OpenApi
14+import Data.Proxy
15+import Data.Set (Set)
16+import qualified Data.Set as S
17+import Data.Text (Text)
18+import OpenAPI.Checker.Behavior
19+import OpenAPI.Checker.Orphans ()
20+import OpenAPI.Checker.Subtree
21+
22+instance Subtree OAuth2Flows where
23+ type CheckEnv OAuth2Flows = '[]
24+ type SubtreeLevel OAuth2Flows = 'SecuritySchemeLevel
25+ checkStructuralCompatibility _ = structuralEq
26+ checkSemanticCompatibility env bhv pc = do
27+ let supportFlow
28+ :: (Subtree t, SubtreeLevel t ~ SubtreeLevel OAuth2Flows, CheckEnv OAuth2Flows ~ CheckEnv t)
29+ => Issue 'SecuritySchemeLevel
30+ -> ProdCons (Maybe (Traced t))
31+ -> SemanticCompatFormula ()
32+ supportFlow i x = case x of
33+ -- producer will not attempt this flow
34+ (ProdCons Nothing _) -> pure ()
35+ -- producer can attempt a flow the consumer does not know about
36+ (ProdCons (Just _) Nothing) -> issueAt bhv i
37+ (ProdCons (Just p) (Just c)) ->
38+ checkCompatibility env bhv $ ProdCons p c
39+ getFlow
40+ :: Typeable x
41+ => (OAuth2Flows -> Maybe (OAuth2Flow x))
42+ -> Traced OAuth2Flows
43+ -> Maybe (Traced (OAuth2Flow x))
44+ getFlow f (Traced t a) = Traced (t >>> step (OAuth2FlowsFlow Proxy)) <$> f a
45+ supportFlow ConsumerDoesNotSupportImplicitFlow $ getFlow _oAuth2FlowsImplicit <$> pc
46+ supportFlow ConsumerDoesNotSupportPasswordFlow $ getFlow _oAuth2FlowsPassword <$> pc
47+ supportFlow ConsumerDoesNotSupportClientCridentialsFlow $ getFlow _oAuth2FlowsClientCredentials <$> pc
48+ supportFlow ConsumerDoesNotSupportAuthorizationCodeFlow $ getFlow _oAuth2FlowsAuthorizationCode <$> pc
49+ pure ()
50+
51+instance Typeable t => Steppable OAuth2Flows (OAuth2Flow t) where
52+ data Step OAuth2Flows (OAuth2Flow t) = OAuth2FlowsFlow (Proxy t)
53+ deriving (Eq, Ord, Show)
54+
55+instance (Typeable t, Subtree t, SubtreeLevel (OAuth2Flow t) ~ SubtreeLevel t) => Subtree (OAuth2Flow t) where
56+ type CheckEnv (OAuth2Flow t) = CheckEnv t
57+ type SubtreeLevel (OAuth2Flow t) = 'SecuritySchemeLevel
58+ checkStructuralCompatibility = undefined
59+ checkSemanticCompatibility env bhv prodCons@(ProdCons p c) = do
60+ let ProdCons pScopes cScopes = S.fromList . IOHM.keys . _oAuth2Scopes . extract <$> prodCons
61+ missingScopes = cScopes S.\\ pScopes
62+ unless (S.null missingScopes) (issueAt bhv $ ScopesMissing missingScopes)
63+ checkCompatibility env bhv $ retraced (>>> step (OAuth2FlowParamsStep Proxy)) . fmap _oAuth2Params <$> prodCons
64+ unless (((==) `on` _oAath2RefreshUrl . extract) p c) $ issueAt bhv RefreshUrlsDontMatch
65+ pure ()
66+
67+instance Typeable t => Steppable (OAuth2Flow t) t where
68+ data Step (OAuth2Flow t) t = OAuth2FlowParamsStep (Proxy t)
69+ deriving (Eq, Ord, Show)
70+
71+instance Subtree OAuth2ImplicitFlow where
72+ type SubtreeLevel OAuth2ImplicitFlow = 'SecuritySchemeLevel
73+ type CheckEnv OAuth2ImplicitFlow = '[]
74+ checkStructuralCompatibility = undefined
75+ checkSemanticCompatibility _ bhv (ProdCons p c) =
76+ unless (extract p == extract c) $ issueAt bhv OAuth2ImplicitFlowNotEqual
77+
78+instance Subtree OAuth2PasswordFlow where
79+ type SubtreeLevel OAuth2PasswordFlow = 'SecuritySchemeLevel
80+ type CheckEnv OAuth2PasswordFlow = '[]
81+ checkStructuralCompatibility = undefined
82+ checkSemanticCompatibility _ bhv (ProdCons p c) =
83+ unless (extract p == extract c) $ issueAt bhv OAuth2PasswordFlowNotEqual
84+
85+instance Subtree OAuth2ClientCredentialsFlow where
86+ type SubtreeLevel OAuth2ClientCredentialsFlow = 'SecuritySchemeLevel
87+ type CheckEnv OAuth2ClientCredentialsFlow = '[]
88+ checkStructuralCompatibility = undefined
89+ checkSemanticCompatibility _ bhv (ProdCons p c) =
90+ unless (extract p == extract c) $ issueAt bhv OAuth2ClientCredentialsFlowNotEqual
91+
92+instance Subtree OAuth2AuthorizationCodeFlow where
93+ type SubtreeLevel OAuth2AuthorizationCodeFlow = 'SecuritySchemeLevel
94+ type CheckEnv OAuth2AuthorizationCodeFlow = '[]
95+ checkStructuralCompatibility = undefined
96+ checkSemanticCompatibility _ bhv (ProdCons p c) =
97+ unless (extract p == extract c) $ issueAt bhv OAuth2AuthorizationCodeFlowNotEqual
98+
99+instance Issuable 'SecurityRequirementLevel where
100+ data Issue 'SecurityRequirementLevel
101+ = SecurityRequirementNotMet
102+ | UndefinedSecurityScheme Text
103+ deriving stock (Eq, Ord, Show)
104+ issueIsUnsupported _ = False
105+
106+instance Issuable 'SecuritySchemeLevel where
107+ data Issue 'SecuritySchemeLevel
108+ = RefreshUrlsDontMatch
109+ | HttpSchemeTypesDontMatch HttpSchemeType HttpSchemeType
110+ | ApiKeyParamsDontMatch ApiKeyParams ApiKeyParams
111+ | OpenIdConnectUrlsDontMatch URL URL
112+ | CustomHttpSchemesDontMatch Text Text
113+ | ConsumerDoesNotSupportImplicitFlow
114+ | ConsumerDoesNotSupportPasswordFlow
115+ | ConsumerDoesNotSupportClientCridentialsFlow
116+ | ConsumerDoesNotSupportAuthorizationCodeFlow
117+ | SecuritySchemeNotMatched
118+ | OAuth2ImplicitFlowNotEqual
119+ | OAuth2PasswordFlowNotEqual
120+ | OAuth2ClientCredentialsFlowNotEqual
121+ | OAuth2AuthorizationCodeFlowNotEqual
122+ | ScopesMissing (Set Text)
123+ | DifferentSecuritySchemes
124+ | CanNotHaveScopes
125+ | ScopeNotDefined Text
126+ deriving stock (Eq, Ord, Show)
127+ issueIsUnsupported _ = False
128+
129+instance Behavable 'SecurityRequirementLevel 'SecuritySchemeLevel where
130+ data Behave 'SecurityRequirementLevel 'SecuritySchemeLevel
131+ = SecuritySchemeStep Text
132+ deriving stock (Eq, Ord, Show)
src/OpenAPI/Checker/Validate/Operation.hs+17−4
@@ -20,10 +20,12 @@ import OpenAPI.Checker.Behavior
2020import OpenAPI.Checker.References
2121import OpenAPI.Checker.Subtree
2222import OpenAPI.Checker.Validate.MediaTypeObject
23+import OpenAPI.Checker.Validate.OAuth2Flows
2324import OpenAPI.Checker.Validate.PathFragment
2425import OpenAPI.Checker.Validate.Products
2526import OpenAPI.Checker.Validate.RequestBody ()
2627import OpenAPI.Checker.Validate.Responses ()
28+import OpenAPI.Checker.Validate.SecurityRequirement ()
2729import OpenAPI.Checker.Validate.Server ()
2830
2931data MatchedOperation = MatchedOperation
@@ -54,9 +56,9 @@ tracedResponses oper =
5456 traced (ask oper >>> step OperationResponsesStep) $
5557 _operationResponses . operation $ extract oper
5658
57−tracedSecurity :: Traced MatchedOperation -> [Traced SecurityRequirement]
59+tracedSecurity :: Traced MatchedOperation -> [(Int, Traced SecurityRequirement)]
5860tracedSecurity oper =
59− [ traced (ask oper >>> step (OperationSecurityRequirementStep i)) x
61+ [ (i, traced (ask oper >>> step (OperationSecurityRequirementStep i)) x)
6062 | (i, x) <- zip [0 ..] $ _operationSecurity . operation $ extract oper
6163 ]
6264
@@ -81,6 +83,11 @@ instance Behavable 'OperationLevel 'RequestLevel where
8183 = InRequest
8284 deriving stock (Eq, Ord, Show)
8385
86+instance Behavable 'OperationLevel 'SecurityRequirementLevel where
87+ data Behave 'OperationLevel 'SecurityRequirementLevel
88+ = SecurityRequirementStep Int
89+ deriving stock (Eq, Ord, Show)
90+
8491instance Subtree MatchedOperation where
8592 type SubtreeLevel MatchedOperation = 'OperationLevel
8693 type
@@ -116,8 +123,8 @@ instance Subtree MatchedOperation where
116123 x <- pc
117124 se <- getH @(ProdCons [Server]) env
118125 pure $ Traced (ask x >>> step OperationServersStep) (getServers se (extract x))
126+ structuralList env $ fmap snd . tracedSecurity <$> pc
119127 -- TODO: Callbacks
120− -- TODO: Security
121128 pure ()
122129 checkSemanticCompatibility env beh prodCons = do
123130 checkParameters
@@ -211,7 +218,13 @@ instance Subtree MatchedOperation where
211218 -- FIXME: https://github.com/typeable/openapi-diff/issues/27
212219 checkCallbacks = pure () -- (error "FIXME: not implemented")
213220 -- FIXME: https://github.com/typeable/openapi-diff/issues/28
214− checkOperationSecurity = pure () -- (error "FIXME: not implemented")
221+ checkOperationSecurity = do
222+ let ProdCons pSecs cSecs = tracedSecurity <$> prodCons
223+ for_ pSecs $ \(i, pSec) -> do
224+ let beh' = beh >>> step (SecurityRequirementStep i)
225+ anyOfAt beh' SecurityRequirementNotMet $
226+ cSecs <&> \(_, cSec) ->
227+ checkCompatibility env beh' $ ProdCons pSec cSec
215228 checkServers =
216229 checkCompatibility env beh $ do
217230 x <- prodCons
src/OpenAPI/Checker/Validate/SecurityRequirement.hs+93−8
@@ -1,3 +1,4 @@
1+{-# LANGUAGE PartialTypeSignatures #-}
12{-# OPTIONS_GHC -Wno-orphans #-}
23
34module OpenAPI.Checker.Validate.SecurityRequirement
@@ -5,15 +6,25 @@ module OpenAPI.Checker.Validate.SecurityRequirement
56 )
67where
78
9+import Control.Comonad
10+import Control.Monad
11+import Control.Monad.Writer
12+import Data.Bifunctor
13+import Data.Either
14+import Data.Foldable
15+import Data.Functor
16+import Data.HList
17+import qualified Data.HashMap.Strict.InsOrd as IOHM
18+import qualified Data.List.NonEmpty as NE
819import Data.OpenApi
20+import Data.Set (Set)
21+import qualified Data.Set as S
22+import Data.Text (Text)
23+import Data.Traversable
924import OpenAPI.Checker.Behavior
1025import OpenAPI.Checker.Subtree
11−
12−instance Issuable 'SecurityRequirementLevel where
13− data Issue 'SecurityRequirementLevel
14− = SecurityRequirementNotMet
15− deriving stock (Eq, Ord, Show)
16− issueIsUnsupported _ = False
26+import OpenAPI.Checker.Validate.OAuth2Flows
27+import OpenAPI.Checker.Validate.SecurityScheme ()
1728
1829instance Subtree SecurityRequirement where
1930 type SubtreeLevel SecurityRequirement = 'SecurityRequirementLevel
@@ -21,5 +32,79 @@ instance Subtree SecurityRequirement where
2132 CheckEnv SecurityRequirement =
2233 '[ ProdCons (Traced (Definitions SecurityScheme))
2334 ]
24− checkStructuralCompatibility = undefined
25− checkSemanticCompatibility = undefined
35+ checkStructuralCompatibility env pc = do
36+ let normalized = do
37+ sec <- extract <$> pc
38+ defs <- getH env
39+ -- lookupScheme
40+ pure $
41+ for (IOHM.toList $ getSecurityRequirement sec) $ \(key, scopes) ->
42+ (,scopes) <$> lookupScheme key defs
43+ structuralMaybeWith
44+ (\pc' -> do
45+ let ProdCons pScopes cScopes = fmap snd <$> pc'
46+ unless (pScopes == cScopes) structuralIssue
47+ structuralList env $ fmap fst <$> pc'
48+ pure ())
49+ normalized
50+ pure ()
51+ checkSemanticCompatibility env bhv' pc = do
52+ let schemes = getH @(ProdCons (Traced (Definitions SecurityScheme))) env
53+ ( ProdCons pErrs cErrs
54+ , (ProdCons pSchemes cSchemes)
55+ :: ProdCons [(Behavior 'SecuritySchemeLevel, Traced SecurityScheme, [Text])]
56+ ) =
57+ NE.unzip $
58+ partitionEithers <$> do
59+ req <- pc
60+ scheme <- schemes
61+ pure $
62+ let -- [(key, scopes)]
63+ pairs = IOHM.toList . getSecurityRequirement . extract $ req
64+ in pairs <&> \(key, scopes) -> do
65+ case lookupScheme key scheme of
66+ Nothing -> Left $ UndefinedSecurityScheme key
67+ Just x -> Right (bhv' >>> step (SecuritySchemeStep key), x, scopes)
68+ lookSimilar :: SecurityScheme -> SecurityScheme -> Bool
69+ lookSimilar x y = case (_securitySchemeType x, _securitySchemeType y) of
70+ (SecuritySchemeOAuth2 {}, SecuritySchemeOAuth2 {}) -> True
71+ (SecuritySchemeHttp {}, SecuritySchemeHttp {}) -> True
72+ (SecuritySchemeApiKey {}, SecuritySchemeApiKey {}) -> True
73+ (SecuritySchemeOpenIdConnect {}, SecuritySchemeOpenIdConnect {}) -> True
74+ _ -> False
75+ issueAt bhv' `traverse_` pErrs
76+ issueAt bhv' `traverse_` cErrs
77+ for_ pSchemes $ \(bhv, pScheme, pScopes) -> do
78+ let lookPromising = filter (lookSimilar (extract pScheme) . extract . (\(_, x, _) -> x)) cSchemes
79+ anyOfAt bhv SecuritySchemeNotMatched $
80+ lookPromising <&> \(_, cScheme, cScopes) -> do
81+ let untracedSchemes = join bimap (_securitySchemeType . extract) (pScheme, cScheme)
82+ scopedFlow :: Set Text -> OAuth2Flow t -> Writer [Issue 'SecuritySchemeLevel] (OAuth2Flow t)
83+ scopedFlow scopes x = do
84+ let scopesMap = _oAuth2Scopes x
85+ for_ scopes $ \scope -> unless (scope `IOHM.member` scopesMap) $ tell [ScopeNotDefined scope]
86+ pure $ x {_oAuth2Scopes = IOHM.filterWithKey (\k _ -> k `S.member` scopes) scopesMap}
87+ scopedSchemeType :: [Text] -> SecuritySchemeType -> Writer [Issue 'SecuritySchemeLevel] SecuritySchemeType
88+ scopedSchemeType scopes (SecuritySchemeOAuth2 (OAuth2Flows a b c d)) =
89+ fmap SecuritySchemeOAuth2 $ OAuth2Flows <$> flow a <*> flow b <*> flow c <*> flow d
90+ where
91+ flow :: Maybe (OAuth2Flow t) -> Writer [Issue 'SecuritySchemeLevel] (Maybe (OAuth2Flow t))
92+ flow = traverse $ scopedFlow $ S.fromList scopes
93+ scopedSchemeType _ x = pure x
94+ scopedScheme scopes x = do
95+ sType <- scopedSchemeType scopes $ _securitySchemeType x
96+ pure $ x {_securitySchemeType = sType}
97+ (pc', errs) = runWriter $ ProdCons <$> scopedScheme pScopes `traverse` pScheme <*> scopedScheme cScopes `traverse` cScheme
98+ case untracedSchemes of
99+ (SecuritySchemeOpenIdConnect _, SecuritySchemeOpenIdConnect _) -> do
100+ let missingScopes = S.fromList cScopes S.\\ S.fromList pScopes
101+ unless (S.null missingScopes) $ issueAt bhv (ScopesMissing missingScopes)
102+ (SecuritySchemeOAuth2 {}, SecuritySchemeOAuth2 {}) -> pure ()
103+ _ -> unless (null pScopes && null cScopes) $ issueAt bhv CanNotHaveScopes
104+ for_ errs $ issueAt bhv
105+ checkCompatibility env bhv pc'
106+ pure ()
107+ pure ()
108+
109+lookupScheme :: Text -> Traced (Definitions SecurityScheme) -> Maybe (Traced SecurityScheme)
110+lookupScheme k (Traced t m) = Traced (t >>> step (InsOrdHashMapKeyStep k)) <$> IOHM.lookup k m
src/OpenAPI/Checker/Validate/SecurityScheme.hsnew file+43
@@ -0,0 +1,43 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.SecurityScheme
4+ (
5+ )
6+where
7+
8+import Control.Monad
9+import Data.OpenApi
10+import OpenAPI.Checker.Behavior
11+import OpenAPI.Checker.Orphans ()
12+import OpenAPI.Checker.Subtree
13+import OpenAPI.Checker.Validate.OAuth2Flows
14+
15+instance Subtree SecurityScheme where
16+ type CheckEnv SecurityScheme = '[]
17+ type SubtreeLevel SecurityScheme = 'SecuritySchemeLevel
18+ checkStructuralCompatibility _ pc = structuralEq $ tracedSecuritySchemaTypes <$> pc
19+ checkSemanticCompatibility env bhv pcSecScheme = case tracedSecuritySchemaTypes <$> pcSecScheme of
20+ (ProdCons (Traced _ (SecuritySchemeHttp pType)) (Traced _ (SecuritySchemeHttp cType))) -> case (pType, cType) of
21+ (HttpSchemeBearer _, HttpSchemeBearer _) -> pure ()
22+ (HttpSchemeBasic, HttpSchemeBasic) -> pure ()
23+ (HttpSchemeCustom p, HttpSchemeCustom c) ->
24+ unless (p == c) (issueAt bhv $ CustomHttpSchemesDontMatch p c)
25+ _ -> issueAt bhv $ HttpSchemeTypesDontMatch pType cType
26+ (ProdCons (Traced _ (SecuritySchemeApiKey pParams)) (Traced _ (SecuritySchemeApiKey cParams))) -> do
27+ unless (pParams == cParams) (issueAt bhv $ ApiKeyParamsDontMatch pParams cParams)
28+ (ProdCons (Traced pT (SecuritySchemeOAuth2 pFlows)) (Traced cT (SecuritySchemeOAuth2 cFlows))) -> do
29+ checkCompatibility env bhv . fmap (stepTraced SecurityOAuthFlowsStep) $ ProdCons (Traced pT pFlows) (Traced cT cFlows)
30+ (ProdCons (Traced _ (SecuritySchemeOpenIdConnect pUrl)) (Traced _ (SecuritySchemeOpenIdConnect cUrl))) -> do
31+ unless (pUrl == cUrl) (issueAt bhv $ OpenIdConnectUrlsDontMatch pUrl cUrl)
32+ _ -> issueAt bhv DifferentSecuritySchemes
33+
34+tracedSecuritySchemaTypes :: Traced SecurityScheme -> Traced SecuritySchemeType
35+tracedSecuritySchemaTypes (Traced t x) = Traced (t >>> step SecuritySchemeTypeStep) (_securitySchemeType x)
36+
37+instance Steppable SecurityScheme SecuritySchemeType where
38+ data Step SecurityScheme SecuritySchemeType = SecuritySchemeTypeStep
39+ deriving (Eq, Ord, Show)
40+
41+instance Steppable SecuritySchemeType OAuth2Flows where
42+ data Step SecuritySchemeType OAuth2Flows = SecurityOAuthFlowsStep
43+ deriving (Eq, Ord, Show)
stack.yaml+1−1
@@ -1 +1 @@
1−resolver: lts-17.11
1+resolver: lts-17.12
stack.yaml.lock+4−4
@@ -6,7 +6,7 @@
66packages: []
77snapshots:
88- completed:
9− size: 567672
10− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/11.yaml
11− sha256: 03181cdbeb671eb605bbcf6f285bea4d094b6ac7433a0e14a9f1dd54ad995938
12− original: lts-17.11
9+ size: 567669
10+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/12.yaml
11+ sha256: facf6cac73b22a83ca955b580a98a7a09ed71f8f974c7a55d28e608c23e689a9
12+ original: lts-17.12
test/golden/common/security-scheme/a.yamlnew file+110
@@ -0,0 +1,110 @@
1+openapi: 3.0.0
2+info:
3+ version: ""
4+ title: ""
5+paths:
6+ /oauth/sign_out:
7+ get:
8+ parameters:
9+ - required: false
10+ schema:
11+ type: string
12+ in: query
13+ name: redirect
14+ responses:
15+ "302":
16+ content:
17+ application/x-www-form-urlencoded: {}
18+ headers:
19+ Location:
20+ schema:
21+ type: string
22+ set-cookie:
23+ schema:
24+ type: string
25+ description: ""
26+ "400":
27+ description: Invalid `redirect`
28+ security:
29+ - sign-out-oauth: []
30+ - oauth: []
31+ /oauth/authorize:
32+ get:
33+ responses:
34+ "302":
35+ content:
36+ application/x-www-form-urlencoded: {}
37+ headers:
38+ Location:
39+ schema:
40+ type: string
41+ description: ""
42+ security:
43+ - get-oauth: []
44+ /oauth/token:
45+ post:
46+ requestBody:
47+ content:
48+ application/x-www-form-urlencoded:
49+ schema:
50+ type: string
51+ responses:
52+ "200":
53+ content:
54+ application/json;charset=utf-8:
55+ schema:
56+ type: string
57+ description: ""
58+ "400":
59+ description: Invalid `body`
60+ security:
61+ - oauth: []
62+ /oauth/check:
63+ post:
64+ requestBody:
65+ content:
66+ application/json;charset=utf-8:
67+ schema:
68+ type: string
69+ responses:
70+ "200":
71+ content:
72+ application/json;charset=utf-8:
73+ schema:
74+ example: []
75+ items: {}
76+ maxItems: 0
77+ type: array
78+ description: ""
79+ "400":
80+ description: Invalid `body`
81+ security:
82+ - oauth: []
83+components:
84+ securitySchemes:
85+ sign-out-oauth:
86+ in: cookie
87+ name: _SESSION
88+ type: apiKey
89+ description: Session cookie
90+ get-oauth:
91+ in: cookie
92+ name: _SESSION
93+ type: apiKey
94+ description: Session cookie
95+ oauth:
96+ scheme: Basic
97+ type: http
98+ oauth-token:
99+ scheme: Bearer
100+ type: http
101+ description: Bearer token
102+ oauth-token-client-only:
103+ scheme: Bearer
104+ type: http
105+ description: Bearer token
106+ spa-oauth:
107+ in: cookie
108+ name: _SESSION
109+ type: apiKey
110+ description: Session cookie
test/golden/common/security-scheme/b.yamlnew file+110
@@ -0,0 +1,110 @@
1+openapi: 3.0.0
2+info:
3+ version: ""
4+ title: ""
5+paths:
6+ /oauth/sign_out:
7+ get:
8+ parameters:
9+ - required: false
10+ schema:
11+ type: string
12+ in: query
13+ name: redirect
14+ responses:
15+ "302":
16+ content:
17+ application/x-www-form-urlencoded: {}
18+ headers:
19+ Location:
20+ schema:
21+ type: string
22+ set-cookie:
23+ schema:
24+ type: string
25+ description: ""
26+ "400":
27+ description: Invalid `redirect`
28+ security:
29+ - sign-out-oauth: []
30+ /oauth/authorize:
31+ get:
32+ responses:
33+ "302":
34+ content:
35+ application/x-www-form-urlencoded: {}
36+ headers:
37+ Location:
38+ schema:
39+ type: string
40+ description: ""
41+ security:
42+ - get-oauth: []
43+ - spa-oauth: []
44+ /oauth/token:
45+ post:
46+ requestBody:
47+ content:
48+ application/x-www-form-urlencoded:
49+ schema:
50+ type: string
51+ responses:
52+ "200":
53+ content:
54+ application/json;charset=utf-8:
55+ schema:
56+ type: string
57+ description: ""
58+ "400":
59+ description: Invalid `body`
60+ security:
61+ - oauth: []
62+ /oauth/check:
63+ post:
64+ requestBody:
65+ content:
66+ application/json;charset=utf-8:
67+ schema:
68+ type: string
69+ responses:
70+ "200":
71+ content:
72+ application/json;charset=utf-8:
73+ schema:
74+ example: []
75+ items: {}
76+ maxItems: 0
77+ type: array
78+ description: ""
79+ "400":
80+ description: Invalid `body`
81+ security:
82+ - spa-oauth: []
83+components:
84+ securitySchemes:
85+ sign-out-oauth:
86+ in: cookie
87+ name: _SESSION
88+ type: apiKey
89+ description: Session cookie
90+ get-oauth:
91+ in: cookie
92+ name: _SESSION
93+ type: apiKey
94+ description: Session cookie
95+ oauth:
96+ scheme: Basic
97+ type: http
98+ oauth-token:
99+ scheme: Bearer
100+ type: http
101+ description: Bearer token
102+ oauth-token-client-only:
103+ scheme: Bearer
104+ type: http
105+ description: Bearer token
106+ spa-oauth:
107+ in: cookie
108+ name: _SESSION
109+ type: apiKey
110+ description: Session cookie
test/golden/common/security-scheme/trace-tree.yamlnew file+9
@@ -0,0 +1,9 @@
1+Left:
2+ AtPath (ProdCons {producer = "/oauth/check", consumer = "/oauth/check"}):
3+ InOperation PostMethod:
4+ SecurityRequirementStep 0:
5+ SecuritySchemeStep "oauth": SecuritySchemeNotMatched
6+ AtPath (ProdCons {producer = "/oauth/sign_out", consumer = "/oauth/sign_out"}):
7+ InOperation GetMethod:
8+ SecurityRequirementStep 1:
9+ SecuritySchemeStep "oauth": SecuritySchemeNotMatched