change: use RFC 9535 syntax for jwt-role-claim-key config

BREAKING CHANGE

Breaks the string comparison operators implemented in #3813. Those can
be replaced with regex searches using JSON Path `search()` function.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
This commit is contained in:
Taimoor Zaeem
2026-06-26 18:32:05 +00:00
committed by Wolfgang Walther
parent a0bb87d693
commit bb63c3fade
37 changed files with 123 additions and 419 deletions
+1 -1
View File
@@ -2,6 +2,6 @@ db-schema = "provided_through_alias"
db-pool-timeout = 5
max-rows = 1000
pre-request = "check_alias"
role-claim-key = ".aliased"
role-claim-key = "$$.aliased"
root-spec = "open_alias"
secret-is-base64 = true
+1 -1
View File
@@ -27,7 +27,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"aliased\""
jwt-role-claim-key = "$$.aliased"
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
@@ -27,7 +27,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"role\""
jwt-role-claim-key = "$$.role"
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
@@ -27,7 +27,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"role\""
jwt-role-claim-key = "$$.role"
jwt-secret = ""
jwt-secret-is-base64 = true
log-level = "error"
+1 -1
View File
@@ -27,7 +27,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"role\""
jwt-role-claim-key = "$$.role"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
@@ -1,44 +0,0 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-config = true
db-extra-search-path = "public"
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
db-max-rows = ""
db-plan-enabled = false
db-pool = 10
db-pool-acquisition-timeout = 10
db-pool-automatic-recovery = true
db-pool-max-idletime = 30
db-pool-max-lifetime = 1800
db-pre-config = ""
db-pre-request = ""
db-prepared-statements = true
db-root-spec = ""
db-schemas = "public"
db-timezone-enabled = true
db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"roles\"[?(@ == \"role1\")]"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
log-query = false
openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
server-unix-socket-mode = "660"
@@ -1,44 +0,0 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-config = true
db-extra-search-path = "public"
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
db-max-rows = ""
db-plan-enabled = false
db-pool = 10
db-pool-acquisition-timeout = 10
db-pool-automatic-recovery = true
db-pool-max-idletime = 30
db-pool-max-lifetime = 1800
db-pre-config = ""
db-pre-request = ""
db-prepared-statements = true
db-root-spec = ""
db-schemas = "public"
db-timezone-enabled = true
db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"roles\"[?(@ != \"role1\")]"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
log-query = false
openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
server-unix-socket-mode = "660"
@@ -1,44 +0,0 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-config = true
db-extra-search-path = "public"
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
db-max-rows = ""
db-plan-enabled = false
db-pool = 10
db-pool-acquisition-timeout = 10
db-pool-automatic-recovery = true
db-pool-max-idletime = 30
db-pool-max-lifetime = 1800
db-pre-config = ""
db-pre-request = ""
db-prepared-statements = true
db-root-spec = ""
db-schemas = "public"
db-timezone-enabled = true
db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"roles\"[?(@ ^== \"role1\")]"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
log-query = false
openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
server-unix-socket-mode = "660"
@@ -1,44 +0,0 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-config = true
db-extra-search-path = "public"
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
db-max-rows = ""
db-plan-enabled = false
db-pool = 10
db-pool-acquisition-timeout = 10
db-pool-automatic-recovery = true
db-pool-max-idletime = 30
db-pool-max-lifetime = 1800
db-pre-config = ""
db-pre-request = ""
db-prepared-statements = true
db-root-spec = ""
db-schemas = "public"
db-timezone-enabled = true
db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"roles\"[?(@ ==^ \"role1\")]"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
log-query = false
openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
server-unix-socket-mode = "660"
@@ -1,44 +0,0 @@
admin-server-host = "!4"
admin-server-port = ""
admin-server-unix-socket = ""
admin-server-unix-socket-mode = "660"
client-error-verbosity = "verbose"
db-aggregates-enabled = false
db-anon-role = ""
db-channel = "pgrst"
db-channel-enabled = true
db-config = true
db-extra-search-path = "public"
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
db-max-rows = ""
db-plan-enabled = false
db-pool = 10
db-pool-acquisition-timeout = 10
db-pool-automatic-recovery = true
db-pool-max-idletime = 30
db-pool-max-lifetime = 1800
db-pre-config = ""
db-pre-request = ""
db-prepared-statements = true
db-root-spec = ""
db-schemas = "public"
db-timezone-enabled = true
db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"roles\"[?(@ *== \"role1\")]"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
log-query = false
openapi-mode = "follow-privileges"
openapi-security-active = false
openapi-server-proxy-uri = ""
server-cors-allowed-origins = ""
server-host = "!4"
server-port = 3000
server-timing-enabled = false
server-trace-header = ""
server-unix-socket = ""
server-unix-socket-mode = "660"
@@ -29,7 +29,7 @@ db-tx-end = "rollback-allow-override"
db-uri = "postgresql://"
jwt-aud = "https://otherexample.org"
jwt-cache-max-entries = 86400
jwt-role-claim-key = ".\"other\".\"pre_config_role\""
jwt-role-claim-key = "$$.other.pre_config_role"
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = false
log-level = "info"
@@ -29,7 +29,7 @@ db-tx-end = "commit-allow-override"
db-uri = "postgresql://"
jwt-aud = "https://example.org"
jwt-cache-max-entries = 86400
jwt-role-claim-key = ".\"a\".\"role\""
jwt-role-claim-key = "$$.a.role"
jwt-secret = "OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE"
jwt-secret-is-base64 = false
log-level = "info"
+1 -1
View File
@@ -29,7 +29,7 @@ db-tx-end = "rollback-allow-override"
db-uri = "tmp_db"
jwt-aud = "https://postgrest.org"
jwt-cache-max-entries = 86400
jwt-role-claim-key = ".\"user\"[0].\"real-role\""
jwt-role-claim-key = "$$.user[0].real_role"
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ="
jwt-secret-is-base64 = true
log-level = "info"
+1 -1
View File
@@ -28,7 +28,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"role\""
jwt-role-claim-key = "$$.role"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "error"
+1 -1
View File
@@ -27,7 +27,7 @@ db-tx-end = "commit"
db-uri = "postgresql://"
jwt-aud = ""
jwt-cache-max-entries = 1000
jwt-role-claim-key = ".\"role\""
jwt-role-claim-key = "$$.role"
jwt-secret = ""
jwt-secret-is-base64 = false
log-level = "crit"
@@ -1,2 +0,0 @@
# For coverage of config dumping with jspath string comparison operator. We allow 5 different operators, so each file test 1 operator.
jwt-role-claim-key = ".roles[?(@ == \"role1\")]"
@@ -1 +0,0 @@
jwt-role-claim-key = ".roles[?(@ != \"role1\")]"
@@ -1 +0,0 @@
jwt-role-claim-key = ".roles[?(@ ^== \"role1\")]"
@@ -1 +0,0 @@
jwt-role-claim-key = ".roles[?(@ ==^ \"role1\")]"
@@ -1 +0,0 @@
jwt-role-claim-key = ".roles[?(@ *== \"role1\")]"
+1 -1
View File
@@ -25,7 +25,7 @@ PGRST_DB_TX_END: rollback-allow-override
PGRST_DB_URI: tmp_db
PGRST_DB_USE_LEGACY_GUCS: false
PGRST_JWT_AUD: 'https://postgrest.org'
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
PGRST_JWT_ROLE_CLAIM_KEY: '$.user[0].real_role'
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ=
PGRST_JWT_SECRET_IS_BASE64: true
PGRST_JWT_CACHE_MAX_ENTRIES: 86400
+1 -1
View File
@@ -22,7 +22,7 @@ db-timezone-enabled = false
db-tx-end = "rollback-allow-override"
db-uri = "tmp_db"
jwt-aud = "https://postgrest.org"
jwt-role-claim-key = ".user[0].\"real-role\""
jwt-role-claim-key = "$$.user[0].real_role"
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5aW5iYXNlNjQ="
jwt-secret-is-base64 = true
jwt-cache-max-entries = 86400
+2 -2
View File
@@ -17,7 +17,7 @@ ALTER ROLE db_config_authenticator SET pgrst.db_timezone_enabled = 'false';
ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override';
ALTER ROLE db_config_authenticator SET pgrst.jwt_aud = 'https://example.org';
ALTER ROLE db_config_authenticator SET pgrst.jwt_cache_max_entries = '86400';
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.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.not_existing = 'should be ignored';
@@ -93,7 +93,7 @@ returns void as $$
begin
if current_user = 'other_authenticator' then
perform
set_config('pgrst.jwt_role_claim_key', '."other"."pre_config_role"', true)
set_config('pgrst.jwt_role_claim_key', '$.other.pre_config_role', true)
, set_config('pgrst.db_anon_role', 'pre_config_role', true)
, set_config('pgrst.db_schemas', 'will be overriden with the above ALTER ROLE.. db_schemas', true)
, set_config('pgrst.db_tx_end', 'rollback-allow-override', true);
+13 -45
View File
@@ -120,13 +120,13 @@ cli:
PGRST_DB_TX_END: rollback
roleclaims:
- key: '.postgrest.a_role'
- key: '$.postgrest.a_role'
data:
postgrest:
a_role: postgrest_test_author
other: claims
expected_status: 200
- key: '.customObject.manyRoles[1]'
- key: '$.customObject.manyRoles[1]'
data:
customObject:
manyRoles:
@@ -134,92 +134,60 @@ roleclaims:
- postgrest_test_author
other: {}
expected_status: 200
- key: '."https://www.example.com/roles"[0].value'
- key: '$["https://www.example.com/roles"][0].value'
data:
'https://www.example.com/roles':
- value: postgrest_test_author
other: 666
expected_status: 200
- key: '.myDomain[3]'
- key: '$.myDomain[3]'
data:
myDomain:
- other
- postgrest_test_author
other: 1.23
expected_status: 401
- key: '.myRole'
- key: '$.myRole'
data:
role: postgrest_test_author
other: true
expected_status: 401
# https://github.com/PostgREST/postgrest/pull/3813
- key: '.realm_access.roles[?(@ == "postgrest_test_author")]'
- key: '$.realm_access.roles[?(@ == "postgrest_test_author")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- key: '.realm_access.roles[?(@ != "other")]'
- key: '$.realm_access.roles[?(@ != "other")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- key: '.realm_access.roles[?(@ ^== "postgrest_te")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- key: '.realm_access.roles[?(@ ==^ "st_test_author")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- key: '.realm_access.roles[?(@ *== "_test_")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- key: '.realm_access.roles[?(@ == "string")]'
data:
realm_access:
roles:
- obj_key: obj_value
expected_status: 401 # fails because it compares an object with a string
jwtaudroleclaims:
- key: '.aud'
- key: '$.aud'
data:
aud: postgrest_test_author
expected_status: 200
- key: '.aud'
- key: '$.aud'
data:
aud: postgrest_test_invalid
expected_status: 401
- key: '.aud[0]'
- key: '$.aud[0]'
data:
aud: [postgrest_test_author]
expected_status: 200
- key: '.aud[1]' # succeeds the aud claims check, but fail when hits the db
- key: '$.aud[1]' # succeeds the aud claims check, but fail when hits the db
data:
aud: [postgrest_test_author, postgrest_test_invalid]
expected_status: 401
invalidroleclaimkeys:
- 'role.other'
- '.role##'
- '.my_role;;domain'
- '.#$$%&$%/'
- '1234'
- '.role[?(@ =)]'
- '.role.other'
- '$.my_role;;domain'
invalidopenapimodes:
- 'follow-'
+2 -2
View File
@@ -24,10 +24,10 @@ import qualified Jose.Jwt as JWT
import Network.HTTP.Types
import qualified PostgREST.AppState as AppState
import PostgREST.Config (AppConfig (..),
JSPathExp (..),
LogLevel (..),
OpenAPIMode (..),
Verbosity (..),
defaultRoleJSPathKey,
parseSecret)
import qualified PostgREST.Metrics as Metrics
import PostgREST.Observation (Observation (..))
@@ -96,7 +96,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configFilePath = Nothing
, configJWKS = rightToMaybe $ parseSecret secret
, configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"]
, configJwtRoleClaimKey = defaultRoleJSPathKey -- $.role
, configJwtSecret = Just secret
, configJwtSecretIsBase64 = False
, configJwtCacheMaxEntries = 10
+4 -3
View File
@@ -25,10 +25,11 @@ import System.IO.Unsafe (unsafePerformIO)
import Text.Regex.TDFA ((=~))
import PostgREST.Config (AppConfig (..),
JSPathExp (..),
LogLevel (..),
OpenAPIMode (..),
Verbosity (..), parseSecret)
Verbosity (..),
defaultRoleJSPathKey,
parseSecret)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import Data.Aeson.Lens
@@ -159,7 +160,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configFilePath = Nothing
, configJWKS = rightToMaybe $ parseSecret secret
, configJwtAudience = Nothing
, configJwtRoleClaimKey = [JSPKey "role"]
, configJwtRoleClaimKey = defaultRoleJSPathKey -- $.role
, configJwtSecret = Just secret
, configJwtSecretIsBase64 = False
, configJwtCacheMaxEntries = 10