Pass custom settings to the DB's SESSION (#1063)
- allows queries to refer to current_setting('app.settings.foo') to retrieve variables
- useful for 12-factor apps (app data can be in environment)
- provides workaround for AWS Relational Database Service (RDS) not
allowing `ALTER DATABASE SET 'app.[KEY]' TO '[VALUE]'` on database.
This commit is contained in:
committed by
Joe Nelson
parent
70ce1b9329
commit
a46b6f5020
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Computed columns now only work if they belong to the db-schema - @steve-chavez
|
- Computed columns now only work if they belong to the db-schema - @steve-chavez
|
||||||
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
|
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
|
||||||
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
|
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
|
||||||
|
- The configuration (e.g. `postgrest.conf`) now accepts arbitrary settings that will be passed through as session-local database settings. This can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: `app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take `MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as `current_setting('app.settings.jwt_secret')`. Only `app.settings.*` values in the configuration file are treated in this way. - @canadaduane
|
||||||
|
|
||||||
## [0.4.4.0] - 2018-01-08
|
## [0.4.4.0] - 2018-01-08
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -7,7 +7,8 @@ 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(..))
|
||||||
@@ -32,6 +33,7 @@ import Network.Wai.Handler.Warp (defaultSettings,
|
|||||||
setTimeout)
|
setTimeout)
|
||||||
import System.IO (BufferMode (..),
|
import System.IO (BufferMode (..),
|
||||||
hSetBuffering)
|
hSetBuffering)
|
||||||
|
|
||||||
#ifndef mingw32_HOST_OS
|
#ifndef mingw32_HOST_OS
|
||||||
import System.Posix.Signals
|
import System.Posix.Signals
|
||||||
#endif
|
#endif
|
||||||
@@ -58,10 +60,11 @@ 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 refDbStructure refIsWorkerOn = do
|
connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
|
||||||
isWorkerOn <- readIORef refIsWorkerOn
|
isWorkerOn <- readIORef refIsWorkerOn
|
||||||
unless isWorkerOn $ do
|
unless isWorkerOn $ do
|
||||||
atomicWriteIORef refIsWorkerOn True
|
atomicWriteIORef refIsWorkerOn True
|
||||||
@@ -79,6 +82,7 @@ connectionWorker mainTid pool schema 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
|
||||||
@@ -173,6 +177,7 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
|
(configSettings conf)
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn
|
refIsWorkerOn
|
||||||
--
|
--
|
||||||
@@ -195,6 +200,7 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
|
(configSettings conf)
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn
|
refIsWorkerOn
|
||||||
) Nothing
|
) Nothing
|
||||||
@@ -211,6 +217,7 @@ main = do
|
|||||||
mainTid
|
mainTid
|
||||||
pool
|
pool
|
||||||
(configSchema conf)
|
(configSchema conf)
|
||||||
|
(configSettings conf)
|
||||||
refDbStructure
|
refDbStructure
|
||||||
refIsWorkerOn)
|
refIsWorkerOn)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ library
|
|||||||
, configurator-ng == 0.0.0.1
|
, configurator-ng == 0.0.0.1
|
||||||
, containers
|
, containers
|
||||||
, contravariant
|
, contravariant
|
||||||
|
, contravariant-extras
|
||||||
, either
|
, either
|
||||||
, gitrev
|
, gitrev
|
||||||
, hasql
|
, hasql
|
||||||
|
|||||||
+12
-3
@@ -76,6 +76,7 @@ data AppConfig = AppConfig {
|
|||||||
, configMaxRows :: Maybe Integer
|
, configMaxRows :: Maybe Integer
|
||||||
, configReqCheck :: Maybe Text
|
, configReqCheck :: Maybe Text
|
||||||
, configQuiet :: Bool
|
, configQuiet :: Bool
|
||||||
|
, configSettings :: [(Text, Text)]
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
@@ -136,6 +137,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")
|
||||||
|
|
||||||
case mAppConf of
|
case mAppConf of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
@@ -145,6 +147,13 @@ 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
|
||||||
@@ -159,9 +168,9 @@ readOptions = do
|
|||||||
coerceInt (String x) = readMaybe $ toS x
|
coerceInt (String x) = readMaybe $ toS x
|
||||||
coerceInt _ = Nothing
|
coerceInt _ = Nothing
|
||||||
|
|
||||||
coerceBool :: Value -> Maybe Bool
|
coerceBool :: Value -> Maybe Bool
|
||||||
coerceBool (Bool b) = Just b
|
coerceBool (Bool b) = Just b
|
||||||
coerceBool (String x) = readMaybe $ toS x
|
coerceBool (String b) = readMaybe $ toS b
|
||||||
coerceBool _ = Nothing
|
coerceBool _ = Nothing
|
||||||
|
|
||||||
opts = info (helper <*> pathParser) $
|
opts = info (helper <*> pathParser) $
|
||||||
@@ -218,7 +227,7 @@ pathParser =
|
|||||||
|
|
||||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||||
minimumPgVersion :: PgVersion
|
minimumPgVersion :: PgVersion
|
||||||
minimumPgVersion = PgVersion 90300 "9.3"
|
minimumPgVersion = PgVersion 90400 "9.4"
|
||||||
|
|
||||||
pgVersion96 :: PgVersion
|
pgVersion96 :: PgVersion
|
||||||
pgVersion96 = PgVersion 90600 "9.6"
|
pgVersion96 = PgVersion 90600 "9.6"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module PostgREST.DbStructure (
|
|||||||
, accessibleProcs
|
, accessibleProcs
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
, getPgVersion
|
, getPgVersion
|
||||||
|
, fillSessionWithSettings
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -18,9 +19,11 @@ import qualified Hasql.Query as H
|
|||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
|
import qualified Data.List as List
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Text (split, strip,
|
import Data.Text (split, strip,
|
||||||
breakOn, dropAround, splitOn)
|
breakOn, dropAround,
|
||||||
|
splitOn)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Hasql.Session as H
|
import qualified Hasql.Session as H
|
||||||
import PostgREST.Types
|
import PostgREST.Types
|
||||||
@@ -30,6 +33,9 @@ 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
|
||||||
@@ -747,3 +753,17 @@ 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 List.unzip $ contrazip2 (vector HE.text) (vector HE.text)
|
||||||
|
where
|
||||||
|
vector value =
|
||||||
|
HE.value $ HE.array $ HE.arrayDimension foldl' $ HE.arrayValue value
|
||||||
|
|
||||||
|
|||||||
@@ -699,7 +699,6 @@ spec = do
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
it "multiple cookies ends up as claims" $
|
it "multiple cookies ends up as claims" $
|
||||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
||||||
[json| {"name":"request.cookie.secondcookie"} |]
|
[json| {"name":"request.cookie.secondcookie"} |]
|
||||||
@@ -708,6 +707,14 @@ spec = do
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
it "app settings available" $
|
||||||
|
request methodPost "/rpc/get_guc_value" []
|
||||||
|
[json| { "name": "app.settings.app_host" } |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[str|"localhost"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
describe "values with quotes in IN and NOT IN" $ do
|
describe "values with quotes in IN and NOT IN" $ do
|
||||||
it "succeeds when only quoted values are present" $ do
|
it "succeeds when only quoted values are present" $ do
|
||||||
|
|||||||
+5
-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 (pgVersion96)
|
import PostgREST.Config (pgVersion96, configSettings)
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
import PostgREST.DbStructure (getDbStructure, getPgVersion, fillSessionWithSettings)
|
||||||
import PostgREST.Types (DbStructure(..))
|
import PostgREST.Types (DbStructure(..))
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
@@ -58,7 +58,9 @@ main = do
|
|||||||
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
|
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool $ pure ()
|
||||||
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure ()
|
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool $ pure ()
|
||||||
|
|
||||||
let reset = resetDb testDbConn
|
let reset :: IO ()
|
||||||
|
reset = P.use pool (fillSessionWithSettings (configSettings $ testCfg testDbConn)) >> resetDb testDbConn
|
||||||
|
|
||||||
actualPgVersion = pgVersion dbStructure
|
actualPgVersion = pgVersion dbStructure
|
||||||
pg96spec | actualPgVersion >= pgVersion96 = [("Feature.PgVersion96Spec" , Feature.PgVersion96Spec.spec)]
|
pg96spec | actualPgVersion >= pgVersion96 = [("Feature.PgVersion96Spec" , Feature.PgVersion96Spec.spec)]
|
||||||
| otherwise = []
|
| otherwise = []
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ _baseCfg = -- Connection Settings
|
|||||||
10 Nothing (Just "test.switch_role")
|
10 Nothing (Just "test.switch_role")
|
||||||
-- Debug Settings
|
-- Debug Settings
|
||||||
True
|
True
|
||||||
|
[ ("app.settings.app_host", "localhost")
|
||||||
|
, ("app.settings.external_api_secret", "0123456789abcdef")
|
||||||
|
]
|
||||||
|
|
||||||
testCfg :: Text -> AppConfig
|
testCfg :: Text -> AppConfig
|
||||||
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
|
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
|
||||||
|
|||||||
Reference in New Issue
Block a user