Add security definitions to the OpenAPI output

This commit is contained in:
Laurence Isla
2022-07-13 22:47:09 -05:00
committed by GitHub
parent bdf1cbe111
commit e0ba6b6d1c
19 changed files with 96 additions and 2 deletions
+1
View File
@@ -34,6 +34,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`. + Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
+ Resource embedding works and the embedded rows will go into the `properties` key + 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. + 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 ### Fixed
+1
View File
@@ -189,6 +189,7 @@ test-suite spec
Feature.OpenApi.OpenApiSpec Feature.OpenApi.OpenApiSpec
Feature.OpenApi.ProxySpec Feature.OpenApi.ProxySpec
Feature.OpenApi.RootSpec Feature.OpenApi.RootSpec
Feature.OpenApi.SecurityOpenApiSpec
Feature.OptionsSpec Feature.OptionsSpec
Feature.Query.AndOrParamsSpec Feature.Query.AndOrParamsSpec
Feature.Query.DeleteSpec Feature.Query.DeleteSpec
+3
View File
@@ -88,6 +88,7 @@ data AppConfig = AppConfig
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
, configOpenApiMode :: OpenAPIMode , configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [MediaType] , configRawMediaTypes :: [MediaType]
, configServerHost :: Text , configServerHost :: Text
@@ -143,6 +144,7 @@ toText conf =
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64) ,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("log-level", q . dumpLogLevel . configLogLevel) ,("log-level", q . dumpLogLevel . configLogLevel)
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode) ,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes) ,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
,("server-host", q . configServerHost) ,("server-host", q . configServerHost)
@@ -238,6 +240,7 @@ parser optPath env dbSettings =
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> parseOpenAPIMode "openapi-mode" <*> parseOpenAPIMode "openapi-mode"
<*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types") <*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
<*> (fromMaybe "!4" <$> optString "server-host") <*> (fromMaybe "!4" <$> optString "server-host")
+14 -2
View File
@@ -50,6 +50,7 @@ encode conf dbStructure tables procs schemaDescription =
(snd <$> HM.toList tables) (snd <$> HM.toList tables)
(proxyUri conf) (proxyUri conf)
schemaDescription schemaDescription
(configOpenApiSecurityActive conf)
makeMimeList :: [MediaType] -> MimeList makeMimeList :: [MediaType] -> MimeList
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
@@ -313,6 +314,14 @@ makePathItems :: [ProcDescription] -> [Table] -> InsOrdHashMap FilePath PathItem
makePathItems pds ti = fromList $ makeRootPathItem : makePathItems pds ti = fromList $ makeRootPathItem :
fmap makePathItem ti ++ fmap makeProcPathItem pds 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 :: Text -> Text
escapeHostName "*" = "0.0.0.0" escapeHostName "*" = "0.0.0.0"
escapeHostName "*4" = "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 "!6" = "0.0.0.0"
escapeHostName h = h escapeHostName h = h
postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Swagger postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger) postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
& basePath ?~ T.unpack b & basePath ?~ T.unpack b
& schemes ?~ [s'] & schemes ?~ [s']
& info .~ ((mempty :: Info) & info .~ ((mempty :: Info)
@@ -338,10 +347,13 @@ postgrestSpec rels pds ti (s, h, p, b) sd = (mempty :: Swagger)
& paths .~ makePathItems pds ti & paths .~ makePathItems pds ti
& produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV] & produces .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
& consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV] & consumes .~ makeMimeList [MTApplicationJSON, MTSingularJSON, MTTextCSV]
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
& security .~ [SecurityRequirement (fromList [(securityDefName, [])]) | allowSecurityDef]
where where
s' = if s == "http" then Http else Https s' = if s == "http" then Http else Https
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p)) h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
d = fromMaybe "This is a dynamic API generated by PostgREST" sd d = fromMaybe "This is a dynamic API generated by PostgREST" sd
securityDefName = "JWT"
pickProxy :: Maybe Text -> Maybe Proxy pickProxy :: Maybe Text -> Maybe Proxy
pickProxy proxy pickProxy proxy
+1
View File
@@ -19,6 +19,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -19,6 +19,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -19,6 +19,7 @@ jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
+1
View File
@@ -19,6 +19,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
log-level = "error" log-level = "error"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -19,6 +19,7 @@ jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "disabled" openapi-mode = "disabled"
openapi-security-active = false
openapi-server-proxy-uri = "https://otherexample.org/api" openapi-server-proxy-uri = "https://otherexample.org/api"
raw-media-types = "application/vnd.pgrst.other-db-config" raw-media-types = "application/vnd.pgrst.other-db-config"
server-host = "0.0.0.0" server-host = "0.0.0.0"
@@ -19,6 +19,7 @@ jwt-secret = "OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE"
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
log-level = "info" log-level = "info"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://example.org/api" openapi-server-proxy-uri = "https://example.org/api"
raw-media-types = "application/vnd.pgrst.db-config" raw-media-types = "application/vnd.pgrst.db-config"
server-host = "0.0.0.0" server-host = "0.0.0.0"
@@ -19,6 +19,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config" raw-media-types = "application/vnd.pgrst.config"
server-host = "0.0.0.0" server-host = "0.0.0.0"
+1
View File
@@ -19,6 +19,7 @@ jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
log-level = "error" log-level = "error"
openapi-mode = "follow-privileges" openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
+1
View File
@@ -22,6 +22,7 @@ PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
PGRST_JWT_SECRET_IS_BASE64: true PGRST_JWT_SECRET_IS_BASE64: true
PGRST_LOG_LEVEL: info PGRST_LOG_LEVEL: info
PGRST_OPENAPI_MODE: 'ignore-privileges' PGRST_OPENAPI_MODE: 'ignore-privileges'
PGRST_OPENAPI_SECURITY_ACTIVE: true
PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org' PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config
PGRST_SERVER_HOST: 0.0.0.0 PGRST_SERVER_HOST: 0.0.0.0
+1
View File
@@ -19,6 +19,7 @@ jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-privileges" openapi-mode = "ignore-privileges"
openapi-security-active = true
openapi-server-proxy-uri = "https://postgrest.org" openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config" raw-media-types = "application/vnd.pgrst.config"
server-host = "0.0.0.0" server-host = "0.0.0.0"
+1
View File
@@ -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_max_rows = '100';
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other'; 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_mode = 'disabled';
ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false';
-- limited authenticator used for failed schema cache loads -- limited authenticator used for failed schema cache loads
CREATE ROLE limited_authenticator LOGIN NOINHERIT; CREATE ROLE limited_authenticator LOGIN NOINHERIT;
+12
View File
@@ -636,3 +636,15 @@ spec actualPgVersion = describe "OpenAPI" $ do
liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|] 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
@@ -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"
}
}
|]
+6
View File
@@ -34,6 +34,7 @@ import qualified Feature.OpenApi.IgnorePrivOpenApiSpec
import qualified Feature.OpenApi.OpenApiSpec import qualified Feature.OpenApi.OpenApiSpec
import qualified Feature.OpenApi.ProxySpec import qualified Feature.OpenApi.ProxySpec
import qualified Feature.OpenApi.RootSpec import qualified Feature.OpenApi.RootSpec
import qualified Feature.OpenApi.SecurityOpenApiSpec
import qualified Feature.OptionsSpec import qualified Feature.OptionsSpec
import qualified Feature.Query.AndOrParamsSpec import qualified Feature.Query.AndOrParamsSpec
import qualified Feature.Query.DeleteSpec import qualified Feature.Query.DeleteSpec
@@ -95,6 +96,7 @@ main = do
let withApp = app testCfg let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg maxRowsApp = app testMaxRowsCfg
disabledOpenApi = app testDisabledOpenApiCfg disabledOpenApi = app testDisabledOpenApiCfg
securityOpenApi = app testSecurityOpenApiCfg
proxyApp = app testProxyCfg proxyApp = app testProxyCfg
noAnonApp = app testCfgNoAnon noAnonApp = app testCfgNoAnon
noJwtApp = app testCfgNoJWT noJwtApp = app testCfgNoJWT
@@ -171,6 +173,10 @@ main = do
parallel $ before proxyApp $ parallel $ before proxyApp $
describe "Feature.OpenApi.ProxySpec" Feature.OpenApi.ProxySpec.spec 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 -- this test runs without an anonymous role
parallel $ before noAnonApp $ parallel $ before noAnonApp $
describe "Feature.Auth.NoAnonSpec" Feature.Auth.NoAnonSpec.spec describe "Feature.Auth.NoAnonSpec" Feature.Auth.NoAnonSpec.spec
+4
View File
@@ -93,6 +93,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configOpenApiMode = OAFollowPriv , configOpenApiMode = OAFollowPriv
, configOpenApiSecurityActive = False
, configOpenApiServerProxyUri = Nothing , configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = [] , configRawMediaTypes = []
, configServerHost = "localhost" , configServerHost = "localhost"
@@ -134,6 +135,9 @@ testIgnorePrivOpenApiCfg = baseCfg { configOpenApiMode = OAIgnorePriv, configDbS
testProxyCfg :: AppConfig testProxyCfg :: AppConfig
testProxyCfg = baseCfg { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" } testProxyCfg = baseCfg { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" }
testSecurityOpenApiCfg :: AppConfig
testSecurityOpenApiCfg = baseCfg { configOpenApiSecurityActive = True }
testCfgBinaryJWT :: AppConfig testCfgBinaryJWT :: AppConfig
testCfgBinaryJWT = testCfgBinaryJWT =
let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in let secret = Just . B64.decodeLenient $ "cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=" in