Skip to content

Commit

Permalink
feat: support string comparison for jwt-role-claim-key
Browse files Browse the repository at this point in the history
  • Loading branch information
taimoorzaeem committed Dec 6, 2024
1 parent 2df1676 commit 60d92f6
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 22 deletions.
7 changes: 6 additions & 1 deletion docs/references/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,12 @@ jwt-role-claim-key
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
jwt-role-claim-key = ".postgrest.roles[1]"
# {"https://www.example.com/role": { "key": "author }}
# {"postgrest":{"roles": ["other", "author"]}}
# accepts string comparison operators, supported operators are "==", "!=", "^==", "$==", "*=="
# see: https://github.com/dfilatov/jspath#comparison-operators
jwt-role-claim-key = ".postgrest.roles[?(@ == "author")]"
# {"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"
Expand Down
16 changes: 15 additions & 1 deletion src/PostgREST/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Cache as C
import qualified Data.Scientific as Sci
import qualified Data.Text as T
import qualified Data.Vault.Lazy as Vault
import qualified Data.Vector as V
import qualified Jose.Jwk as JWT
Expand All @@ -46,7 +47,8 @@ import System.TimeIt (timeItT)

import PostgREST.AppState (AppState, AuthResult (..), getConfig,
getJwtCache, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
JSPathExp (..))
import PostgREST.Error (Error (..))

import Protolude
Expand Down Expand Up @@ -121,8 +123,20 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
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 (EqualsCond txt)] = findFirstMatch (==) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
walkJSPath _ _ = Nothing

findFirstMatch matchWith pattern = foldr checkMatch Nothing
where
checkMatch (JSON.String txt) acc
| pattern `matchWith` txt = Just $ JSON.String txt
| otherwise = acc
checkMatch _ acc = acc

Check warning on line 138 in src/PostgREST/Auth.hs

View check run for this annotation

Codecov / codecov/patch

src/PostgREST/Auth.hs#L138

Added line #L138 was not covered by tests

unquoted :: JSON.Value -> BS.ByteString
unquoted (JSON.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v
Expand Down
6 changes: 4 additions & 2 deletions src/PostgREST/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module PostgREST.Config
, Environment
, JSPath
, JSPathExp(..)
, FilterExp(..)
, LogLevel(..)
, OpenAPIMode(..)
, Proxy(..)
Expand Down Expand Up @@ -54,8 +55,9 @@ import System.Posix.Types (FileMode)

import PostgREST.Config.Database (RoleIsolationLvl,
RoleSettings)
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
dumpJSPath, pRoleClaimKey)
import PostgREST.Config.JSPath (FilterExp (..), JSPath,
JSPathExp (..), dumpJSPath,
pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
Expand Down
83 changes: 65 additions & 18 deletions src/PostgREST/Config/JSPath.hs
Original file line number Diff line number Diff line change
@@ -1,51 +1,98 @@
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
module PostgREST.Config.JSPath
( JSPath
, JSPathExp(..)
, FilterExp(..)
, dumpJSPath
, pRoleClaimKey
) where

import qualified Text.ParserCombinators.Parsec as P

import Data.Either.Combinators (mapLeft)
import Text.ParserCombinators.Parsec ((<?>))
import Text.Read (read)
import Data.Either.Combinators (mapLeft)
import Text.Read (read)

import Protolude


-- | full jspath, e.g. .property[0].attr.detail
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
type JSPath = [JSPathExp]

-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
-- | jspath expression
data JSPathExp
= JSPKey Text
| JSPIdx Int
= JSPKey Text -- .property or ."property-dash"
| JSPIdx Int -- [0]
| JSPFilter FilterExp -- [?(@ == "match")], [?(@ ^== "match-prefix")], etc

data FilterExp
= EqualsCond Text
| NotEqualsCond Text
| StartsWithCond Text
| EndsWithCond Text
| ContainsCond Text

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 <> "]"

Check warning on line 38 in src/PostgREST/Config/JSPath.hs

View check run for this annotation

Codecov / codecov/patch

src/PostgREST/Config/JSPath.hs#L38

Added line #L38 was not covered by tests
where
expr =
case cond of
EqualsCond text -> " == " <> text
NotEqualsCond text -> " != " <> text
StartsWithCond text -> " ^== " <> text
EndsWithCond text -> " $== " <> text
ContainsCond text -> " *== " <> text

Check warning on line 46 in src/PostgREST/Config/JSPath.hs

View check run for this annotation

Codecov / codecov/patch

src/PostgREST/Config/JSPath.hs#L40-L46

Added lines #L40 - L46 were not covered by tests


-- 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)

pJSPath :: P.Parser JSPath
pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
where
toJSPath :: [(Text, Maybe Int)] -> JSPath
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
period = P.char '.' <?> "period (.)"
pPath :: P.Parser (Text, Maybe Int)
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
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

pJSPIdx :: P.Parser JSPathExp
pJSPIdx = do
P.char '['
num <- read <$> P.many1 P.digit
P.char ']'
return $ JSPIdx num

pJSPKey :: P.Parser Text
pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
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

pJSPIdx :: P.Parser Int
pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
pFilterConditionParser :: P.Parser FilterExp
pFilterConditionParser = do
P.char '@'
P.spaces
condOp <- P.choice $ map P.string ["==", "!=", "^==", "$==", "*=="]
P.spaces
value <- pQuotedValue
return $ case condOp of
"==" -> EqualsCond value
"!=" -> NotEqualsCond value
"^==" -> StartsWithCond value
"$==" -> EndsWithCond value
"*==" -> ContainsCond value
_ -> EqualsCond value -- Impossible case

Check warning on line 95 in src/PostgREST/Config/JSPath.hs

View check run for this annotation

Codecov / codecov/patch

src/PostgREST/Config/JSPath.hs#L95

Added line #L95 was not covered by tests

pQuotedValue :: P.Parser Text
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
35 changes: 35 additions & 0 deletions test/io/fixtures.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,41 @@ roleclaims:
role: postgrest_test_author
other: true
expected_status: 401
- key: '.realm_access.roles[?(@ == "postgrest_test_author")]'
data:
realm_access:
roles:
- other
- postgrest_test_author
expected_status: 200
- 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

invalidroleclaimkeys:
- 'role.other'
Expand Down

0 comments on commit 60d92f6

Please sign in to comment.