feat: add openapi-mode config (#1881)

* openapi-mode="follow-acl"(default): follows access control for the
JWT role.
* openapi-mode="ignore-acl": ignores access control for the JWT role.
* openapi-mode="disabled": disables OpenAPI output, the root endpoint
replies with 404 Not Found.
This commit is contained in:
Steve Chavez
2021-06-25 19:20:04 -05:00
committed by GitHub
parent 214a92f207
commit 41d119b19f
20 changed files with 144 additions and 12 deletions
+2
View File
@@ -174,10 +174,12 @@ test-suite spec
Feature.ConcurrentSpec
Feature.CorsSpec
Feature.DeleteSpec
Feature.DisabledOpenApiSpec
Feature.EmbedDisambiguationSpec
Feature.ExtraSearchPathSpec
Feature.HtmlRawOutputSpec
Feature.InsertSpec
Feature.IgnoreAclOpenApiSpec
Feature.JsonOperatorSpec
Feature.MultipleSchemaSpec
Feature.NoJwtSpec
+15 -6
View File
@@ -53,7 +53,8 @@ import qualified PostgREST.Request.DbRequestBuilder as ReqBuilder
import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..),
LogLevel (..))
LogLevel (..),
OpenAPIMode (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.ContentType (ContentType (..))
import PostgREST.DbStructure (DbStructure (..),
@@ -463,11 +464,19 @@ handleInvoke invMethod proc context@RequestContext{..} = do
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do
body <-
lift $
OpenAPI.encode conf dbStructure
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
lift $ case configOpenApiMode of
OAFollowACL ->
OpenAPI.encode conf dbStructure
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
OAIgnoreACL ->
OpenAPI.encode conf dbStructure
(DbStructure.dbTables dbStructure)
(DbStructure.dbProcs dbStructure)
<$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
OADisabled ->
pure mempty
return $
Wai.responseLBS HTTP.status200
+4
View File
@@ -192,6 +192,10 @@ exampleConfigFile =
|## when none is provided, 660 is applied by default
|# server-unix-socket-mode = "660"
|
|## determine if swagger output should follow or ignore ACL constraints or be disabled entirely
|## admitted values: follow-acl, ignore-acl, disabled
|openapi-mode = "follow-acl"
|
|## base url for swagger output
|openapi-server-proxy-uri = ""
|
+21
View File
@@ -16,6 +16,7 @@ module PostgREST.Config
, JSPath
, JSPathExp(..)
, LogLevel(..)
, OpenAPIMode(..)
, Proxy(..)
, toText
, isMalformedProxyUri
@@ -85,6 +86,7 @@ data AppConfig = AppConfig
, configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool
, configLogLevel :: LogLevel
, configOpenApiMode :: OpenAPIMode
, configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [B.ByteString]
, configServerHost :: Text
@@ -101,6 +103,14 @@ instance Show LogLevel where
show LogWarn = "warn"
show LogInfo = "info"
data OpenAPIMode = OAFollowACL | OAIgnoreACL | OADisabled
deriving Eq
instance Show OpenAPIMode where
show OAFollowACL = "follow-acl"
show OAIgnoreACL = "ignore-acl"
show OADisabled = "disabled"
-- | Dump the config
toText :: AppConfig -> Text
toText conf =
@@ -127,6 +137,7 @@ toText conf =
,("jwt-secret", q . toS . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("log-level", q . show . configLogLevel)
,("openapi-mode", q . show . configOpenApiMode)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
,("server-host", q . configServerHost)
@@ -220,6 +231,7 @@ parser optPath env dbSettings =
(optBool "jwt-secret-is-base64")
(optBool "secret-is-base64"))
<*> parseLogLevel "log-level"
<*> parseOpenAPIMode "openapi-mode"
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
<*> (fromMaybe "!4" <$> optString "server-host")
@@ -247,6 +259,15 @@ parser optPath env dbSettings =
then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
else pure fileMode
parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode
parseOpenAPIMode k =
optString k >>= \case
Nothing -> pure OAFollowACL
Just "follow-acl" -> pure OAFollowACL
Just "ignore-acl" -> pure OAIgnoreACL
Just "disabled" -> pure OADisabled
Just _ -> fail "Invalid openapi-mode. Check your configuration."
parseOpenAPIServerProxyURI :: C.Key -> C.Parser C.Config (Maybe Text)
parseOpenAPIServerProxyURI k =
optString k >>= \case
+2 -2
View File
@@ -40,8 +40,8 @@ import PostgREST.ContentType
import Protolude hiding (Proxy, get, toS)
import Protolude.Conv (toS)
encode :: AppConfig -> DbStructure -> [Table] -> Maybe Text -> HashMap.HashMap k [ProcDescription] -> LBS.ByteString
encode conf dbStructure tables schemaDescription procs =
encode :: AppConfig -> DbStructure -> [Table] -> HashMap.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
encode conf dbStructure tables procs schemaDescription =
JSON.encode $
postgrestSpec
(dbRelationships dbStructure)
+5 -3
View File
@@ -44,7 +44,8 @@ import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies)
import PostgREST.Config (AppConfig (..))
import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.ContentType (ContentType (..))
import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure.Identifiers (FieldName,
@@ -313,8 +314,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
in
case path of
[] -> case configDbRootSpec of
Just (QualifiedIdentifier pSch pName) -> TargetProc (callFindProc (if pSch == mempty then schema else pSch) pName) True
Nothing -> TargetDefaultSpec schema
Just (QualifiedIdentifier pSch pName) -> TargetProc (callFindProc (if pSch == mempty then schema else pSch) pName) True
Nothing | configOpenApiMode == OADisabled -> TargetUnknown
| otherwise -> TargetDefaultSpec schema
[table] -> TargetIdent $ QualifiedIdentifier schema table
["rpc", pName] -> TargetProc (callFindProc schema pName) False
_ -> TargetUnknown
+20
View File
@@ -0,0 +1,20 @@
module Feature.DisabledOpenApiSpec where
import Network.HTTP.Types
import Network.Wai (Application)
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Protolude
spec :: SpecWith ((), Application)
spec =
describe "Disabled OpenApi" $ do
it "does not accept application/openapi+json and responds with 415" $
request methodGet "/"
[("Accept","application/openapi+json")] "" `shouldRespondWith` 415
it "accepts application/json and responds with 404" $
request methodGet "/"
[("Accept","application/json")] "" `shouldRespondWith` 404
+43
View File
@@ -0,0 +1,43 @@
module Feature.IgnoreAclOpenApiSpec where
import Control.Lens ((^?))
import Data.Aeson.Lens
import Data.Aeson.QQ
import Network.HTTP.Types
import Network.Wai (Application)
import Network.Wai.Test (SResponse (..))
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application)
spec = describe "OpenAPI Ignore ACL" $ do
it "root path returns a valid openapi spec" $ do
validateOpenApiResponse [("Accept", "application/openapi+json")]
request methodHead "/" (acceptHdrs "application/openapi+json") ""
`shouldRespondWith` "" { matchStatus = 200 }
describe "table" $ do
it "includes privileged table even if user does not have permission" $ do
r <- simpleBody <$> get "/"
let tableTag = r ^? key "paths" . key "/authors_only"
. key "post" . key "tags"
. nth 0
liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]
describe "RPC" $ do
it "includes privileged function even if user does not have permission" $ do
r <- simpleBody <$> get "/"
let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"
. key "post" . key "tags"
. nth 0
liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]
+12
View File
@@ -28,9 +28,11 @@ import qualified Feature.BinaryJwtSecretSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
import qualified Feature.DisabledOpenApiSpec
import qualified Feature.EmbedDisambiguationSpec
import qualified Feature.ExtraSearchPathSpec
import qualified Feature.HtmlRawOutputSpec
import qualified Feature.IgnoreAclOpenApiSpec
import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec
import qualified Feature.MultipleSchemaSpec
@@ -92,6 +94,8 @@ main = do
let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg
disabledOpenApi = app testDisabledOpenApiCfg
ignoreAclOpenApi = app testIgnoreAclOpenApiCfg
proxyApp = app testProxyCfg
noJwtApp = app testCfgNoJWT
binaryJwtApp = app testCfgBinaryJWT
@@ -152,6 +156,14 @@ main = do
parallel $ before unicodeApp $
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec
-- this test runs with openapi-mode set to disabled
parallel $ before disabledOpenApi $
describe "Feature.DisabledOpenApiSpec" Feature.DisabledOpenApiSpec.spec
-- this test runs with openapi-mode set to ignore-acl
parallel $ before ignoreAclOpenApi $
describe "Feature.IgnoreAclOpenApiSpec" Feature.IgnoreAclOpenApiSpec.spec
-- this test runs with a proxy
parallel $ before proxyApp $
describe "Feature.ProxySpec" Feature.ProxySpec.spec
+10 -1
View File
@@ -24,7 +24,9 @@ import Text.Heredoc
import PostgREST.Config (AppConfig (..),
JSPathExp (..),
LogLevel (..), parseSecret)
LogLevel (..),
OpenAPIMode (..),
parseSecret)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
import Protolude hiding (toS)
import Protolude.Conv (toS)
@@ -94,6 +96,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configJwtSecret = secret
, configJwtSecretIsBase64 = False
, configLogLevel = LogCrit
, configOpenApiMode = OAFollowACL
, configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = []
, configServerHost = "localhost"
@@ -122,6 +125,12 @@ testUnicodeCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["
testMaxRowsCfg :: Text -> AppConfig
testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 }
testDisabledOpenApiCfg :: Text -> AppConfig
testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled }
testIgnoreAclOpenApiCfg :: Text -> AppConfig
testIgnoreAclOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OAIgnoreACL }
testProxyCfg :: Text -> AppConfig
testProxyCfg testDbConn = (testCfg testDbConn) { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" }
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"aliased\""
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = ""
raw-media-types = ""
server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = ""
raw-media-types = ""
server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = ""
raw-media-types = ""
server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = ""
raw-media-types = ""
server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"other\".\"role\""
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true
log-level = "info"
openapi-mode = "ignore-acl"
openapi-server-proxy-uri = "https://otherexample.org/api"
raw-media-types = "application/vnd.pgrst.other-db-config"
server-host = "0.0.0.0"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"a\".\"role\""
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true
log-level = "info"
openapi-mode = "ignore-acl"
openapi-server-proxy-uri = "https://example.org/api"
raw-media-types = "application/vnd.pgrst.db-config"
server-host = "0.0.0.0"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"user\"[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true
log-level = "info"
openapi-mode = "ignore-acl"
openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config"
server-host = "0.0.0.0"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = ""
raw-media-types = ""
server-host = "!4"
@@ -19,6 +19,7 @@ PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
PGRST_JWT_SECRET_IS_BASE64: true
PGRST_LOG_LEVEL: info
PGRST_OPENAPI_MODE: 'ignore-acl'
PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config
PGRST_SERVER_HOST: 0.0.0.0
+1
View File
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".user[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true
log-level = "info"
openapi-mode = "ignore-acl"
openapi-server-proxy-uri = "https://postgrest.org"
raw-media-types = "application/vnd.pgrst.config"
server-host = "0.0.0.0"