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.ConcurrentSpec
Feature.CorsSpec Feature.CorsSpec
Feature.DeleteSpec Feature.DeleteSpec
Feature.DisabledOpenApiSpec
Feature.EmbedDisambiguationSpec Feature.EmbedDisambiguationSpec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
Feature.HtmlRawOutputSpec Feature.HtmlRawOutputSpec
Feature.InsertSpec Feature.InsertSpec
Feature.IgnoreAclOpenApiSpec
Feature.JsonOperatorSpec Feature.JsonOperatorSpec
Feature.MultipleSchemaSpec Feature.MultipleSchemaSpec
Feature.NoJwtSpec Feature.NoJwtSpec
+15 -6
View File
@@ -53,7 +53,8 @@ import qualified PostgREST.Request.DbRequestBuilder as ReqBuilder
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
LogLevel (..)) LogLevel (..),
OpenAPIMode (..))
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.ContentType (ContentType (..)) import PostgREST.ContentType (ContentType (..))
import PostgREST.DbStructure (DbStructure (..), import PostgREST.DbStructure (DbStructure (..),
@@ -463,11 +464,19 @@ handleInvoke invMethod proc context@RequestContext{..} = do
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do
body <- body <-
lift $ lift $ case configOpenApiMode of
OpenAPI.encode conf dbStructure OAFollowACL ->
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements) OpenAPI.encode conf dbStructure
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements) <$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.accessibleProcs 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 $ return $
Wai.responseLBS HTTP.status200 Wai.responseLBS HTTP.status200
+4
View File
@@ -192,6 +192,10 @@ exampleConfigFile =
|## when none is provided, 660 is applied by default |## when none is provided, 660 is applied by default
|# server-unix-socket-mode = "660" |# 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 |## base url for swagger output
|openapi-server-proxy-uri = "" |openapi-server-proxy-uri = ""
| |
+21
View File
@@ -16,6 +16,7 @@ module PostgREST.Config
, JSPath , JSPath
, JSPathExp(..) , JSPathExp(..)
, LogLevel(..) , LogLevel(..)
, OpenAPIMode(..)
, Proxy(..) , Proxy(..)
, toText , toText
, isMalformedProxyUri , isMalformedProxyUri
@@ -85,6 +86,7 @@ data AppConfig = AppConfig
, configJwtSecret :: Maybe B.ByteString , configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
, configOpenApiMode :: OpenAPIMode
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [B.ByteString] , configRawMediaTypes :: [B.ByteString]
, configServerHost :: Text , configServerHost :: Text
@@ -101,6 +103,14 @@ instance Show LogLevel where
show LogWarn = "warn" show LogWarn = "warn"
show LogInfo = "info" 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 -- | Dump the config
toText :: AppConfig -> Text toText :: AppConfig -> Text
toText conf = toText conf =
@@ -127,6 +137,7 @@ toText conf =
,("jwt-secret", q . toS . showJwtSecret) ,("jwt-secret", q . toS . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64) ,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("log-level", q . show . configLogLevel) ,("log-level", q . show . configLogLevel)
,("openapi-mode", q . show . configOpenApiMode)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes) ,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
,("server-host", q . configServerHost) ,("server-host", q . configServerHost)
@@ -220,6 +231,7 @@ parser optPath env dbSettings =
(optBool "jwt-secret-is-base64") (optBool "jwt-secret-is-base64")
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> parseOpenAPIMode "openapi-mode"
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types") <*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
<*> (fromMaybe "!4" <$> optString "server-host") <*> (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" then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
else pure fileMode 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 :: C.Key -> C.Parser C.Config (Maybe Text)
parseOpenAPIServerProxyURI k = parseOpenAPIServerProxyURI k =
optString k >>= \case optString k >>= \case
+2 -2
View File
@@ -40,8 +40,8 @@ import PostgREST.ContentType
import Protolude hiding (Proxy, get, toS) import Protolude hiding (Proxy, get, toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
encode :: AppConfig -> DbStructure -> [Table] -> Maybe Text -> HashMap.HashMap k [ProcDescription] -> LBS.ByteString encode :: AppConfig -> DbStructure -> [Table] -> HashMap.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
encode conf dbStructure tables schemaDescription procs = encode conf dbStructure tables procs schemaDescription =
JSON.encode $ JSON.encode $
postgrestSpec postgrestSpec
(dbRelationships dbStructure) (dbRelationships dbStructure)
+5 -3
View File
@@ -44,7 +44,8 @@ import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept) import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies) import Web.Cookie (parseCookies)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.ContentType (ContentType (..)) import PostgREST.ContentType (ContentType (..))
import PostgREST.DbStructure (DbStructure (..)) import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.DbStructure.Identifiers (FieldName,
@@ -313,8 +314,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
in in
case path of case path of
[] -> case configDbRootSpec of [] -> case configDbRootSpec of
Just (QualifiedIdentifier pSch pName) -> TargetProc (callFindProc (if pSch == mempty then schema else pSch) pName) True Just (QualifiedIdentifier pSch pName) -> TargetProc (callFindProc (if pSch == mempty then schema else pSch) pName) True
Nothing -> TargetDefaultSpec schema Nothing | configOpenApiMode == OADisabled -> TargetUnknown
| otherwise -> TargetDefaultSpec schema
[table] -> TargetIdent $ QualifiedIdentifier schema table [table] -> TargetIdent $ QualifiedIdentifier schema table
["rpc", pName] -> TargetProc (callFindProc schema pName) False ["rpc", pName] -> TargetProc (callFindProc schema pName) False
_ -> TargetUnknown _ -> 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.ConcurrentSpec
import qualified Feature.CorsSpec import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec import qualified Feature.DeleteSpec
import qualified Feature.DisabledOpenApiSpec
import qualified Feature.EmbedDisambiguationSpec import qualified Feature.EmbedDisambiguationSpec
import qualified Feature.ExtraSearchPathSpec import qualified Feature.ExtraSearchPathSpec
import qualified Feature.HtmlRawOutputSpec import qualified Feature.HtmlRawOutputSpec
import qualified Feature.IgnoreAclOpenApiSpec
import qualified Feature.InsertSpec import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec import qualified Feature.JsonOperatorSpec
import qualified Feature.MultipleSchemaSpec import qualified Feature.MultipleSchemaSpec
@@ -92,6 +94,8 @@ main = do
let withApp = app testCfg let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg maxRowsApp = app testMaxRowsCfg
disabledOpenApi = app testDisabledOpenApiCfg
ignoreAclOpenApi = app testIgnoreAclOpenApiCfg
proxyApp = app testProxyCfg proxyApp = app testProxyCfg
noJwtApp = app testCfgNoJWT noJwtApp = app testCfgNoJWT
binaryJwtApp = app testCfgBinaryJWT binaryJwtApp = app testCfgBinaryJWT
@@ -152,6 +156,14 @@ main = do
parallel $ before unicodeApp $ parallel $ before unicodeApp $
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec 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 -- this test runs with a proxy
parallel $ before proxyApp $ parallel $ before proxyApp $
describe "Feature.ProxySpec" Feature.ProxySpec.spec describe "Feature.ProxySpec" Feature.ProxySpec.spec
+10 -1
View File
@@ -24,7 +24,9 @@ import Text.Heredoc
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
JSPathExp (..), JSPathExp (..),
LogLevel (..), parseSecret) LogLevel (..),
OpenAPIMode (..),
parseSecret)
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..)) import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
import Protolude hiding (toS) import Protolude hiding (toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
@@ -94,6 +96,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configJwtSecret = secret , configJwtSecret = secret
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configOpenApiMode = OAFollowACL
, configOpenApiServerProxyUri = Nothing , configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = [] , configRawMediaTypes = []
, configServerHost = "localhost" , configServerHost = "localhost"
@@ -122,6 +125,12 @@ testUnicodeCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["
testMaxRowsCfg :: Text -> AppConfig testMaxRowsCfg :: Text -> AppConfig
testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 } 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 :: Text -> AppConfig
testProxyCfg testDbConn = (testCfg testDbConn) { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" } testProxyCfg testDbConn = (testCfg testDbConn) { configOpenApiServerProxyUri = Just "https://postgrest.com/openapi.json" }
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"aliased\""
jwt-secret = "" jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "error" log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
log-level = "error" log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"other\".\"role\""
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-acl"
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"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"a\".\"role\""
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-acl"
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"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"user\"[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-acl"
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"
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".\"role\""
jwt-secret = "" jwt-secret = ""
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
log-level = "error" log-level = "error"
openapi-mode = "follow-acl"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -19,6 +19,7 @@ PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5 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-acl'
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
@@ -17,6 +17,7 @@ jwt-role-claim-key = ".user[0].\"real-role\""
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
openapi-mode = "ignore-acl"
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"