From e0ba6b6d1c54cb8b89b0b917f1c54312ca16c199 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 13 Jul 2022 22:47:09 -0500 Subject: [PATCH] Add security definitions to the OpenAPI output --- CHANGELOG.md | 1 + postgrest.cabal | 1 + src/PostgREST/Config.hs | 3 ++ src/PostgREST/OpenAPI.hs | 16 ++++++- test/io/configs/expected/aliases.config | 1 + .../configs/expected/boolean-numeric.config | 1 + .../io/configs/expected/boolean-string.config | 1 + test/io/configs/expected/defaults.config | 1 + ...efaults-with-db-other-authenticator.config | 1 + .../expected/no-defaults-with-db.config | 1 + test/io/configs/expected/no-defaults.config | 1 + test/io/configs/expected/types.config | 1 + test/io/configs/no-defaults-env.yaml | 1 + test/io/configs/no-defaults.config | 1 + test/io/db_config.sql | 1 + test/spec/Feature/OpenApi/OpenApiSpec.hs | 12 +++++ .../Feature/OpenApi/SecurityOpenApiSpec.hs | 44 +++++++++++++++++++ test/spec/Main.hs | 6 +++ test/spec/SpecHelper.hs | 4 ++ 19 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 test/spec/Feature/OpenApi/SecurityOpenApiSpec.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index a6109b441..58961ab34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). + Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`. + Resource embedding works and the embedded rows will go into the `properties` key + In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter. + - #1082, Add security definitions to the OpenAPI output - @laurenceisla ### Fixed diff --git a/postgrest.cabal b/postgrest.cabal index 7cfb7a55f..746a2ac83 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -189,6 +189,7 @@ test-suite spec Feature.OpenApi.OpenApiSpec Feature.OpenApi.ProxySpec Feature.OpenApi.RootSpec + Feature.OpenApi.SecurityOpenApiSpec Feature.OptionsSpec Feature.Query.AndOrParamsSpec Feature.Query.DeleteSpec diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index f070575eb..331e82ffb 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -88,6 +88,7 @@ data AppConfig = AppConfig , configJwtSecretIsBase64 :: Bool , configLogLevel :: LogLevel , configOpenApiMode :: OpenAPIMode + , configOpenApiSecurityActive :: Bool , configOpenApiServerProxyUri :: Maybe Text , configRawMediaTypes :: [MediaType] , configServerHost :: Text @@ -143,6 +144,7 @@ toText conf = ,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64) ,("log-level", q . dumpLogLevel . configLogLevel) ,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode) + ,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes) ,("server-host", q . configServerHost) @@ -238,6 +240,7 @@ parser optPath env dbSettings = (optBool "secret-is-base64")) <*> parseLogLevel "log-level" <*> parseOpenAPIMode "openapi-mode" + <*> (fromMaybe False <$> optBool "openapi-security-active") <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types") <*> (fromMaybe "!4" <$> optString "server-host") diff --git a/src/PostgREST/OpenAPI.hs b/src/PostgREST/OpenAPI.hs index 139b9757e..d6e73d2e9 100644 --- a/src/PostgREST/OpenAPI.hs +++ b/src/PostgREST/OpenAPI.hs @@ -50,6 +50,7 @@ encode conf dbStructure tables procs schemaDescription = (snd <$> HM.toList tables) (proxyUri conf) schemaDescription + (configOpenApiSecurityActive conf) makeMimeList :: [MediaType] -> MimeList makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs @@ -313,6 +314,14 @@ makePathItems :: [ProcDescription] -> [Table] -> InsOrdHashMap FilePath PathItem makePathItems pds ti = fromList $ makeRootPathItem : fmap makePathItem ti ++ fmap makeProcPathItem pds +makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions +makeSecurityDefinitions secName allow + | allow = SecurityDefinitions (fromList [(secName, SecurityScheme secSchType secSchDescription)]) + | otherwise = mempty + where + secSchType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader) + secSchDescription = Just "Add the token prepending \"Bearer \" (without quotes) to it" + escapeHostName :: Text -> Text escapeHostName "*" = "0.0.0.0" escapeHostName "*4" = "0.0.0.0" @@ -321,8 +330,8 @@ escapeHostName "*6" = "0.0.0.0" escapeHostName "!6" = "0.0.0.0" escapeHostName h = h -postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Swagger -postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger) +postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger +postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger) & basePath ?~ T.unpack b & schemes ?~ [s'] & info .~ ((mempty :: Info) @@ -338,10 +347,13 @@ postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger) & paths .~ makePathItems pds ti & produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV] & consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV] + & securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef + & security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef] where s' = if s == "http" then Http else Https h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p)) d = fromMaybe "This is a dynamic API generated by PostgREST" sd + securityDefName = "JWT" pickProxy :: Maybe Text -> Maybe Proxy pickProxy proxy diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index ef5c3a5b7..24282ea9c 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -19,6 +19,7 @@ jwt-secret = "" jwt-secret-is-base64 = true log-level = "error" openapi-mode = "follow-privileges" +openapi-security-active = false openapi-server-proxy-uri = "" raw-media-types = "" server-host = "!4" diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index cd6fc6e2e..da7be5a65 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -19,6 +19,7 @@ jwt-secret = "" jwt-secret-is-base64 = true log-level = "error" openapi-mode = "follow-privileges" +openapi-security-active = false openapi-server-proxy-uri = "" raw-media-types = "" server-host = "!4" diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index cd6fc6e2e..da7be5a65 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -19,6 +19,7 @@ jwt-secret = "" jwt-secret-is-base64 = true log-level = "error" openapi-mode = "follow-privileges" +openapi-security-active = false openapi-server-proxy-uri = "" raw-media-types = "" server-host = "!4" diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index b34105bc9..903f194e0 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -19,6 +19,7 @@ jwt-secret = "" jwt-secret-is-base64 = false log-level = "error" openapi-mode = "follow-privileges" +openapi-security-active = false openapi-server-proxy-uri = "" raw-media-types = "" server-host = "!4" diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index 5f9d38fb1..3903c53a9 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -19,6 +19,7 @@ jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret-is-base64 = true log-level = "info" openapi-mode = "disabled" +openapi-security-active = false openapi-server-proxy-uri = "https://otherexample.org/api" raw-media-types = "application/vnd.pgrst.other-db-config" server-host = "0.0.0.0" diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 40eee3c87..7cb1b8574 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -19,6 +19,7 @@ jwt-secret = "OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE" jwt-secret-is-base64 = false log-level = "info" openapi-mode = "ignore-privileges" +openapi-security-active = true openapi-server-proxy-uri = "https://example.org/api" raw-media-types = "application/vnd.pgrst.db-config" server-host = "0.0.0.0" diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index c438a5f61..d83c30915 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -19,6 +19,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret-is-base64 = true log-level = "info" openapi-mode = "ignore-privileges" +openapi-security-active = true openapi-server-proxy-uri = "https://postgrest.org" raw-media-types = "application/vnd.pgrst.config" server-host = "0.0.0.0" diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index 5967f50cc..718072452 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -19,6 +19,7 @@ jwt-secret = "" jwt-secret-is-base64 = false log-level = "error" openapi-mode = "follow-privileges" +openapi-security-active = false openapi-server-proxy-uri = "" raw-media-types = "" server-host = "!4" diff --git a/test/io/configs/no-defaults-env.yaml b/test/io/configs/no-defaults-env.yaml index bc7790da0..06002f463 100644 --- a/test/io/configs/no-defaults-env.yaml +++ b/test/io/configs/no-defaults-env.yaml @@ -22,6 +22,7 @@ PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5 PGRST_JWT_SECRET_IS_BASE64: true PGRST_LOG_LEVEL: info PGRST_OPENAPI_MODE: 'ignore-privileges' +PGRST_OPENAPI_SECURITY_ACTIVE: true PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org' PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config PGRST_SERVER_HOST: 0.0.0.0 diff --git a/test/io/configs/no-defaults.config b/test/io/configs/no-defaults.config index 65153f1e3..feac7aae5 100644 --- a/test/io/configs/no-defaults.config +++ b/test/io/configs/no-defaults.config @@ -19,6 +19,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret-is-base64 = true log-level = "info" openapi-mode = "ignore-privileges" +openapi-security-active = true openapi-server-proxy-uri = "https://postgrest.org" raw-media-types = "application/vnd.pgrst.config" server-host = "0.0.0.0" diff --git a/test/io/db_config.sql b/test/io/db_config.sql index d79c3b47b..e71d11440 100644 --- a/test/io/db_config.sql +++ b/test/io/db_config.sql @@ -55,6 +55,7 @@ ALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_hea ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100'; ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other'; ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled'; +ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false'; -- limited authenticator used for failed schema cache loads CREATE ROLE limited_authenticator LOGIN NOINHERIT; diff --git a/test/spec/Feature/OpenApi/OpenApiSpec.hs b/test/spec/Feature/OpenApi/OpenApiSpec.hs index 50d14ea46..0336da367 100644 --- a/test/spec/Feature/OpenApi/OpenApiSpec.hs +++ b/test/spec/Feature/OpenApi/OpenApiSpec.hs @@ -636,3 +636,15 @@ spec actualPgVersion = describe "OpenAPI" $ do liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|] + describe "Security" $ + it "does not include security or security definitions by default" $ do + r <- simpleBody <$> get "/" + + let sec = r ^? key "security" + secDef = r ^? key "securityDefinitions" + + liftIO $ do + + sec `shouldBe` Nothing + + secDef `shouldBe` Nothing diff --git a/test/spec/Feature/OpenApi/SecurityOpenApiSpec.hs b/test/spec/Feature/OpenApi/SecurityOpenApiSpec.hs new file mode 100644 index 000000000..1332be058 --- /dev/null +++ b/test/spec/Feature/OpenApi/SecurityOpenApiSpec.hs @@ -0,0 +1,44 @@ +module Feature.OpenApi.SecurityOpenApiSpec where + +import Control.Lens ((^?)) + +import Data.Aeson.Lens +import Data.Aeson.QQ + +import Network.Wai (Application) +import Network.Wai.Test (SResponse (..)) + +import Test.Hspec hiding (pendingWith) +import Test.Hspec.Wai + +import Protolude hiding (get) + +spec :: SpecWith ((), Application) +spec = + describe "Security active" $ + it "includes security and security definitions" $ do + r <- simpleBody <$> get "/" + + let sec = r ^? key "security" + secDef = r ^? key "securityDefinitions" + + liftIO $ do + + sec `shouldBe` Just + [aesonQQ| + [ + { "JWT": [] } + ] + |] + + secDef `shouldBe` Just + [aesonQQ| + { + "JWT": { + "description": "Add the token prepending \"Bearer \" (without quotes) to it", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } + |] diff --git a/test/spec/Main.hs b/test/spec/Main.hs index ab8120def..60cab2a86 100644 --- a/test/spec/Main.hs +++ b/test/spec/Main.hs @@ -34,6 +34,7 @@ import qualified Feature.OpenApi.IgnorePrivOpenApiSpec import qualified Feature.OpenApi.OpenApiSpec import qualified Feature.OpenApi.ProxySpec import qualified Feature.OpenApi.RootSpec +import qualified Feature.OpenApi.SecurityOpenApiSpec import qualified Feature.OptionsSpec import qualified Feature.Query.AndOrParamsSpec import qualified Feature.Query.DeleteSpec @@ -95,6 +96,7 @@ main = do let withApp = app testCfg maxRowsApp = app testMaxRowsCfg disabledOpenApi = app testDisabledOpenApiCfg + securityOpenApi = app testSecurityOpenApiCfg proxyApp = app testProxyCfg noAnonApp = app testCfgNoAnon noJwtApp = app testCfgNoJWT @@ -171,6 +173,10 @@ main = do parallel $ before proxyApp $ describe "Feature.OpenApi.ProxySpec" Feature.OpenApi.ProxySpec.spec + -- this test runs with openapi-security-active set to true + parallel $ before securityOpenApi $ + describe "Feature.OpenApi.SecurityOpenApiSpec" Feature.OpenApi.SecurityOpenApiSpec.spec + -- this test runs without an anonymous role parallel $ before noAnonApp $ describe "Feature.Auth.NoAnonSpec" Feature.Auth.NoAnonSpec.spec diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index 4145f012e..cba7d2bc5 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -93,6 +93,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in , configJwtSecretIsBase64 = False , configLogLevel = LogCrit , configOpenApiMode = OAFollowPriv + , configOpenApiSecurityActive = False , configOpenApiServerProxyUri = Nothing , configRawMediaTypes = [] , configServerHost = "localhost" @@ -134,6 +135,9 @@ testIgnorePrivOpenApiCfg = baseCfg { configOpenApiMode = OAIgnorePriv, configDbS testProxyCfg :: AppConfig testProxyCfg = baseCfg { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" } +testSecurityOpenApiCfg :: AppConfig +testSecurityOpenApiCfg = baseCfg { configOpenApiSecurityActive = True } + testCfgBinaryJWT :: AppConfig testCfgBinaryJWT = let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in