Correct openapi ignore mode to filter schemas

Also rename openapi-mode options

* follow-acl -> follow-privileges
* ignore-acl -> ignore-privileges
This commit is contained in:
steve-chavez
2021-06-27 22:48:49 -05:00
committed by Steve Chavez
parent 41d119b19f
commit f3a184af01
21 changed files with 145 additions and 36 deletions
+1 -1
View File
@@ -179,7 +179,7 @@ test-suite spec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
Feature.HtmlRawOutputSpec Feature.HtmlRawOutputSpec
Feature.InsertSpec Feature.InsertSpec
Feature.IgnoreAclOpenApiSpec Feature.IgnorePrivOpenApiSpec
Feature.JsonOperatorSpec Feature.JsonOperatorSpec
Feature.MultipleSchemaSpec Feature.MultipleSchemaSpec
Feature.NoJwtSpec Feature.NoJwtSpec
+5 -4
View File
@@ -28,6 +28,7 @@ import System.Posix.Types (FileMode)
import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Hasql.DynamicStatements.Snippet as SQL import qualified Hasql.DynamicStatements.Snippet as SQL
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
@@ -465,15 +466,15 @@ 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 $ case configOpenApiMode of lift $ case configOpenApiMode of
OAFollowACL -> OAFollowPriv ->
OpenAPI.encode conf dbStructure OpenAPI.encode conf dbStructure
<$> SQL.statement tSchema (DbStructure.accessibleTables 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) <*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
OAIgnoreACL -> OAIgnorePriv ->
OpenAPI.encode conf dbStructure OpenAPI.encode conf dbStructure
(DbStructure.dbTables dbStructure) (filter (\x -> tableSchema x == tSchema) $ DbStructure.dbTables dbStructure)
(DbStructure.dbProcs dbStructure) (Map.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ DbStructure.dbProcs dbStructure)
<$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements) <$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
OADisabled -> OADisabled ->
pure mempty pure mempty
+4 -4
View File
@@ -192,11 +192,11 @@ 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 |## determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely
|## admitted values: follow-acl, ignore-acl, disabled |## admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-acl" |openapi-mode = "follow-privileges"
| |
|## base url for swagger output |## base url for the OpenAPI output
|openapi-server-proxy-uri = "" |openapi-server-proxy-uri = ""
| |
|## choose a secret, JSON Web Key (or set) to enable JWT auth |## choose a secret, JSON Web Key (or set) to enable JWT auth
+9 -9
View File
@@ -103,13 +103,13 @@ instance Show LogLevel where
show LogWarn = "warn" show LogWarn = "warn"
show LogInfo = "info" show LogInfo = "info"
data OpenAPIMode = OAFollowACL | OAIgnoreACL | OADisabled data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
deriving Eq deriving Eq
instance Show OpenAPIMode where instance Show OpenAPIMode where
show OAFollowACL = "follow-acl" show OAFollowPriv = "follow-privileges"
show OAIgnoreACL = "ignore-acl" show OAIgnorePriv = "ignore-privileges"
show OADisabled = "disabled" show OADisabled = "disabled"
-- | Dump the config -- | Dump the config
toText :: AppConfig -> Text toText :: AppConfig -> Text
@@ -262,11 +262,11 @@ parser optPath env dbSettings =
parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode
parseOpenAPIMode k = parseOpenAPIMode k =
optString k >>= \case optString k >>= \case
Nothing -> pure OAFollowACL Nothing -> pure OAFollowPriv
Just "follow-acl" -> pure OAFollowACL Just "follow-privileges" -> pure OAFollowPriv
Just "ignore-acl" -> pure OAIgnoreACL Just "ignore-privileges" -> pure OAIgnorePriv
Just "disabled" -> pure OADisabled Just "disabled" -> pure OADisabled
Just _ -> fail "Invalid openapi-mode. Check your configuration." 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 =
+23 -1
View File
@@ -1,4 +1,4 @@
module Feature.IgnoreAclOpenApiSpec where module Feature.IgnorePrivOpenApiSpec where
import Control.Lens ((^?)) import Control.Lens ((^?))
@@ -32,6 +32,17 @@ spec = describe "OpenAPI Ignore ACL" $ do
liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|] liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]
it "only includes tables that belong to another schema if the Accept-Profile header is used" $ do
r1 <- simpleBody <$> get "/"
let tableKey1 = r1 ^? key "paths" . key "/children"
liftIO $ tableKey1 `shouldBe` Nothing
r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""
let tableKey2 = r2 ^? key "paths" . key "/children"
liftIO $ tableKey2 `shouldNotBe` Nothing
describe "RPC" $ do describe "RPC" $ do
it "includes privileged function even if user does not have permission" $ do it "includes privileged function even if user does not have permission" $ do
@@ -41,3 +52,14 @@ spec = describe "OpenAPI Ignore ACL" $ do
. nth 0 . nth 0
liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|] liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]
it "only includes functions that belong to another schema if the Accept-Profile header is used" $ do
r1 <- simpleBody <$> get "/"
let funcKey1 = r1 ^? key "paths" . key "/rpc/get_parents_below"
liftIO $ funcKey1 `shouldBe` Nothing
r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""
let funcKey2 = r2 ^? key "paths" . key "/rpc/get_parents_below"
liftIO $ funcKey2 `shouldNotBe` Nothing
+65
View File
@@ -0,0 +1,65 @@
module Feature.IgnorePrivOpenApiSpec 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 Privileges" $ 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"|]
it "only includes tables that belong to another schema if the Accept-Profile header is used" $ do
r1 <- simpleBody <$> get "/"
let tableKey1 = r1 ^? key "paths" . key "/children"
liftIO $ tableKey1 `shouldBe` Nothing
r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""
let tableKey2 = r2 ^? key "paths" . key "/children"
liftIO $ tableKey2 `shouldNotBe` Nothing
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"|]
it "only includes functions that belong to another schema if the Accept-Profile header is used" $ do
r1 <- simpleBody <$> get "/"
let funcKey1 = r1 ^? key "paths" . key "/rpc/get_parents_below"
liftIO $ funcKey1 `shouldBe` Nothing
r2 <- simpleBody <$> request methodGet "/" [("Accept-Profile", "v1")] ""
let funcKey2 = r2 ^? key "paths" . key "/rpc/get_parents_below"
liftIO $ funcKey2 `shouldNotBe` Nothing
+4 -4
View File
@@ -32,7 +32,7 @@ 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.IgnorePrivOpenApiSpec
import qualified Feature.InsertSpec import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec import qualified Feature.JsonOperatorSpec
import qualified Feature.MultipleSchemaSpec import qualified Feature.MultipleSchemaSpec
@@ -95,7 +95,6 @@ main = do
let withApp = app testCfg let withApp = app testCfg
maxRowsApp = app testMaxRowsCfg maxRowsApp = app testMaxRowsCfg
disabledOpenApi = app testDisabledOpenApiCfg disabledOpenApi = app testDisabledOpenApiCfg
ignoreAclOpenApi = app testIgnoreAclOpenApiCfg
proxyApp = app testProxyCfg proxyApp = app testProxyCfg
noJwtApp = app testCfgNoJWT noJwtApp = app testCfgNoJWT
binaryJwtApp = app testCfgBinaryJWT binaryJwtApp = app testCfgBinaryJWT
@@ -112,6 +111,7 @@ main = do
unicodeApp = appDbs testUnicodeCfg unicodeApp = appDbs testUnicodeCfg
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
multipleSchemaApp = appDbs testMultipleSchemaCfg multipleSchemaApp = appDbs testMultipleSchemaCfg
ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg
let analyze :: IO () let analyze :: IO ()
analyze = do analyze = do
@@ -161,8 +161,8 @@ main = do
describe "Feature.DisabledOpenApiSpec" Feature.DisabledOpenApiSpec.spec describe "Feature.DisabledOpenApiSpec" Feature.DisabledOpenApiSpec.spec
-- this test runs with openapi-mode set to ignore-acl -- this test runs with openapi-mode set to ignore-acl
parallel $ before ignoreAclOpenApi $ parallel $ before ignorePrivOpenApi $
describe "Feature.IgnoreAclOpenApiSpec" Feature.IgnoreAclOpenApiSpec.spec describe "Feature.IgnorePrivOpenApiSpec" Feature.IgnorePrivOpenApiSpec.spec
-- this test runs with a proxy -- this test runs with a proxy
parallel $ before proxyApp $ parallel $ before proxyApp $
+3 -3
View File
@@ -96,7 +96,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configJwtSecret = secret , configJwtSecret = secret
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configLogLevel = LogCrit , configLogLevel = LogCrit
, configOpenApiMode = OAFollowACL , configOpenApiMode = OAFollowPriv
, configOpenApiServerProxyUri = Nothing , configOpenApiServerProxyUri = Nothing
, configRawMediaTypes = [] , configRawMediaTypes = []
, configServerHost = "localhost" , configServerHost = "localhost"
@@ -128,8 +128,8 @@ testMaxRowsCfg testDbConn = (testCfg testDbConn) { configDbMaxRows = Just 2 }
testDisabledOpenApiCfg :: Text -> AppConfig testDisabledOpenApiCfg :: Text -> AppConfig
testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled } testDisabledOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OADisabled }
testIgnoreAclOpenApiCfg :: Text -> AppConfig testIgnorePrivOpenApiCfg :: Text -> AppConfig
testIgnoreAclOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OAIgnoreACL } testIgnorePrivOpenApiCfg testDbConn = (testCfg testDbConn) { configOpenApiMode = OAIgnorePriv, configDbSchemas = fromList ["test", "v1"] }
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" }
+1
View File
@@ -59,3 +59,4 @@ ALTER ROLE other_authenticator SET pgrst.db_prepared_statements = 'false';
ALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_headers'; ALTER ROLE other_authenticator SET pgrst.db_pre_request = 'test.other_custom_headers';
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';
@@ -17,7 +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-mode = "follow-privileges"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,7 +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-mode = "follow-privileges"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,7 +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-mode = "follow-privileges"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,7 +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-mode = "follow-privileges"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
@@ -17,7 +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-mode = "disabled"
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,7 +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-mode = "ignore-privileges"
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,7 +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-mode = "ignore-privileges"
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 -1
View File
@@ -17,7 +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-mode = "follow-privileges"
openapi-server-proxy-uri = "" openapi-server-proxy-uri = ""
raw-media-types = "" raw-media-types = ""
server-host = "!4" server-host = "!4"
+1 -1
View File
@@ -19,7 +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_MODE: 'ignore-privileges'
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 -1
View File
@@ -17,7 +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-mode = "ignore-privileges"
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"
+5
View File
@@ -174,3 +174,8 @@ invalidroleclaimkeys:
- '.my_role;;domain' - '.my_role;;domain'
- '.#$$%&$%/' - '.#$$%&$%/'
- '1234' - '1234'
invalidopenapimodes:
- 'follow-'
- 'ignore-'
- '.#$$%&$%/'
+15
View File
@@ -425,6 +425,21 @@ def test_invalid_role_claim_key(invalidroleclaimkey, defaultenv):
print(line) print(line)
@pytest.mark.parametrize("invalidopenapimodes", FIXTURES["invalidopenapimodes"])
def test_invalid_openapi_mode(invalidopenapimodes, defaultenv):
"Given an invalid openapi-mode, Postgrest should exit with a non-zero exit code."
env = {
**defaultenv,
"PGRST_OPENAPI_MODE": invalidopenapimodes,
}
with pytest.raises(PostgrestError):
dump = dumpconfig(CONFIGSDIR / "defaults.config", env=env)
for line in dump.split("\n"):
if line.startswith("openapi-mode"):
print(line)
def test_iat_claim(defaultenv): def test_iat_claim(defaultenv):
""" """
A claim with an 'iat' (issued at) attribute should be successful. A claim with an 'iat' (issued at) attribute should be successful.