feat: Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14
Getting the value for a header GUC on PostgreSQL v14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims`
PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
module Feature.LegacyGucsSpec where
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Network.HTTP.Types
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
import Protolude hiding (get)
|
||||
import SpecHelper
|
||||
|
||||
spec :: SpecWith ((), Application)
|
||||
spec =
|
||||
describe "remote procedure call with legacy gucs disabled" $ do
|
||||
it "custom header is set" $
|
||||
request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")]
|
||||
[json| { "prefix": "request.headers", "name": "custom-header" } |]
|
||||
`shouldRespondWith`
|
||||
[json|"test"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
}
|
||||
|
||||
it "standard header is set" $
|
||||
request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")]
|
||||
[json| { "prefix": "request.headers", "name": "origin" } |]
|
||||
`shouldRespondWith`
|
||||
[json|"http://example.com"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
}
|
||||
|
||||
it "current role is available as GUC claim" $
|
||||
request methodPost "/rpc/get_guc_value" []
|
||||
[json| { "prefix": "request.jwt.claims", "name": "role" } |]
|
||||
`shouldRespondWith`
|
||||
[json|"postgrest_test_anonymous"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [ matchContentTypeJson ]
|
||||
}
|
||||
|
||||
it "single cookie ends up as claims" $
|
||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
|
||||
[json| {"prefix": "request.cookies", "name":"acookie"} |]
|
||||
`shouldRespondWith`
|
||||
[json|"cookievalue"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
it "multiple cookies ends up as claims" $
|
||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
||||
[json| {"prefix": "request.cookies", "name":"secondcookie"} |]
|
||||
`shouldRespondWith`
|
||||
[json|"anothervalue"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
it "gets the Authorization value" $
|
||||
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
|
||||
[json| {"prefix": "request.headers", "name":"authorization"} |]
|
||||
`shouldRespondWith`
|
||||
[json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = []
|
||||
}
|
||||
+37
-7
@@ -15,7 +15,7 @@ import Text.Heredoc
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||
pgVersion109, pgVersion110,
|
||||
pgVersion112, pgVersion114,
|
||||
pgVersion96)
|
||||
pgVersion140, pgVersion96)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import SpecHelper
|
||||
@@ -798,7 +798,12 @@ spec actualPgVersion =
|
||||
it "custom header is set" $
|
||||
request methodPost "/rpc/get_guc_value"
|
||||
[("Custom-Header", "test")]
|
||||
[json| { "name": "request.header.custom-header" } |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| { "prefix": "request.headers", "name": "custom-header" } |]
|
||||
else
|
||||
[json| { "name": "request.header.custom-header" } |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"test"|]
|
||||
{ matchStatus = 200
|
||||
@@ -807,7 +812,12 @@ spec actualPgVersion =
|
||||
it "standard header is set" $
|
||||
request methodPost "/rpc/get_guc_value"
|
||||
[("Origin", "http://example.com")]
|
||||
[json| { "name": "request.header.origin" } |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| { "prefix": "request.headers", "name": "origin" } |]
|
||||
else
|
||||
[json| { "name": "request.header.origin" } |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"http://example.com"|]
|
||||
{ matchStatus = 200
|
||||
@@ -815,7 +825,12 @@ spec actualPgVersion =
|
||||
}
|
||||
it "current role is available as GUC claim" $
|
||||
request methodPost "/rpc/get_guc_value" []
|
||||
[json| { "name": "request.jwt.claim.role" } |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| { "prefix": "request.jwt.claims", "name": "role" } |]
|
||||
else
|
||||
[json| { "name": "request.jwt.claim.role" } |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"postgrest_test_anonymous"|]
|
||||
{ matchStatus = 200
|
||||
@@ -823,7 +838,12 @@ spec actualPgVersion =
|
||||
}
|
||||
it "single cookie ends up as claims" $
|
||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
|
||||
[json| {"name":"request.cookie.acookie"} |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| {"prefix": "request.cookies", "name":"acookie"} |]
|
||||
else
|
||||
[json| {"name":"request.cookie.acookie"} |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"cookievalue"|]
|
||||
{ matchStatus = 200
|
||||
@@ -831,7 +851,12 @@ spec actualPgVersion =
|
||||
}
|
||||
it "multiple cookies ends up as claims" $
|
||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
||||
[json| {"name":"request.cookie.secondcookie"} |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| {"prefix": "request.cookies", "name":"secondcookie"} |]
|
||||
else
|
||||
[json| {"name":"request.cookie.secondcookie"} |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"anothervalue"|]
|
||||
{ matchStatus = 200
|
||||
@@ -847,7 +872,12 @@ spec actualPgVersion =
|
||||
}
|
||||
it "gets the Authorization value" $
|
||||
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
|
||||
[json| {"name":"request.header.authorization"} |]
|
||||
(
|
||||
if actualPgVersion >= pgVersion140 then
|
||||
[json| {"prefix": "request.headers", "name":"authorization"} |]
|
||||
else
|
||||
[json| {"name":"request.header.authorization"} |]
|
||||
)
|
||||
`shouldRespondWith`
|
||||
[json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
|
||||
{ matchStatus = 200
|
||||
|
||||
@@ -36,6 +36,7 @@ import qualified Feature.HtmlRawOutputSpec
|
||||
import qualified Feature.IgnorePrivOpenApiSpec
|
||||
import qualified Feature.InsertSpec
|
||||
import qualified Feature.JsonOperatorSpec
|
||||
import qualified Feature.LegacyGucsSpec
|
||||
import qualified Feature.MultipleSchemaSpec
|
||||
import qualified Feature.NoJwtSpec
|
||||
import qualified Feature.NonexistentSchemaSpec
|
||||
@@ -88,6 +89,7 @@ main = do
|
||||
(configDbSchemas config)
|
||||
(configDbExtraSearchPath config)
|
||||
appState <- AppState.initWithPool pool config
|
||||
AppState.putPgVersion appState actualPgVersion
|
||||
AppState.putDbStructure appState customDbStructure
|
||||
when (isJust $ configDbRootSpec config) $
|
||||
AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure
|
||||
@@ -108,6 +110,7 @@ main = do
|
||||
responseHeadersApp = app testCfgResponseHeaders
|
||||
disallowRollbackApp = app testCfgDisallowRollback
|
||||
forceRollbackApp = app testCfgForceRollback
|
||||
testCfgLegacyGucsApp = app testCfgLegacyGucs
|
||||
|
||||
extraSearchPathApp = appDbs testCfgExtraSearchPath
|
||||
unicodeApp = appDbs testUnicodeCfg
|
||||
@@ -210,6 +213,10 @@ main = do
|
||||
parallel $ before multipleSchemaApp $
|
||||
describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion
|
||||
|
||||
-- this test runs with db-uses-legacy-gucs = false
|
||||
parallel $ before testCfgLegacyGucsApp $
|
||||
describe "Feature.LegacyGucsSpec" Feature.LegacyGucsSpec.spec
|
||||
|
||||
-- this test runs with db-embed-default-join = inner
|
||||
before embedInnerJoinApp $
|
||||
describe "Feature.EmbedInnerJoinSpecNotDefaultConfig" Feature.EmbedInnerJoinSpec.notDefaultConfig
|
||||
|
||||
@@ -91,6 +91,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
||||
, configDbConfig = False
|
||||
, configDbUri = mempty
|
||||
, configDbEmbedDefaultJoin = JTLeft
|
||||
, configDbUseLegacyGucs = True
|
||||
, configFilePath = Nothing
|
||||
, configJWKS = parseSecret <$> secret
|
||||
, configJwtAudience = Nothing
|
||||
@@ -190,6 +191,9 @@ testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configDbPreRequest =
|
||||
testMultipleSchemaCfg :: Text -> AppConfig
|
||||
testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] }
|
||||
|
||||
testCfgLegacyGucs :: Text -> AppConfig
|
||||
testCfgLegacyGucs testDbConn = (testCfg testDbConn) { configDbUseLegacyGucs = False }
|
||||
|
||||
resetDb :: Text -> IO ()
|
||||
resetDb dbConn = loadFixture dbConn "data"
|
||||
|
||||
|
||||
Vendored
+52
-15
@@ -82,7 +82,10 @@ CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
NEW.owner = current_setting('request.jwt.claim.id');
|
||||
NEW.owner = case when current_setting('server_version_num')::int >= 140000
|
||||
then current_setting('request.jwt.claims')::json->>'id'
|
||||
else current_setting('request.jwt.claim.id')
|
||||
end;
|
||||
RETURN NEW;
|
||||
end
|
||||
$$;
|
||||
@@ -301,7 +304,10 @@ CREATE OR REPLACE FUNCTION switch_role() RETURNS void
|
||||
declare
|
||||
user_id text;
|
||||
Begin
|
||||
user_id = current_setting('request.jwt.claim.id')::text;
|
||||
user_id = case when current_setting('server_version_num')::int >= 140000
|
||||
then (current_setting('request.jwt.claims')::json->>'id')::text
|
||||
else current_setting('request.jwt.claim.id')::text
|
||||
end;
|
||||
if user_id = '1'::text then
|
||||
execute 'set local role postgrest_test_author';
|
||||
elseif user_id = '2'::text then
|
||||
@@ -329,18 +335,33 @@ CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
|
||||
iss text, sub text, exp bigint,
|
||||
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
||||
)
|
||||
LANGUAGE sql SECURITY DEFINER
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
STABLE
|
||||
AS $$
|
||||
SELECT current_setting('request.jwt.claim.iss') as iss,
|
||||
current_setting('request.jwt.claim.sub') as sub,
|
||||
current_setting('request.jwt.claim.exp')::bigint as exp,
|
||||
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
||||
current_setting('request.jwt.claim.iat')::bigint as iat,
|
||||
current_setting('request.jwt.claim.jti') as jti,
|
||||
-- role is not included in the claims list
|
||||
current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean
|
||||
as "http://postgrest.com/foo";
|
||||
BEGIN
|
||||
-- JWT claims are set in JSON format since v14
|
||||
IF (current_setting('server_version_num')::INT >= 140000) THEN
|
||||
RETURN QUERY
|
||||
SELECT current_setting('request.jwt.claims')::json->>'iss' as iss,
|
||||
current_setting('request.jwt.claims')::json->>'sub' as sub,
|
||||
(current_setting('request.jwt.claims')::json->>'exp')::bigint as exp,
|
||||
(current_setting('request.jwt.claims')::json->>'nbf')::bigint as nbf,
|
||||
(current_setting('request.jwt.claims')::json->>'iat')::bigint as iat,
|
||||
current_setting('request.jwt.claims')::json->>'jti' as jti,
|
||||
(current_setting('request.jwt.claims')::json->>'http://postgrest.com/foo')::boolean
|
||||
as "http://postgrest.com/foo";
|
||||
ELSE
|
||||
RETURN QUERY
|
||||
SELECT current_setting('request.jwt.claim.iss') as iss,
|
||||
current_setting('request.jwt.claim.sub') as sub,
|
||||
current_setting('request.jwt.claim.exp')::bigint as exp,
|
||||
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
||||
current_setting('request.jwt.claim.iat')::bigint as iat,
|
||||
current_setting('request.jwt.claim.jti') as jti,
|
||||
current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean
|
||||
as "http://postgrest.com/foo";
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -1093,6 +1114,11 @@ create function test.get_guc_value(name text) returns text as $$
|
||||
select nullif(current_setting(name), '')::text;
|
||||
$$ language sql;
|
||||
|
||||
-- Get the GUC values for Postgres v14.0 and up
|
||||
create function test.get_guc_value(prefix text, name text) returns text as $$
|
||||
select nullif(current_setting(prefix)::json->>name, '')::text;
|
||||
$$ language sql;
|
||||
|
||||
create table w_or_wo_comma_names ( name text );
|
||||
|
||||
create table items_with_different_col_types (
|
||||
@@ -1784,8 +1810,13 @@ openapi json = $$
|
||||
}
|
||||
}
|
||||
$$;
|
||||
accept text;
|
||||
begin
|
||||
case current_setting('request.header.accept', true)
|
||||
accept = case when current_setting('server_version_num')::int >= 140000
|
||||
then current_setting('request.headers', true)::json->>'accept'
|
||||
else current_setting('request.header.accept', true)
|
||||
end;
|
||||
case accept
|
||||
when 'application/openapi+json' then
|
||||
return openapi;
|
||||
when 'application/json' then
|
||||
@@ -1958,9 +1989,15 @@ add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift
|
||||
-- for a pre-request function
|
||||
create or replace function custom_headers() returns void as $$
|
||||
declare
|
||||
user_agent text := current_setting('request.header.user-agent', true);
|
||||
user_agent text := case when current_setting('server_version_num')::int >= 140000
|
||||
then current_setting('request.headers', true)::json->>'user-agent'
|
||||
else current_setting('request.header.user-agent', true)
|
||||
end;
|
||||
req_path text := current_setting('request.path', true);
|
||||
req_accept text := current_setting('request.header.accept', true);
|
||||
req_accept text := case when current_setting('server_version_num')::int >= 140000
|
||||
then current_setting('request.headers', true)::json->>'accept'
|
||||
else current_setting('request.header.accept', true)
|
||||
end;
|
||||
req_method text := current_setting('request.method', true);
|
||||
begin
|
||||
if user_agent similar to 'MSIE (6.0|7.0)' then
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "commit"
|
||||
db-uri = "required"
|
||||
db-embed-default-join = "left"
|
||||
db-use-legacy-gucs = true
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"aliased\""
|
||||
jwt-secret = ""
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "commit"
|
||||
db-uri = "required"
|
||||
db-embed-default-join = "left"
|
||||
db-use-legacy-gucs = true
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"role\""
|
||||
jwt-secret = ""
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "commit"
|
||||
db-uri = "required"
|
||||
db-embed-default-join = "left"
|
||||
db-use-legacy-gucs = true
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"role\""
|
||||
jwt-secret = ""
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "commit"
|
||||
db-uri = "required"
|
||||
db-embed-default-join = "left"
|
||||
db-use-legacy-gucs = true
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"role\""
|
||||
jwt-secret = ""
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "true"
|
||||
db-tx-end = "rollback-allow-override"
|
||||
db-uri = "<REPLACED_WITH_DB_URI>"
|
||||
db-embed-default-join = "inner"
|
||||
db-use-legacy-gucs = false
|
||||
jwt-aud = "https://otherexample.org"
|
||||
jwt-role-claim-key = ".\"other\".\"role\""
|
||||
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "true"
|
||||
db-tx-end = "commit-allow-override"
|
||||
db-uri = "<REPLACED_WITH_DB_URI>"
|
||||
db-embed-default-join = "inner"
|
||||
db-use-legacy-gucs = false
|
||||
jwt-aud = "https://example.org"
|
||||
jwt-role-claim-key = ".\"a\".\"role\""
|
||||
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "rollback-allow-override"
|
||||
db-uri = "tmp_db"
|
||||
db-embed-default-join = "inner"
|
||||
db-use-legacy-gucs = false
|
||||
jwt-aud = "https://postgrest.org"
|
||||
jwt-role-claim-key = ".\"user\"[0].\"real-role\""
|
||||
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "true"
|
||||
db-tx-end = "commit"
|
||||
db-uri = "required"
|
||||
db-embed-default-join = "left"
|
||||
db-use-legacy-gucs = true
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"role\""
|
||||
jwt-secret = ""
|
||||
|
||||
@@ -15,6 +15,7 @@ PGRST_DB_CONFIG: false
|
||||
PGRST_DB_TX_END: rollback-allow-override
|
||||
PGRST_DB_URI: tmp_db
|
||||
PGRST_DB_EMBED_DEFAULT_JOIN: inner
|
||||
PGRST_DB_USE_LEGACY_GUCS: false
|
||||
PGRST_JWT_AUD: 'https://postgrest.org'
|
||||
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
|
||||
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
|
||||
|
||||
@@ -13,6 +13,7 @@ db-config = "false"
|
||||
db-tx-end = "rollback-allow-override"
|
||||
db-uri = "tmp_db"
|
||||
db-embed-default-join = "inner"
|
||||
db-use-legacy-gucs = false
|
||||
jwt-aud = "https://postgrest.org"
|
||||
jwt-role-claim-key = ".user[0].\"real-role\""
|
||||
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||
|
||||
Reference in New Issue
Block a user