* Add test for ensuring app.settings don't reset
This commit is contained in:
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384
|
- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384
|
||||||
- #905, Fix intermittent empty replies - @steve-chavez
|
- #905, Fix intermittent empty replies - @steve-chavez
|
||||||
- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez
|
- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez
|
||||||
|
- #1141, Fix app.settings resetting on pool timeout - @steve-chavez
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
+2
-8
@@ -7,8 +7,7 @@ import PostgREST.App (postgrest)
|
|||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
minimumPgVersion,
|
minimumPgVersion,
|
||||||
prettyVersion, readOptions)
|
prettyVersion, readOptions)
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion,
|
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||||
fillSessionWithSettings)
|
|
||||||
import PostgREST.Error (encodeError)
|
import PostgREST.Error (encodeError)
|
||||||
import PostgREST.OpenAPI (isMalformedProxyUri)
|
import PostgREST.OpenAPI (isMalformedProxyUri)
|
||||||
import PostgREST.Types (DbStructure, Schema, PgVersion(..))
|
import PostgREST.Types (DbStructure, Schema, PgVersion(..))
|
||||||
@@ -63,11 +62,10 @@ connectionWorker
|
|||||||
:: ThreadId -- ^ This thread is killed if pg version is unsupported
|
:: ThreadId -- ^ This thread is killed if pg version is unsupported
|
||||||
-> P.Pool -- ^ The PostgreSQL connection pool
|
-> P.Pool -- ^ The PostgreSQL connection pool
|
||||||
-> Schema -- ^ Schema PostgREST is serving up
|
-> Schema -- ^ Schema PostgREST is serving up
|
||||||
-> [(Text, Text)] -- ^ Settings or Environment passed in through the config
|
|
||||||
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
|
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
|
||||||
-> IORef Bool -- ^ Used as a binary Semaphore
|
-> IORef Bool -- ^ Used as a binary Semaphore
|
||||||
-> IO ()
|
-> IO ()
|
||||||
connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
|
connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
|
||||||
isWorkerOn <- readIORef refIsWorkerOn
|
isWorkerOn <- readIORef refIsWorkerOn
|
||||||
unless isWorkerOn $ do
|
unless isWorkerOn $ do
|
||||||
atomicWriteIORef refIsWorkerOn True
|
atomicWriteIORef refIsWorkerOn True
|
||||||
@@ -85,7 +83,6 @@ connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
|
|||||||
("Cannot run in this PostgreSQL version, PostgREST needs at least "
|
("Cannot run in this PostgreSQL version, PostgREST needs at least "
|
||||||
<> pgvName minimumPgVersion)
|
<> pgvName minimumPgVersion)
|
||||||
killThread mainTid
|
killThread mainTid
|
||||||
fillSessionWithSettings settings
|
|
||||||
dbStructure <- getDbStructure schema actualPgVersion
|
dbStructure <- getDbStructure schema actualPgVersion
|
||||||
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
|
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
|
||||||
case result of
|
case result of
|
||||||
@@ -184,7 +181,6 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
(configSettings conf)
|
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn
|
refIsWorkerOn
|
||||||
--
|
--
|
||||||
@@ -208,7 +204,6 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
(configSettings conf)
|
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn
|
refIsWorkerOn
|
||||||
) Nothing
|
) Nothing
|
||||||
@@ -229,7 +224,6 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
(configSettings conf)
|
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn)
|
refIsWorkerOn)
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ readOptions = do
|
|||||||
<*> (join . fmap coerceInt <$> C.key "max-rows")
|
<*> (join . fmap coerceInt <$> C.key "max-rows")
|
||||||
<*> (mfilter (/= "") <$> C.key "pre-request")
|
<*> (mfilter (/= "") <$> C.key "pre-request")
|
||||||
<*> pure False
|
<*> pure False
|
||||||
<*> (fmap parsedPairToTextPair <$> C.subassocs "app.settings")
|
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings")
|
||||||
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> C.key "role-claim-key")
|
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> C.key "role-claim-key")
|
||||||
|
|
||||||
case mAppConf of
|
case mAppConf of
|
||||||
@@ -152,13 +152,6 @@ readOptions = do
|
|||||||
return appConf
|
return appConf
|
||||||
|
|
||||||
where
|
where
|
||||||
parsedPairToTextPair :: (Name, Value) -> (Text, Text)
|
|
||||||
parsedPairToTextPair (k, v) = (k, newValue)
|
|
||||||
where
|
|
||||||
newValue = case v of
|
|
||||||
String textVal -> textVal
|
|
||||||
_ -> show v
|
|
||||||
|
|
||||||
parseJwtAudience :: Name -> C.ConfigParserM (Maybe StringOrURI)
|
parseJwtAudience :: Name -> C.ConfigParserM (Maybe StringOrURI)
|
||||||
parseJwtAudience k =
|
parseJwtAudience k =
|
||||||
C.key k >>= \case
|
C.key k >>= \case
|
||||||
@@ -168,6 +161,10 @@ readOptions = do
|
|||||||
(Just "") -> pure Nothing
|
(Just "") -> pure Nothing
|
||||||
aud' -> pure aud'
|
aud' -> pure aud'
|
||||||
|
|
||||||
|
coerceText :: Value -> Text
|
||||||
|
coerceText (String s) = s
|
||||||
|
coerceText v = show v
|
||||||
|
|
||||||
coerceInt :: (Read i, Integral i) => Value -> Maybe i
|
coerceInt :: (Read i, Integral i) => Value -> Maybe i
|
||||||
coerceInt (Number x) = rightToMaybe $ floatingOrInteger x
|
coerceInt (Number x) = rightToMaybe $ floatingOrInteger x
|
||||||
coerceInt (String x) = readMaybe $ toS x
|
coerceInt (String x) = readMaybe $ toS x
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module PostgREST.DbStructure (
|
|||||||
, accessibleProcs
|
, accessibleProcs
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
, getPgVersion
|
, getPgVersion
|
||||||
, fillSessionWithSettings
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -33,9 +32,6 @@ import GHC.Exts (groupWith)
|
|||||||
import Protolude
|
import Protolude
|
||||||
import Unsafe (unsafeHead)
|
import Unsafe (unsafeHead)
|
||||||
|
|
||||||
import Data.Functor.Contravariant (contramap)
|
|
||||||
import Contravariant.Extras (contrazip2)
|
|
||||||
|
|
||||||
getDbStructure :: Schema -> PgVersion -> H.Session DbStructure
|
getDbStructure :: Schema -> PgVersion -> H.Session DbStructure
|
||||||
getDbStructure schema pgVer = do
|
getDbStructure schema pgVer = do
|
||||||
tabs <- H.query () allTables
|
tabs <- H.query () allTables
|
||||||
@@ -764,17 +760,3 @@ getPgVersion = H.query () $ H.statement sql HE.unit versionRow False
|
|||||||
where
|
where
|
||||||
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
||||||
versionRow = HD.singleRow $ PgVersion <$> HD.value HD.int4 <*> HD.value HD.text
|
versionRow = HD.singleRow $ PgVersion <$> HD.value HD.int4 <*> HD.value HD.text
|
||||||
|
|
||||||
fillSessionWithSettings :: [(Text, Text)] -> H.Session ()
|
|
||||||
fillSessionWithSettings settings =
|
|
||||||
-- Send all of the config settings to the set_config function, using pgsql's `unnest` to transform arrays of values
|
|
||||||
H.query settings $ H.statement "SELECT set_config(k, v, false) FROM unnest($1, $2) AS f1(k, v)" encoder HD.unit False
|
|
||||||
|
|
||||||
where
|
|
||||||
-- Take a list of (key, value) pairs and encode each as an array to later bind to the query
|
|
||||||
-- see Insert Many section at https://hackage.haskell.org/package/hasql-1.1.1/docs/Hasql-Encoders.html
|
|
||||||
encoder = contramap L.unzip $ contrazip2 (vector HE.text) (vector HE.text)
|
|
||||||
where
|
|
||||||
vector value =
|
|
||||||
HE.value $ HE.array $ HE.arrayDimension foldl' $ HE.arrayValue value
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import PostgREST.ApiRequest (ApiRequest(..))
|
|||||||
import PostgREST.Auth (JWTAttempt(..))
|
import PostgREST.Auth (JWTAttempt(..))
|
||||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||||
import PostgREST.Error (simpleError)
|
import PostgREST.Error (simpleError)
|
||||||
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
|
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtSetLocal)
|
||||||
|
|
||||||
import Protolude hiding (concat, null)
|
import Protolude hiding (concat, null)
|
||||||
|
|
||||||
@@ -32,13 +32,14 @@ runWithClaims conf eClaims app req =
|
|||||||
JWTInvalid e -> return $ unauthed $ show e
|
JWTInvalid e -> return $ unauthed $ show e
|
||||||
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
|
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
|
||||||
JWTClaims claims -> do
|
JWTClaims claims -> do
|
||||||
H.sql $ toS.mconcat $ setSchemaSql ++ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
|
H.sql $ toS.mconcat $ setSchemaSql ++ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
|
||||||
mapM_ H.sql customReqCheck
|
mapM_ H.sql customReqCheck
|
||||||
app req
|
app req
|
||||||
where
|
where
|
||||||
headersSql = map (pgFmtEnvVar "request.header.") $ iHeaders req
|
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req
|
||||||
cookiesSql = map (pgFmtEnvVar "request.cookie.") $ iCookies req
|
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req
|
||||||
claimsSql = map (pgFmtEnvVar "request.jwt.claim.") [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||||
|
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf
|
||||||
setRoleSql = maybeToList $
|
setRoleSql = maybeToList $
|
||||||
(\r -> "set local role " <> r <> ";") . toS . pgFmtLit . unquoted <$> M.lookup "role" claimsWithRole
|
(\r -> "set local role " <> r <> ";") . toS . pgFmtLit . unquoted <$> M.lookup "role" claimsWithRole
|
||||||
setSchemaSql = ["set schema " <> pgFmtLit (configSchema conf) <> ";"] :: [Text]
|
setSchemaSql = ["set schema " <> pgFmtLit (configSchema conf) <> ";"] :: [Text]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module PostgREST.QueryBuilder (
|
|||||||
, requestToCountQuery
|
, requestToCountQuery
|
||||||
, unquoted
|
, unquoted
|
||||||
, ResultsWithCount
|
, ResultsWithCount
|
||||||
, pgFmtEnvVar
|
, pgFmtSetLocal
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Query as H
|
import qualified Hasql.Query as H
|
||||||
@@ -469,8 +469,8 @@ pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
|
|||||||
Nothing -> ""
|
Nothing -> ""
|
||||||
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||||
|
|
||||||
pgFmtEnvVar :: Text -> (Text, Text) -> SqlFragment
|
pgFmtSetLocal :: Text -> (Text, Text) -> SqlFragment
|
||||||
pgFmtEnvVar prefix (k, v) =
|
pgFmtSetLocal prefix (k, v) =
|
||||||
"set local " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
|
"set local " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
|
||||||
|
|
||||||
trimNullChars :: Text -> Text
|
trimNullChars :: Text -> Text
|
||||||
|
|||||||
+3
-3
@@ -6,8 +6,8 @@ import SpecHelper
|
|||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config (pgVersion95, pgVersion96, configSettings)
|
import PostgREST.Config (pgVersion95, pgVersion96)
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion, fillSessionWithSettings)
|
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||||
import PostgREST.Types (DbStructure(..))
|
import PostgREST.Types (DbStructure(..))
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
@@ -66,7 +66,7 @@ main = do
|
|||||||
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
|
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||||
|
|
||||||
let reset :: IO ()
|
let reset :: IO ()
|
||||||
reset = P.use pool (fillSessionWithSettings (configSettings $ testCfg testDbConn)) >> resetDb testDbConn
|
reset = resetDb testDbConn
|
||||||
|
|
||||||
actualPgVersion = pgVersion dbStructure
|
actualPgVersion = pgVersion dbStructure
|
||||||
extraSpecs =
|
extraSpecs =
|
||||||
|
|||||||
@@ -143,6 +143,26 @@ ensureIatClaimWorks(){
|
|||||||
pgrStop
|
pgrStop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ensure app settings don't reset on pool timeout of 10 seconds, see https://github.com/PostgREST/postgrest/issues/1141
|
||||||
|
ensureAppSettings(){
|
||||||
|
pgrStart "./configs/app-settings.config"
|
||||||
|
while pgrStarted && test "$( rootStatus )" -ne 200
|
||||||
|
do
|
||||||
|
# wait for the server to start
|
||||||
|
sleep 0.1 \
|
||||||
|
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
|
||||||
|
done
|
||||||
|
sleep 11
|
||||||
|
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.external_api_secret")
|
||||||
|
if test "$response" = "\"0123456789abcdef\""
|
||||||
|
then
|
||||||
|
ok "GET /rpc/get_guc_value response is $response"
|
||||||
|
else
|
||||||
|
ko "GET /rpc/get_guc_value response was $response"
|
||||||
|
fi
|
||||||
|
pgrStop
|
||||||
|
}
|
||||||
|
|
||||||
# PRE: curl must be available
|
# PRE: curl must be available
|
||||||
test -n "$(command -v curl)" || bailOut 'curl is not available'
|
test -n "$(command -v curl)" || bailOut 'curl is not available'
|
||||||
|
|
||||||
@@ -181,6 +201,7 @@ invalidRoleClaimKey ''
|
|||||||
invalidRoleClaimKey 1234
|
invalidRoleClaimKey 1234
|
||||||
|
|
||||||
ensureIatClaimWorks
|
ensureIatClaimWorks
|
||||||
|
ensureAppSettings
|
||||||
|
|
||||||
cleanUp
|
cleanUp
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
db-uri = "postgres:///postgrest_test"
|
||||||
|
db-schema = "test"
|
||||||
|
db-anon-role = "postgrest_test_anonymous"
|
||||||
|
db-pool = 1
|
||||||
|
server-host = "127.0.0.1"
|
||||||
|
server-port = 49421
|
||||||
|
|
||||||
|
app.settings.external_api_secret = "0123456789abcdef"
|
||||||
Reference in New Issue
Block a user