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
+13
View File
@@ -34,6 +34,19 @@ All notable changes to this project will be documented in this file. From versio
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
- Config `jwt-role-claim-key` now uses RFC 9535 syntax for JSON Path by @taimoorzaeem in #4984
#### Changed Syntax for JWT Role Extraction
The `jwt-role-claim-key` config should be updated according to the following:
- All config values must start with `$` character.
+ Example: `.roles.read` -> `$.roles.read`
- Keys with special characters, with the exception of `_` char must be quoted.
+ Example: `.roles.write-role` -> `$.roles["write-role"]`
- String comparison operators (`^==`, `==^` and `*==`) are replaced with regular expression search.
+ Example: `.roles[?(@ ^== "postgrest_test_")]` -> `$.roles[?search(@, "^postgrest_test_")]`
- Detailed reference for syntax: [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html#name-jsonpath-syntax-and-semanti).
## [14.13] - 2026-06-04
-2
View File
@@ -30,7 +30,6 @@ CSV
durations
DDL
DOM
DSL
DevOps
Dramatiq
dockerize
@@ -76,7 +75,6 @@ isdistinct
JS
js
JSON
JSPath
JWK
JWT
jwt
+11 -20
View File
@@ -224,40 +224,31 @@ It's recommended to leave the JWT cache enabled as our load tests indicate ~20%
JWT Role Extraction
-------------------
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
A JSON Path (`RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_) can be specified for the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
- ``==`` selects the first array element that exactly matches the right operand
- ``!=`` selects the first array element that does not match the right operand
- ``^==`` selects the first array element that starts with the right operand
- ``==^`` selects the first array element that ends with the right operand
- ``*==`` selects the first array element that contains the right operand
You can quickly try out JSON Path by visiting https://serdejsonpath.live.
Usage examples:
.. code:: bash
# {"postgrest":{"roles": ["other", "author"]}}
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
jwt-role-claim-key = ".postgrest.roles[1]"
jwt-role-claim-key = "$$.postgrest.roles[1]"
# {"https://www.example.com/role": { "key": "author" }}
# non-alphanumerical characters can go inside quotes(escaped in the config value)
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
# non-alphanumerical characters can go inside single quotes
jwt-role-claim-key = "$$['https://www.example.com/role'].key"
# {"postgrest":{"roles": ["other", "author"]}}
# `@` represents the current element in the array
# all the these match the string "author"
jwt-role-claim-key = ".postgrest.roles[?(@ == \"author\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ != \"other\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ^== \"aut\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
# filter based on equality or regular expression
jwt-role-claim-key = "$$.postgrest.roles[?(@ == 'author')]"
jwt-role-claim-key = "$$.postgrest.roles[?search(@, '^au')]"
.. note::
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
- If JSON Path query returns multiple values, the first one gets selected.
- Only when using the :ref:`file_config`, all ``$`` characters in the value must be escaped with an additional ``$`` char. For :ref:`env_variables_config` and :ref:`in_db_config`, only use a single ``$`` char.
- In our implementation, only the `search()` function from `JSON Path Functions <https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions>`_ is available for filtering.
JWT Security
------------
+5 -1
View File
@@ -694,7 +694,7 @@ jwt-role-claim-key
=============== =================================
**Type** String
**Default** .role
**Default** $.role
**Reloadable** Y
**Environment** PGRST_JWT_ROLE_CLAIM_KEY
**In-Database** pgrst.jwt_role_claim_key
@@ -704,6 +704,10 @@ jwt-role-claim-key
See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
.. warning::
Only when using :ref:`file_config`, the ``$`` char needs to be escaped, so use ``$$`` and PostgREST will interpret it as a single ``$`` character.
.. _jwt-secret:
jwt-secret
+10
View File
@@ -49,6 +49,16 @@ let
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
fuzzyset = prev.fuzzyset_0_2_4;
# TODO: Remove once available in nixpkgs
aeson-jsonpath =
prev.callHackageDirect
{
pkg = "aeson-jsonpath";
ver = "0.4.2.0";
sha256 = "sha256-K+3brf1zjSSjojtSCXFrip5rrP7AO/S4zndAxAnvEfc=";
}
{ };
http2 =
prev.callHackageDirect
{
+1
View File
@@ -103,6 +103,7 @@ library
, HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.6
, aeson >= 2.0.3 && < 2.3
, aeson-jsonpath >= 0.4.2 && < 0.5
, auto-update >= 0.1.4 && < 0.3
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
+2 -2
View File
@@ -31,7 +31,7 @@ import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), audMatchesCfg)
import PostgREST.Config.JSPath (walkJSPath)
import PostgREST.Config.JSPath (evaluateJSPath)
import PostgREST.Error (Error (..), JwtClaimsError (..),
JwtDecodeError (..), JwtError (..))
@@ -114,7 +114,7 @@ parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims
validateClaims time (audMatchesCfg cfg) mclaims
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
unquoted <$> evaluateJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
pure AuthResult
{ authClaims = mclaims
, authRole = role
+6 -7
View File
@@ -15,8 +15,7 @@ module PostgREST.Config
( AppConfig (..)
, Environment
, JSPath
, JSPathExp(..)
, FilterExp(..)
, defaultRoleJSPathKey
, LogLevel(..)
, OpenAPIMode(..)
, Proxy(..)
@@ -63,9 +62,9 @@ import System.Posix.Types (FileMode)
import PostgREST.Config.Database (RoleIsolationLvl,
RoleSettings)
import PostgREST.Config.JSPath (FilterExp (..), JSPath,
JSPathExp (..), dumpJSPath,
pRoleClaimKey)
import PostgREST.Config.JSPath (JSPath (..),
defaultRoleJSPathKey,
dumpJSPath, pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
@@ -192,7 +191,7 @@ toText conf =
,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri)
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
,("jwt-role-claim-key", q . dumpJSPath . configJwtRoleClaimKey)
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("jwt-cache-max-entries", show . configJwtCacheMaxEntries)
@@ -428,7 +427,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath
parseRoleClaimKey k al =
optWithAlias (optString k) (optString al) >>= \case
Nothing -> pure [JSPKey "role"]
Nothing -> pure defaultRoleJSPathKey -- $.role
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
parseCORSAllowedOrigins k =
+34 -97
View File
@@ -1,123 +1,60 @@
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
{-# LANGUAGE LambdaCase #-}
{-|
Module : PostgREST.Config.JSPath
Description : Parsing and evaluation logic of JSPath
-}
module PostgREST.Config.JSPath
( JSPath
, JSPathExp(..)
, FilterExp(..)
( JSPath(..)
, defaultRoleJSPathKey
, dumpJSPath
, pRoleClaimKey
, walkJSPath
, evaluateJSPath
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Aeson.JSONPath as JSP
import qualified Data.Aeson.JSONPath.Parser as JSP
import qualified Data.Aeson.JSONPath.Types as JSP
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Text.ParserCombinators.Parsec as P
import Data.Either.Combinators (mapLeft)
import Data.Either.Extra (fromRight')
import Text.ParserCombinators.Parsec ((<?>))
import Text.Read (read)
import Protolude
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
type JSPath = [JSPathExp]
-- | full jspath, e.g. "$.property[0].attr.detail[?(@ == "role1")]"
newtype JSPath = JSPath JSP.Query
-- NOTE: We only accept one JSPFilter expr (at the end of input)
-- | jspath expression
data JSPathExp
= JSPKey Text -- .property or ."property-dash"
| JSPIdx Int -- [0]
| JSPFilter FilterExp -- [?(@ == "match")]
-- | Default value for "jwt-role-claim-key" config
defaultRoleJSPathKey :: JSPath
defaultRoleJSPathKey = fromRight' $ P.parse pJSPath "" "$.role"
data FilterExp
= EqualsCond Text
| NotEqualsCond Text
| StartsWithCond Text
| EndsWithCond Text
| ContainsCond Text
-- | Dump JSPath
-- e.g. "$.property[0].attr.detail[?(@ == "role1")]"
dumpJSPath :: JSPath -> Text
dumpJSPath (JSPath query) = (escapeDollarChar . escapeDoubleQuotes) jsPathDump
where
jsPathDump = JSP.dumpQuery query
escapeDoubleQuotes = T.replace "\"" "\\\""
-- When dumping, $ must be escaped
escapeDollarChar = T.replace "$" "$$"
dumpJSPath :: JSPathExp -> Text
-- TODO: this needs to be quoted properly for special chars
dumpJSPath (JSPKey k) = "." <> show k
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
where
expr =
case cond of
EqualsCond text -> " == " <> show text
NotEqualsCond text -> " != " <> show text
StartsWithCond text -> " ^== " <> show text
EndsWithCond text -> " ==^ " <> show text
ContainsCond text -> " *== " <> show text
-- | Evaluate JSPath on a JSON
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath (Just (JSON.Array ar)) [JSPFilter jspFilter] = case jspFilter of
EqualsCond txt -> findFirstMatch (==) txt ar
NotEqualsCond txt -> findFirstMatch (/=) txt ar
StartsWithCond txt -> findFirstMatch T.isPrefixOf txt ar
EndsWithCond txt -> findFirstMatch T.isSuffixOf txt ar
ContainsCond txt -> findFirstMatch T.isInfixOf txt ar
where
findFirstMatch matchWith pattern = find (\case
JSON.String txt -> pattern `matchWith` txt
_ -> False)
walkJSPath _ _ = Nothing
-- |
-- Evaluate JSPath on a JSON
-- The result of JSON Path query is a Vector, we select the first
-- string element as the role.
evaluateJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
evaluateJSPath Nothing _ = Nothing
evaluateJSPath (Just json) (JSPath query) = JSP.queryQQ query json V.!? 0
-- Used for the config value "role-claim-key"
pRoleClaimKey :: Text -> Either Text JSPath
pRoleClaimKey selStr =
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
-- | Parse RFC 9535 JSPath: $.roles[0]
pJSPath :: P.Parser JSPath
pJSPath = P.many1 pJSPathExp <* P.eof
pJSPathExp :: P.Parser JSPathExp
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
pJSPKey :: P.Parser JSPathExp
pJSPKey = do
P.char '.'
val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue
return (JSPKey val) <?> "pJSPKey: JSPath attribute key"
pJSPIdx :: P.Parser JSPathExp
pJSPIdx = do
P.char '['
num <- read <$> P.many1 P.digit
P.char ']'
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
pJSPFilter :: P.Parser JSPathExp
pJSPFilter = do
P.try $ P.string "[?("
condition <- pFilterConditionParser
P.char ')'
P.char ']'
P.eof -- this should be the last jspath expression
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
pFilterConditionParser :: P.Parser FilterExp
pFilterConditionParser = do
P.char '@'
P.spaces
filt <- matchOperator
P.spaces
filt <$> pQuotedValue
where
matchOperator =
P.try (P.string "==^" $> EndsWithCond)
<|> P.try (P.string "==" $> EqualsCond)
<|> P.try (P.string "!=" $> NotEqualsCond)
<|> P.try (P.string "^==" $> StartsWithCond)
<|> P.try (P.string "*==" $> ContainsCond)
pQuotedValue :: P.Parser Text
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
pJSPath = JSPath <$> JSP.pQuery <?> "pJSPath: JSPath root query"
+1
View File
@@ -9,6 +9,7 @@ nix:
- zlib
extra-deps:
- aeson-jsonpath-0.4.2.0
- configurator-pg-0.2.11
- fuzzyset-0.2.4
- hasql-notifications-0.2.4.0
+7
View File
@@ -4,6 +4,13 @@
# https://docs.haskellstack.org/en/stable/topics/lock_files
packages:
- completed:
hackage: aeson-jsonpath-0.4.2.0@sha256:e582474eba0ea4cbaa21cc75355c36f1b767d240fde75cfd929a8feaf223dfa9,4235
pantry-tree:
sha256: c5992b0319ac43f179438fc1965d565b5d0e710a04355639843fb085d44f2f55
size: 2160
original:
hackage: aeson-jsonpath-0.4.2.0
- completed:
hackage: configurator-pg-0.2.11@sha256:de0c56386591e85159436b0af04a8f15a4f4e156354e99709676c2c2ee959505,2850
pantry-tree:
+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