Add security definitions to the OpenAPI output
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user