feat: Make db-anon-role optional
Without db-anon-role, PostgREST will block any anonymous access without hitting the database. Resolves #1689, Ref #1823
This commit is contained in:
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
||||||
- #1991, Add the ability to run without `db-uri` using libpq's PG environment variables to connect. @wolfgangwalther
|
- #1991, Add the ability to run without `db-uri` using libpq's PG environment variables to connect. @wolfgangwalther
|
||||||
- #1769, Add the ability to run without `db-schemas`, defaulting to `db-schemas=public`. @wolfgangwalther
|
- #1769, Add the ability to run without `db-schemas`, defaulting to `db-schemas=public`. @wolfgangwalther
|
||||||
|
- #1689, Add the ability to run without `db-anon-role` disabling anonymous access. @wolfgangwalther
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
# previously required settings to make this work with older branches
|
# previously required settings to make this work with older branches
|
||||||
|
export PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
|
||||||
export PGRST_DB_URI="postgresql://"
|
export PGRST_DB_URI="postgresql://"
|
||||||
export PGRST_DB_SCHEMAS="test"
|
export PGRST_DB_SCHEMAS="test"
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ let
|
|||||||
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
||||||
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
||||||
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
||||||
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [Anonymous PG role])"
|
|
||||||
];
|
];
|
||||||
positionalCompletion = "_command";
|
positionalCompletion = "_command";
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
@@ -54,7 +53,6 @@ let
|
|||||||
export PGUSER
|
export PGUSER
|
||||||
export PGDATABASE
|
export PGDATABASE
|
||||||
export PGRST_DB_SCHEMAS
|
export PGRST_DB_SCHEMAS
|
||||||
export PGRST_DB_ANON_ROLE
|
|
||||||
|
|
||||||
log "Initializing database cluster..."
|
log "Initializing database cluster..."
|
||||||
# We try to make the database cluster as independent as possible from the host
|
# We try to make the database cluster as independent as possible from the host
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ test-suite spec
|
|||||||
Feature.JsonOperatorSpec
|
Feature.JsonOperatorSpec
|
||||||
Feature.LegacyGucsSpec
|
Feature.LegacyGucsSpec
|
||||||
Feature.MultipleSchemaSpec
|
Feature.MultipleSchemaSpec
|
||||||
|
Feature.NoAnonSpec
|
||||||
Feature.NoJwtSpec
|
Feature.NoJwtSpec
|
||||||
Feature.NonexistentSchemaSpec
|
Feature.NonexistentSchemaSpec
|
||||||
Feature.OpenApiSpec
|
Feature.OpenApiSpec
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ postgrestResponse conf@AppConfig{..} maybeDbStructure jsonDbS pgVer pool AuthRes
|
|||||||
|
|
||||||
let handleReq apiReq = handleRequest $ RequestContext conf dbStructure apiReq pgVer
|
let handleReq apiReq = handleRequest $ RequestContext conf dbStructure apiReq pgVer
|
||||||
|
|
||||||
runDbHandler pool (txMode apiRequest) (authRole /= configDbAnonRole) configDbPreparedStatements .
|
runDbHandler pool (txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements .
|
||||||
Middleware.optionalRollback conf apiRequest $
|
Middleware.optionalRollback conf apiRequest $
|
||||||
Middleware.runPgLocals conf authClaims authRole handleReq apiRequest jsonDbS pgVer
|
Middleware.runPgLocals conf authClaims authRole handleReq apiRequest jsonDbS pgVer
|
||||||
|
|
||||||
|
|||||||
+16
-15
@@ -32,7 +32,7 @@ import qualified Network.Wai.Middleware.HttpAuth as Wai
|
|||||||
|
|
||||||
import Control.Lens (set)
|
import Control.Lens (set)
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft, mapRight)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.Time.Clock (UTCTime)
|
import Data.Time.Clock (UTCTime)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
@@ -71,16 +71,17 @@ parseToken AppConfig{..} token time = do
|
|||||||
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||||
jwtClaimsError e = JwtTokenInvalid $ show e
|
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||||
|
|
||||||
parseClaims :: AppConfig -> JSON.Value -> AuthResult
|
parseClaims :: Monad m =>
|
||||||
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) =
|
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||||
AuthResult
|
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
||||||
{ authClaims = mclaims & M.insert "role" (JSON.toJSON role)
|
-- role defaults to anon if not specified in jwt
|
||||||
, authRole = role
|
role <- liftEither . maybeToRight JwtTokenRequired $
|
||||||
}
|
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
|
return AuthResult
|
||||||
|
{ authClaims = mclaims & M.insert "role" (JSON.toJSON role)
|
||||||
|
, authRole = role
|
||||||
|
}
|
||||||
where
|
where
|
||||||
-- role defaults to anon if not specified in jwt
|
|
||||||
role = maybe configDbAnonRole unquoted (walkJSPath (Just jclaims) configJwtRoleClaimKey)
|
|
||||||
|
|
||||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||||
walkJSPath x [] = x
|
walkJSPath x [] = x
|
||||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest
|
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest
|
||||||
@@ -91,7 +92,7 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) =
|
|||||||
unquoted (JSON.String t) = t
|
unquoted (JSON.String t) = t
|
||||||
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||||
-- impossible case - just added to please -Wincomplete-patterns
|
-- impossible case - just added to please -Wincomplete-patterns
|
||||||
parseClaims _ _ = AuthResult { authClaims = M.empty, authRole = mempty }
|
parseClaims _ _ = return AuthResult { authClaims = M.empty, authRole = mempty }
|
||||||
|
|
||||||
-- | Validate authorization header.
|
-- | Validate authorization header.
|
||||||
-- Parse and store JWT claims for future use in the request.
|
-- Parse and store JWT claims for future use in the request.
|
||||||
@@ -101,11 +102,11 @@ middleware appState app req respond = do
|
|||||||
time <- getTime appState
|
time <- getTime appState
|
||||||
|
|
||||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||||
claims <- runExceptT $ parseToken conf (LBS.fromStrict token) time
|
authResult <- runExceptT $
|
||||||
|
parseToken conf (LBS.fromStrict token) time >>=
|
||||||
|
parseClaims conf
|
||||||
|
|
||||||
let
|
let req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
authResult = mapRight (parseClaims conf) claims
|
|
||||||
req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
|
||||||
app req' respond
|
app req' respond
|
||||||
|
|
||||||
authResultKey :: Vault.Key (Either Error AuthResult)
|
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import Protolude hiding (Proxy, toList)
|
|||||||
|
|
||||||
data AppConfig = AppConfig
|
data AppConfig = AppConfig
|
||||||
{ configAppSettings :: [(Text, Text)]
|
{ configAppSettings :: [(Text, Text)]
|
||||||
, configDbAnonRole :: Text
|
, configDbAnonRole :: Maybe Text
|
||||||
, configDbChannel :: Text
|
, configDbChannel :: Text
|
||||||
, configDbChannelEnabled :: Bool
|
, configDbChannelEnabled :: Bool
|
||||||
, configDbExtraSearchPath :: [Text]
|
, configDbExtraSearchPath :: [Text]
|
||||||
@@ -121,7 +121,7 @@ toText conf =
|
|||||||
where
|
where
|
||||||
-- apply conf to all pgrst settings
|
-- apply conf to all pgrst settings
|
||||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||||
[("db-anon-role", q . configDbAnonRole)
|
[("db-anon-role", q . fromMaybe "" . configDbAnonRole)
|
||||||
,("db-channel", q . configDbChannel)
|
,("db-channel", q . configDbChannel)
|
||||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||||
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||||
@@ -207,7 +207,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> C.Parser C.Config A
|
|||||||
parser optPath env dbSettings =
|
parser optPath env dbSettings =
|
||||||
AppConfig
|
AppConfig
|
||||||
<$> parseAppSettings "app.settings"
|
<$> parseAppSettings "app.settings"
|
||||||
<*> reqString "db-anon-role"
|
<*> optString "db-anon-role"
|
||||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||||
@@ -322,9 +322,6 @@ parser optPath env dbSettings =
|
|||||||
Just v -> pure $ Just v
|
Just v -> pure $ Just v
|
||||||
Nothing -> alias
|
Nothing -> alias
|
||||||
|
|
||||||
reqString :: C.Key -> C.Parser C.Config Text
|
|
||||||
reqString k = overrideFromDbOrEnvironment C.required k coerceText
|
|
||||||
|
|
||||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||||
|
|
||||||
@@ -352,7 +349,7 @@ parser optPath env dbSettings =
|
|||||||
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||||
if dbSettingName `notElem` [
|
if dbSettingName `notElem` [
|
||||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "log_level",
|
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "log_level",
|
||||||
"db_anon_role", "db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
||||||
then lookup dbSettingName dbSettings
|
then lookup dbSettingName dbSettings
|
||||||
else Nothing
|
else Nothing
|
||||||
|
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ data Error
|
|||||||
| PutRangeNotAllowedError
|
| PutRangeNotAllowedError
|
||||||
| JwtTokenMissing
|
| JwtTokenMissing
|
||||||
| JwtTokenInvalid Text
|
| JwtTokenInvalid Text
|
||||||
|
| JwtTokenRequired
|
||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
| NotFound
|
| NotFound
|
||||||
| ApiRequestError ApiRequestError
|
| ApiRequestError ApiRequestError
|
||||||
@@ -288,6 +289,7 @@ instance PgrstError Error where
|
|||||||
status PutRangeNotAllowedError = HTTP.status400
|
status PutRangeNotAllowedError = HTTP.status400
|
||||||
status JwtTokenMissing = HTTP.status500
|
status JwtTokenMissing = HTTP.status500
|
||||||
status (JwtTokenInvalid _) = HTTP.unauthorized401
|
status (JwtTokenInvalid _) = HTTP.unauthorized401
|
||||||
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
status (SingularityError _) = HTTP.status406
|
status (SingularityError _) = HTTP.status406
|
||||||
status NotFound = HTTP.status404
|
status NotFound = HTTP.status404
|
||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
@@ -295,6 +297,7 @@ instance PgrstError Error where
|
|||||||
|
|
||||||
headers (SingularityError _) = [ContentType.toHeader CTSingularJSON]
|
headers (SingularityError _) = [ContentType.toHeader CTSingularJSON]
|
||||||
headers (JwtTokenInvalid m) = [ContentType.toHeader CTApplicationJSON, invalidTokenHeader m]
|
headers (JwtTokenInvalid m) = [ContentType.toHeader CTApplicationJSON, invalidTokenHeader m]
|
||||||
|
headers JwtTokenRequired = [ContentType.toHeader CTApplicationJSON, requiredTokenHeader]
|
||||||
headers (PgErr err) = headers err
|
headers (PgErr err) = headers err
|
||||||
headers (ApiRequestError err) = headers err
|
headers (ApiRequestError err) = headers err
|
||||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||||
@@ -322,6 +325,8 @@ instance JSON.ToJSON Error where
|
|||||||
"message" .= ("Server lacks JWT secret" :: Text)]
|
"message" .= ("Server lacks JWT secret" :: Text)]
|
||||||
toJSON (JwtTokenInvalid message) = JSON.object [
|
toJSON (JwtTokenInvalid message) = JSON.object [
|
||||||
"message" .= (message :: Text)]
|
"message" .= (message :: Text)]
|
||||||
|
toJSON JwtTokenRequired = JSON.object [
|
||||||
|
"message" .= ("Anonymous access is disabled" :: Text)]
|
||||||
toJSON NotFound = JSON.object []
|
toJSON NotFound = JSON.object []
|
||||||
toJSON (PgErr err) = JSON.toJSON err
|
toJSON (PgErr err) = JSON.toJSON err
|
||||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||||
@@ -330,5 +335,8 @@ invalidTokenHeader :: Text -> Header
|
|||||||
invalidTokenHeader m =
|
invalidTokenHeader m =
|
||||||
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
|
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
|
||||||
|
|
||||||
|
requiredTokenHeader :: Header
|
||||||
|
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||||
|
|
||||||
singularityError :: (Integral a) => a -> Error
|
singularityError :: (Integral a) => a -> Error
|
||||||
singularityError = SingularityError . toInteger
|
singularityError = SingularityError . toInteger
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
db-anon-role = "required"
|
|
||||||
|
|
||||||
db-schema = "provided_through_alias"
|
db-schema = "provided_through_alias"
|
||||||
max-rows = 1000
|
max-rows = 1000
|
||||||
pre-request = "check_alias"
|
pre-request = "check_alias"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
db-anon-role = "required"
|
|
||||||
|
|
||||||
db-channel-enabled = "1"
|
db-channel-enabled = "1"
|
||||||
db-prepared-statements = "0"
|
db-prepared-statements = "0"
|
||||||
jwt-secret-is-base64 = "2"
|
jwt-secret-is-base64 = "2"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
db-anon-role = "required"
|
|
||||||
|
|
||||||
db-channel-enabled = "true"
|
db-channel-enabled = "true"
|
||||||
db-prepared-statements = "FALSE"
|
db-prepared-statements = "FALSE"
|
||||||
jwt-secret-is-base64 = "\"true\""
|
jwt-secret-is-base64 = "\"true\""
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
db-anon-role = "required"
|
|
||||||
# Not the default, but only works with PG* variables, which are not set
|
# Not the default, but only works with PG* variables, which are not set
|
||||||
db-config = false
|
db-config = false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "required"
|
db-anon-role = ""
|
||||||
db-channel = "pgrst"
|
db-channel = "pgrst"
|
||||||
db-channel-enabled = true
|
db-channel-enabled = true
|
||||||
db-extra-search-path = "public"
|
db-extra-search-path = "public"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "required"
|
db-anon-role = ""
|
||||||
db-channel = "pgrst"
|
db-channel = "pgrst"
|
||||||
db-channel-enabled = true
|
db-channel-enabled = true
|
||||||
db-extra-search-path = "public"
|
db-extra-search-path = "public"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "required"
|
db-anon-role = ""
|
||||||
db-channel = "pgrst"
|
db-channel = "pgrst"
|
||||||
db-channel-enabled = true
|
db-channel-enabled = true
|
||||||
db-extra-search-path = "public"
|
db-extra-search-path = "public"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "required"
|
db-anon-role = ""
|
||||||
db-channel = "pgrst"
|
db-channel = "pgrst"
|
||||||
db-channel-enabled = true
|
db-channel-enabled = true
|
||||||
db-extra-search-path = "public"
|
db-extra-search-path = "public"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "postgrest_test_anonymous"
|
db-anon-role = "other"
|
||||||
db-channel = "postgrest"
|
db-channel = "postgrest"
|
||||||
db-channel-enabled = false
|
db-channel-enabled = false
|
||||||
db-extra-search-path = "public,extensions,other"
|
db-extra-search-path = "public,extensions,other"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "postgrest_test_anonymous"
|
db-anon-role = "anonymous"
|
||||||
db-channel = "postgrest"
|
db-channel = "postgrest"
|
||||||
db-channel-enabled = false
|
db-channel-enabled = false
|
||||||
db-extra-search-path = "public,extensions,private"
|
db-extra-search-path = "public,extensions,private"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
db-anon-role = "required"
|
db-anon-role = ""
|
||||||
db-channel = "pgrst"
|
db-channel = "pgrst"
|
||||||
db-channel-enabled = true
|
db-channel-enabled = true
|
||||||
db-extra-search-path = "public"
|
db-extra-search-path = "public"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
# tests how config options fall back with invalid types
|
# tests how config options fall back with invalid types
|
||||||
db-anon-role = "required"
|
|
||||||
|
|
||||||
# expects string
|
# expects string
|
||||||
app.settings.test = false
|
app.settings.test = false
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ ALTER ROLE db_config_authenticator SET pgrst.raw_media_types = 'application/vnd.
|
|||||||
ALTER ROLE db_config_authenticator SET pgrst.jwt_secret = 'REALLY=REALLY=REALLY=REALLY=VERY=SAFE';
|
ALTER ROLE db_config_authenticator SET pgrst.jwt_secret = 'REALLY=REALLY=REALLY=REALLY=VERY=SAFE';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.jwt_secret_is_base64 = 'false';
|
ALTER ROLE db_config_authenticator SET pgrst.jwt_secret_is_base64 = 'false';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.jwt_role_claim_key = '."a"."role"';
|
ALTER ROLE db_config_authenticator SET pgrst.jwt_role_claim_key = '."a"."role"';
|
||||||
|
ALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'anonymous';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override';
|
ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2';
|
ALTER ROLE db_config_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root';
|
ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root';
|
||||||
@@ -29,7 +30,6 @@ ALTER ROLE db_config_authenticator SET pgrst.server_port = 'ignored';
|
|||||||
ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket = 'ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket = 'ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket_mode = 'ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket_mode = 'ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.log_level = 'ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.log_level = 'ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'ignored';
|
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_uri = 'postgresql://ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.db_uri = 'postgresql://ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_channel_enabled = 'ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.db_channel_enabled = 'ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_channel = 'ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.db_channel = 'ignored';
|
||||||
@@ -45,6 +45,7 @@ ALTER ROLE other_authenticator SET pgrst.raw_media_types = 'application/vnd.pgrs
|
|||||||
ALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';
|
ALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';
|
||||||
ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'true';
|
ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'true';
|
||||||
ALTER ROLE other_authenticator SET pgrst.jwt_role_claim_key = '."other"."role"';
|
ALTER ROLE other_authenticator SET pgrst.jwt_role_claim_key = '."other"."role"';
|
||||||
|
ALTER ROLE other_authenticator SET pgrst.db_anon_role = 'other';
|
||||||
ALTER ROLE other_authenticator SET pgrst.db_tx_end = 'rollback-allow-override';
|
ALTER ROLE other_authenticator SET pgrst.db_tx_end = 'rollback-allow-override';
|
||||||
ALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2';
|
ALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2';
|
||||||
ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root';
|
ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
\ir db_config.sql
|
\ir db_config.sql
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
|
ALTER ROLE :USER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous, postgrest_test_author TO :USER;
|
GRANT postgrest_test_anonymous, postgrest_test_author TO :USER;
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ cli:
|
|||||||
- name: invalid config file
|
- name: invalid config file
|
||||||
expect: error
|
expect: error
|
||||||
args: ['test/io-tests/configs/invalid.yaml']
|
args: ['test/io-tests/configs/invalid.yaml']
|
||||||
# failures: required config options
|
|
||||||
- name: missing db-anon-role
|
|
||||||
expect: error
|
|
||||||
env:
|
|
||||||
# failures: wrong config values
|
# failures: wrong config values
|
||||||
- name: invalid server-unix-socket-mode not octal
|
- name: invalid server-unix-socket-mode not octal
|
||||||
expect: error
|
expect: error
|
||||||
|
|||||||
+7
-4
@@ -92,8 +92,7 @@ def defaultenv():
|
|||||||
"PGDATABASE": os.environ["PGDATABASE"],
|
"PGDATABASE": os.environ["PGDATABASE"],
|
||||||
"PGHOST": os.environ["PGHOST"],
|
"PGHOST": os.environ["PGHOST"],
|
||||||
"PGUSER": os.environ["PGUSER"],
|
"PGUSER": os.environ["PGUSER"],
|
||||||
"PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"],
|
"PGRST_DB_CONFIG": "true",
|
||||||
"PGRST_DB_CONFIG": "false",
|
|
||||||
"PGRST_LOG_LEVEL": "info",
|
"PGRST_LOG_LEVEL": "info",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +383,7 @@ def test_read_secret_from_file(secretpath, defaultenv):
|
|||||||
|
|
||||||
with run(stdin=secret, env=env) as postgrest:
|
with run(stdin=secret, env=env) as postgrest:
|
||||||
response = postgrest.session.get("/authors_only", headers=headers)
|
response = postgrest.session.get("/authors_only", headers=headers)
|
||||||
|
print(response.text)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
@@ -600,6 +600,8 @@ def test_jwt_secret_external_file_reload(tmp_path, defaultenv):
|
|||||||
**defaultenv,
|
**defaultenv,
|
||||||
"PGRST_JWT_SECRET": f"@{external_secret_file}",
|
"PGRST_JWT_SECRET": f"@{external_secret_file}",
|
||||||
"PGRST_DB_CHANNEL_ENABLED": "true",
|
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||||
|
"PGRST_DB_CONFIG": "false",
|
||||||
|
"PGRST_DB_ANON_ROLE": "postgrest_test_anonymous", # required for NOTIFY
|
||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
@@ -609,7 +611,7 @@ def test_jwt_secret_external_file_reload(tmp_path, defaultenv):
|
|||||||
# change external file
|
# change external file
|
||||||
external_secret_file.write_text(SECRET)
|
external_secret_file.write_text(SECRET)
|
||||||
|
|
||||||
# SIGUSR1 doesn't reload external files
|
# SIGUSR1 doesn't reload external files, at least when db-config=false
|
||||||
postgrest.process.send_signal(signal.SIGUSR1)
|
postgrest.process.send_signal(signal.SIGUSR1)
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
@@ -627,7 +629,8 @@ def test_jwt_secret_external_file_reload(tmp_path, defaultenv):
|
|||||||
external_secret_file.write_text("invalid" * 5)
|
external_secret_file.write_text("invalid" * 5)
|
||||||
|
|
||||||
# reload config and external file with NOTIFY
|
# reload config and external file with NOTIFY
|
||||||
postgrest.session.post("/rpc/reload_pgrst_config")
|
response = postgrest.session.post("/rpc/reload_pgrst_config")
|
||||||
|
assert response.status_code == 200
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
response = postgrest.session.get("/authors_only", headers=headers)
|
response = postgrest.session.get("/authors_only", headers=headers)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
module Feature.NoAnonSpec where
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Network.HTTP.Types
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
|
spec :: SpecWith ((), Application)
|
||||||
|
spec = describe "server started without anonymous role" $ do
|
||||||
|
it "behaves normally on attempted auth" $ do
|
||||||
|
-- token body: { "role": "postgrest_test_author" }
|
||||||
|
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"
|
||||||
|
request methodGet "/authors_only"
|
||||||
|
[auth]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
200
|
||||||
|
|
||||||
|
it "responds with error when user does not attempt auth" $
|
||||||
|
get "/items"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"message":"Anonymous access is disabled"}|]
|
||||||
|
{ matchStatus = 401
|
||||||
|
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ import qualified Feature.InsertSpec
|
|||||||
import qualified Feature.JsonOperatorSpec
|
import qualified Feature.JsonOperatorSpec
|
||||||
import qualified Feature.LegacyGucsSpec
|
import qualified Feature.LegacyGucsSpec
|
||||||
import qualified Feature.MultipleSchemaSpec
|
import qualified Feature.MultipleSchemaSpec
|
||||||
|
import qualified Feature.NoAnonSpec
|
||||||
import qualified Feature.NoJwtSpec
|
import qualified Feature.NoJwtSpec
|
||||||
import qualified Feature.NonexistentSchemaSpec
|
import qualified Feature.NonexistentSchemaSpec
|
||||||
import qualified Feature.OpenApiSpec
|
import qualified Feature.OpenApiSpec
|
||||||
@@ -96,6 +97,7 @@ main = do
|
|||||||
maxRowsApp = app testMaxRowsCfg
|
maxRowsApp = app testMaxRowsCfg
|
||||||
disabledOpenApi = app testDisabledOpenApiCfg
|
disabledOpenApi = app testDisabledOpenApiCfg
|
||||||
proxyApp = app testProxyCfg
|
proxyApp = app testProxyCfg
|
||||||
|
noAnonApp = app testCfgNoAnon
|
||||||
noJwtApp = app testCfgNoJWT
|
noJwtApp = app testCfgNoJWT
|
||||||
binaryJwtApp = app testCfgBinaryJWT
|
binaryJwtApp = app testCfgBinaryJWT
|
||||||
audJwtApp = app testCfgAudienceJWT
|
audJwtApp = app testCfgAudienceJWT
|
||||||
@@ -170,6 +172,10 @@ main = do
|
|||||||
parallel $ before proxyApp $
|
parallel $ before proxyApp $
|
||||||
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
describe "Feature.ProxySpec" Feature.ProxySpec.spec
|
||||||
|
|
||||||
|
-- this test runs without an anonymous role
|
||||||
|
parallel $ before noAnonApp $
|
||||||
|
describe "Feature.NoAnonSpec" Feature.NoAnonSpec.spec
|
||||||
|
|
||||||
-- this test runs without a JWT secret
|
-- this test runs without a JWT secret
|
||||||
parallel $ before noJwtApp $
|
parallel $ before noJwtApp $
|
||||||
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ baseCfg :: AppConfig
|
|||||||
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||||
AppConfig {
|
AppConfig {
|
||||||
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
|
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
|
||||||
, configDbAnonRole = "postgrest_test_anonymous"
|
, configDbAnonRole = Just "postgrest_test_anonymous"
|
||||||
, configDbChannel = mempty
|
, configDbChannel = mempty
|
||||||
, configDbChannelEnabled = True
|
, configDbChannelEnabled = True
|
||||||
, configDbExtraSearchPath = []
|
, configDbExtraSearchPath = []
|
||||||
@@ -118,6 +118,9 @@ testCfgDisallowRollback = baseCfg { configDbTxAllowOverride = False, configDbTxR
|
|||||||
testCfgForceRollback :: AppConfig
|
testCfgForceRollback :: AppConfig
|
||||||
testCfgForceRollback = baseCfg { configDbTxAllowOverride = False, configDbTxRollbackAll = True }
|
testCfgForceRollback = baseCfg { configDbTxAllowOverride = False, configDbTxRollbackAll = True }
|
||||||
|
|
||||||
|
testCfgNoAnon :: AppConfig
|
||||||
|
testCfgNoAnon = baseCfg { configDbAnonRole = Nothing }
|
||||||
|
|
||||||
testCfgNoJWT :: AppConfig
|
testCfgNoJWT :: AppConfig
|
||||||
testCfgNoJWT = baseCfg { configJwtSecret = Nothing, configJWKS = Nothing }
|
testCfgNoJWT = baseCfg { configJwtSecret = Nothing, configJWKS = Nothing }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user