From bb63c3faded803a370ea1475b00e2830171df81b Mon Sep 17 00:00:00 2001 From: Taimoor Zaeem Date: Fri, 26 Jun 2026 15:24:33 +0500 Subject: [PATCH] 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 --- CHANGELOG.md | 13 ++ docs/postgrest.dict | 2 - docs/references/auth.rst | 31 ++--- docs/references/configuration.rst | 6 +- nix/overlays/haskell-packages.nix | 10 ++ postgrest.cabal | 1 + src/PostgREST/Auth/Jwt.hs | 4 +- src/PostgREST/Config.hs | 13 +- src/PostgREST/Config/JSPath.hs | 131 +++++------------- stack.yaml | 1 + stack.yaml.lock | 7 + test/io/configs/aliases.config | 2 +- test/io/configs/expected/aliases.config | 2 +- .../configs/expected/boolean-numeric.config | 2 +- .../io/configs/expected/boolean-string.config | 2 +- test/io/configs/expected/defaults.config | 2 +- .../expected/jspath-str-op-dump1.config | 44 ------ .../expected/jspath-str-op-dump2.config | 44 ------ .../expected/jspath-str-op-dump3.config | 44 ------ .../expected/jspath-str-op-dump4.config | 44 ------ .../expected/jspath-str-op-dump5.config | 44 ------ ...efaults-with-db-other-authenticator.config | 2 +- .../expected/no-defaults-with-db.config | 2 +- test/io/configs/expected/no-defaults.config | 2 +- test/io/configs/expected/types.config | 2 +- test/io/configs/expected/utf-8.config | 2 +- test/io/configs/jspath-str-op-dump1.config | 2 - test/io/configs/jspath-str-op-dump2.config | 1 - test/io/configs/jspath-str-op-dump3.config | 1 - test/io/configs/jspath-str-op-dump4.config | 1 - test/io/configs/jspath-str-op-dump5.config | 1 - test/io/configs/no-defaults-env.yaml | 2 +- test/io/configs/no-defaults.config | 2 +- test/io/fixtures/db_config.sql | 4 +- test/io/fixtures/fixtures.yaml | 58 ++------ test/observability/ObsHelper.hs | 4 +- test/spec/SpecHelper.hs | 7 +- 37 files changed, 123 insertions(+), 419 deletions(-) delete mode 100644 test/io/configs/expected/jspath-str-op-dump1.config delete mode 100644 test/io/configs/expected/jspath-str-op-dump2.config delete mode 100644 test/io/configs/expected/jspath-str-op-dump3.config delete mode 100644 test/io/configs/expected/jspath-str-op-dump4.config delete mode 100644 test/io/configs/expected/jspath-str-op-dump5.config delete mode 100644 test/io/configs/jspath-str-op-dump1.config delete mode 100644 test/io/configs/jspath-str-op-dump2.config delete mode 100644 test/io/configs/jspath-str-op-dump3.config delete mode 100644 test/io/configs/jspath-str-op-dump4.config delete mode 100644 test/io/configs/jspath-str-op-dump5.config diff --git a/CHANGELOG.md b/CHANGELOG.md index a60d62e21..06a8fae52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/postgrest.dict b/docs/postgrest.dict index 324d51b88..5941b782b 100644 --- a/docs/postgrest.dict +++ b/docs/postgrest.dict @@ -30,7 +30,6 @@ CSV durations DDL DOM -DSL DevOps Dramatiq dockerize @@ -76,7 +75,6 @@ isdistinct JS js JSON -JSPath JWK JWT jwt diff --git a/docs/references/auth.rst b/docs/references/auth.rst index 4970dd39a..bd80122b3 100644 --- a/docs/references/auth.rst +++ b/docs/references/auth.rst @@ -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 `_) 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 `_ 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 `_. + - 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 `_ is available for filtering. JWT Security ------------ diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 3ac9e731a..3f7c97831 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -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 diff --git a/nix/overlays/haskell-packages.nix b/nix/overlays/haskell-packages.nix index 288a7d48c..85b6cf127 100644 --- a/nix/overlays/haskell-packages.nix +++ b/nix/overlays/haskell-packages.nix @@ -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 { diff --git a/postgrest.cabal b/postgrest.cabal index 507697316..0a3d38d94 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -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 diff --git a/src/PostgREST/Auth/Jwt.hs b/src/PostgREST/Auth/Jwt.hs index e63664e04..bc02e0a25 100644 --- a/src/PostgREST/Auth/Jwt.hs +++ b/src/PostgREST/Auth/Jwt.hs @@ -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 diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 3832088e1..914128ab8 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -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 = diff --git a/src/PostgREST/Config/JSPath.hs b/src/PostgREST/Config/JSPath.hs index e28e5c366..2b3eeafcb 100644 --- a/src/PostgREST/Config/JSPath.hs +++ b/src/PostgREST/Config/JSPath.hs @@ -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" diff --git a/stack.yaml b/stack.yaml index 1ea29d7d6..a3aa95bba 100644 --- a/stack.yaml +++ b/stack.yaml @@ -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 diff --git a/stack.yaml.lock b/stack.yaml.lock index 28f7faca7..f4bc9a5cd 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -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: diff --git a/test/io/configs/aliases.config b/test/io/configs/aliases.config index cd66cc299..a83c3bac2 100644 --- a/test/io/configs/aliases.config +++ b/test/io/configs/aliases.config @@ -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 diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index baddfc84c..ae27520ff 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -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" diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index a2dd532e8..2f0332720 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -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" diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index a2dd532e8..2f0332720 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -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" diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index ac1720ac7..da3a95ef7 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -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" diff --git a/test/io/configs/expected/jspath-str-op-dump1.config b/test/io/configs/expected/jspath-str-op-dump1.config deleted file mode 100644 index ea15fdd5a..000000000 --- a/test/io/configs/expected/jspath-str-op-dump1.config +++ /dev/null @@ -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" diff --git a/test/io/configs/expected/jspath-str-op-dump2.config b/test/io/configs/expected/jspath-str-op-dump2.config deleted file mode 100644 index 3309d9f71..000000000 --- a/test/io/configs/expected/jspath-str-op-dump2.config +++ /dev/null @@ -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" diff --git a/test/io/configs/expected/jspath-str-op-dump3.config b/test/io/configs/expected/jspath-str-op-dump3.config deleted file mode 100644 index 207dea177..000000000 --- a/test/io/configs/expected/jspath-str-op-dump3.config +++ /dev/null @@ -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" diff --git a/test/io/configs/expected/jspath-str-op-dump4.config b/test/io/configs/expected/jspath-str-op-dump4.config deleted file mode 100644 index 74e342984..000000000 --- a/test/io/configs/expected/jspath-str-op-dump4.config +++ /dev/null @@ -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" diff --git a/test/io/configs/expected/jspath-str-op-dump5.config b/test/io/configs/expected/jspath-str-op-dump5.config deleted file mode 100644 index 52a741eb7..000000000 --- a/test/io/configs/expected/jspath-str-op-dump5.config +++ /dev/null @@ -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" diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index ddbf5acb3..a1727d662 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -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" diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 7c51fa999..45c99b4e1 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -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" diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index a3a611b12..a75fb5d52 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -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" diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index 855b3db8d..5ce9cde22 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -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" diff --git a/test/io/configs/expected/utf-8.config b/test/io/configs/expected/utf-8.config index de80d6441..99c4b65f4 100644 --- a/test/io/configs/expected/utf-8.config +++ b/test/io/configs/expected/utf-8.config @@ -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" diff --git a/test/io/configs/jspath-str-op-dump1.config b/test/io/configs/jspath-str-op-dump1.config deleted file mode 100644 index 770d8c7d8..000000000 --- a/test/io/configs/jspath-str-op-dump1.config +++ /dev/null @@ -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\")]" diff --git a/test/io/configs/jspath-str-op-dump2.config b/test/io/configs/jspath-str-op-dump2.config deleted file mode 100644 index d92b49fe4..000000000 --- a/test/io/configs/jspath-str-op-dump2.config +++ /dev/null @@ -1 +0,0 @@ -jwt-role-claim-key = ".roles[?(@ != \"role1\")]" diff --git a/test/io/configs/jspath-str-op-dump3.config b/test/io/configs/jspath-str-op-dump3.config deleted file mode 100644 index d55e8376f..000000000 --- a/test/io/configs/jspath-str-op-dump3.config +++ /dev/null @@ -1 +0,0 @@ -jwt-role-claim-key = ".roles[?(@ ^== \"role1\")]" diff --git a/test/io/configs/jspath-str-op-dump4.config b/test/io/configs/jspath-str-op-dump4.config deleted file mode 100644 index 52c4ae3e1..000000000 --- a/test/io/configs/jspath-str-op-dump4.config +++ /dev/null @@ -1 +0,0 @@ -jwt-role-claim-key = ".roles[?(@ ==^ \"role1\")]" diff --git a/test/io/configs/jspath-str-op-dump5.config b/test/io/configs/jspath-str-op-dump5.config deleted file mode 100644 index 16838837c..000000000 --- a/test/io/configs/jspath-str-op-dump5.config +++ /dev/null @@ -1 +0,0 @@ -jwt-role-claim-key = ".roles[?(@ *== \"role1\")]" diff --git a/test/io/configs/no-defaults-env.yaml b/test/io/configs/no-defaults-env.yaml index 68a1d602d..3f64cf464 100644 --- a/test/io/configs/no-defaults-env.yaml +++ b/test/io/configs/no-defaults-env.yaml @@ -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 diff --git a/test/io/configs/no-defaults.config b/test/io/configs/no-defaults.config index 4553776f9..96e1ad814 100644 --- a/test/io/configs/no-defaults.config +++ b/test/io/configs/no-defaults.config @@ -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 diff --git a/test/io/fixtures/db_config.sql b/test/io/fixtures/db_config.sql index 2405fb2d1..1a17280ee 100644 --- a/test/io/fixtures/db_config.sql +++ b/test/io/fixtures/db_config.sql @@ -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); diff --git a/test/io/fixtures/fixtures.yaml b/test/io/fixtures/fixtures.yaml index 640a584b4..83b092122 100644 --- a/test/io/fixtures/fixtures.yaml +++ b/test/io/fixtures/fixtures.yaml @@ -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-' diff --git a/test/observability/ObsHelper.hs b/test/observability/ObsHelper.hs index 508060bf1..24e079774 100644 --- a/test/observability/ObsHelper.hs +++ b/test/observability/ObsHelper.hs @@ -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 diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index d663f89a5..d869e5af3 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -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