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:
committed by
Wolfgang Walther
parent
a0bb87d693
commit
bb63c3fade
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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\")]"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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-'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user