mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
accept a new argument claims_namespace_path
in JWT config (#4365)
* add new optional field `claims_namespace_path` in JWT config * return value when empty array is found in executeJSONPath * update the docs related to claims_namespace_path * improve encodeJSONPath, add property tests for parseJSONPath * throw error if both claims_namespace_path and claims_namespace are set * refactor the Data.Parser.JsonPath to Data.Parser.JSONPathSpec * update the JWT docs Co-Authored-By: Marion Schleifer <marion@hasura.io> Co-authored-by: Marion Schleifer <marion@hasura.io> Co-authored-by: rakeshkky <12475069+rakeshkky@users.noreply.github.com> Co-authored-by: Tirumarai Selvan <tirumarai.selvan@gmail.com>
This commit is contained in:
parent
2aa971ae83
commit
a26bc80496
@ -318,6 +318,20 @@ kill_hge_servers
|
||||
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
##########
|
||||
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET AND JWT (with claims_namespace_path) #####################################>\n"
|
||||
TEST_TYPE="jwt-with-claims-namespace-path"
|
||||
|
||||
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasuraClaims"}')"
|
||||
|
||||
run_hge_with_args serve
|
||||
wait_for_port 8080
|
||||
|
||||
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-jwt-key-file="$OUTPUT_FOLDER/ssl/jwt_private.key" --hge-jwt-conf="$HASURA_GRAPHQL_JWT_SECRET" test_jwt.py
|
||||
|
||||
kill_hge_servers
|
||||
|
||||
unset HASURA_GRAPHQL_JWT_SECRET
|
||||
|
||||
# test with CORS modes
|
||||
|
||||
|
@ -32,6 +32,7 @@ The order, collapsed state of columns and page size is now persisted across page
|
||||
- server: fix an edge case where some events wouldn't be processed because of internal erorrs (#4213)
|
||||
- server: fix downgrade not working to version v1.1.1 (#4354)
|
||||
- server: `type` field is not required if `jwk_url` is provided in JWT config
|
||||
- server: add a new field `claims_namespace_path` which accepts a JSON Path for looking up hasura claim in the JWT token (#4349)
|
||||
|
||||
## `v1.2.0-beta.3`
|
||||
|
||||
|
@ -92,7 +92,8 @@ etc.) JWT claims, as well as Hasura specific claims inside a custom namespace
|
||||
(or key) i.e. ``https://hasura.io/jwt/claims``.
|
||||
|
||||
The ``https://hasura.io/jwt/claims`` is the custom namespace where all Hasura
|
||||
specific claims have to be present. This value can be configured in the JWT
|
||||
specific claims have to be present. This value can be configured using
|
||||
``claims_namespace`` or ``claims_namespace_path`` in the JWT
|
||||
config while starting the server.
|
||||
|
||||
**Note**: ``x-hasura-default-role`` and ``x-hasura-allowed-roles`` are
|
||||
@ -129,6 +130,7 @@ JSON object:
|
||||
"key": "<optional-key-as-string>",
|
||||
"jwk_url": "<optional-url-to-refresh-jwks>",
|
||||
"claims_namespace": "<optional-key-name-in-claims>",
|
||||
"claims_namespace_path":"<optional-json-path-to-the-claims>",
|
||||
"claims_format": "json|stringified_json",
|
||||
"audience": <optional-string-or-list-of-strings-to-verify-audience>,
|
||||
"issuer": "<optional-string-to-verify-issuer>"
|
||||
@ -220,6 +222,38 @@ inside which the Hasura specific claims will be present, e.g. ``https://mydomain
|
||||
|
||||
**Default value** is: ``https://hasura.io/jwt/claims``.
|
||||
|
||||
``claims_namespace_path``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
An optional JSON path value to the Hasura claims in the JWT token.
|
||||
|
||||
Example values are ``$.hasura.claims`` or ``$`` (i.e. root of the payload)
|
||||
|
||||
The JWT token should be in this format if the ``claims_namespace_path`` is
|
||||
set to ``$.hasura.claims``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"sub": "1234567890",
|
||||
"name": "John Doe",
|
||||
"admin": true,
|
||||
"iat": 1516239022,
|
||||
"hasura": {
|
||||
"claims": {
|
||||
"x-hasura-allowed-roles": ["editor","user", "mod"],
|
||||
"x-hasura-default-role": "user",
|
||||
"x-hasura-user-id": "1234567890",
|
||||
"x-hasura-org-id": "123",
|
||||
"x-hasura-custom": "custom-value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
The JWT config can only have one of ``claims_namespace`` or ``claims_namespace_path``
|
||||
values set. If neither keys are set, then the default value of
|
||||
``claims_namespace`` i.e. https://hasura.io/jwt/claims will be used.
|
||||
|
||||
``claims_format``
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
@ -231,6 +231,7 @@ library
|
||||
, Hasura.Server.PGDump
|
||||
-- Exposed for testing:
|
||||
, Hasura.Server.Telemetry.Counters
|
||||
, Data.Parser.JSONPath
|
||||
|
||||
, Hasura.RQL.Types
|
||||
, Hasura.RQL.Types.Run
|
||||
@ -374,7 +375,6 @@ library
|
||||
, Data.List.Extended
|
||||
, Data.HashMap.Strict.Extended
|
||||
, Data.HashMap.Strict.InsOrd.Extended
|
||||
, Data.Parser.JSONPath
|
||||
, Data.Sequence.NonEmpty
|
||||
, Data.TByteString
|
||||
, Data.Text.Extended
|
||||
@ -426,11 +426,13 @@ test-suite graphql-engine-tests
|
||||
, time
|
||||
, transformers-base
|
||||
, unordered-containers
|
||||
, text
|
||||
hs-source-dirs: src-test
|
||||
main-is: Main.hs
|
||||
other-modules:
|
||||
Data.Parser.CacheControlSpec
|
||||
Data.Parser.URLTemplate
|
||||
Data.Parser.JSONPathSpec
|
||||
Data.TimeSpec
|
||||
Hasura.IncrementalSpec
|
||||
Hasura.RQL.MetadataSpec
|
||||
|
@ -8,50 +8,45 @@ import Control.Applicative ((<|>))
|
||||
import Data.Aeson.Internal (JSONPath, JSONPathElement (..))
|
||||
import Data.Attoparsec.Text
|
||||
import Data.Bool (bool)
|
||||
import Data.Char (isDigit)
|
||||
import qualified Data.Text as T
|
||||
import Prelude hiding (takeWhile)
|
||||
import Text.Read (readMaybe)
|
||||
import qualified Data.Text as T
|
||||
|
||||
parseKey :: Parser T.Text
|
||||
parseKey = do
|
||||
firstChar <- letter
|
||||
<?> "the first character of property name must be a letter."
|
||||
name <- many' (letter
|
||||
<|> digit
|
||||
<|> satisfy (`elem` ("-_" :: String))
|
||||
)
|
||||
return $ T.pack (firstChar:name)
|
||||
parseSimpleKeyText :: Parser T.Text
|
||||
parseSimpleKeyText = takeWhile1 (inClass "a-zA-Z0-9_-")
|
||||
|
||||
parseIndex :: Parser Int
|
||||
parseIndex = skip (== '[') *> anyChar >>= parseDigits
|
||||
where
|
||||
parseDigits :: Char -> Parser Int
|
||||
parseDigits firstDigit
|
||||
| firstDigit == ']' = fail "empty array index"
|
||||
| not $ isDigit firstDigit =
|
||||
fail $ "invalid array index: " ++ [firstDigit]
|
||||
| otherwise = do
|
||||
remain <- many' (notChar ']')
|
||||
skip (== ']')
|
||||
let content = firstDigit:remain
|
||||
case (readMaybe content :: Maybe Int) of
|
||||
Nothing -> fail $ "invalid array index: " ++ content
|
||||
Just v -> return v
|
||||
parseKey :: Parser JSONPathElement
|
||||
parseKey = Key <$>
|
||||
( (char '.' *> parseSimpleKeyText) -- Parse `.key`
|
||||
<|> T.pack <$> ((string ".['" <|> string "['") *> manyTill anyChar (string "']")) -- Parse `['key']` or `.['key']`
|
||||
<|> fail "invalid key element"
|
||||
)
|
||||
|
||||
parseElement :: Parser JSONPathElement
|
||||
parseElement = do
|
||||
dotLen <- T.length <$> takeWhile (== '.')
|
||||
if dotLen > 1
|
||||
then fail "multiple dots in json path"
|
||||
else peekChar >>= \case
|
||||
Nothing -> fail "empty json path"
|
||||
Just '[' -> Index <$> parseIndex
|
||||
_ -> Key <$> parseKey
|
||||
parseIndex :: Parser JSONPathElement
|
||||
parseIndex = Index <$>
|
||||
( ((char '[' *> manyTill anyChar (char ']')) >>= maybe (fail "invalid array index") pure . readMaybe) -- Parse `[Int]`
|
||||
<|> fail "invalid index element"
|
||||
)
|
||||
|
||||
parseElements :: Parser JSONPath
|
||||
parseElements = skipWhile (== '$') *> many1 parseElement
|
||||
parseElements = skipWhile (== '$') *> parseRemaining
|
||||
where
|
||||
parseFirstKey = Key <$> parseSimpleKeyText
|
||||
parseElements' = many1 (parseIndex <|> parseKey)
|
||||
parseRemaining = do
|
||||
maybeFirstChar <- peekChar
|
||||
case maybeFirstChar of
|
||||
Nothing -> pure []
|
||||
Just firstChar ->
|
||||
-- If first char is not any of '.' and '[', then parse first key
|
||||
-- Eg:- Parse "key1.key2[0]"
|
||||
if firstChar `notElem` (".[" :: String) then do
|
||||
firstKey <- parseFirstKey
|
||||
remainingElements <- parseElements'
|
||||
pure $ firstKey:remainingElements
|
||||
else parseElements'
|
||||
|
||||
-- | Parse jsonpath String value
|
||||
parseJSONPath :: T.Text -> Either String JSONPath
|
||||
parseJSONPath = parseResult . parse parseElements
|
||||
where
|
||||
@ -64,6 +59,6 @@ parseJSONPath = parseResult . parse parseElements
|
||||
Left $ invalidMessage remain
|
||||
else
|
||||
Right r
|
||||
|
||||
invalidMessage s = "invalid property name: " ++ T.unpack s
|
||||
++ ". Accept letters, digits, underscore (_) or hyphen (-) only"
|
||||
++ ". Use single quotes enclosed in bracket if there are any special characters"
|
||||
|
@ -82,12 +82,12 @@ renderURLTemplate template = do
|
||||
|
||||
-- QuickCheck generators
|
||||
instance Arbitrary Variable where
|
||||
arbitrary = Variable . T.pack <$> listOf1 (elements $ alphaNumerics <> "-_")
|
||||
arbitrary = Variable . T.pack <$> listOf1 (elements $ alphaNumerics <> " -_")
|
||||
|
||||
instance Arbitrary URLTemplate where
|
||||
arbitrary = URLTemplate <$> listOf (oneof [genText, genVariable])
|
||||
where
|
||||
genText = (TIText . T.pack) <$> listOf1 (elements $ alphaNumerics <> "://")
|
||||
genText = (TIText . T.pack) <$> listOf1 (elements $ alphaNumerics <> " ://")
|
||||
genVariable = TIVariable <$> arbitrary
|
||||
|
||||
genURLTemplate :: Gen URLTemplate
|
||||
|
@ -72,7 +72,7 @@ import qualified GHC.Clock as Clock
|
||||
import qualified Test.QuickCheck as QC
|
||||
|
||||
alphaNumerics :: String
|
||||
alphaNumerics = ['a'..'z'] ++ ['A'..'Z'] ++ "0123456789 "
|
||||
alphaNumerics = ['a'..'z'] ++ ['A'..'Z'] ++ "0123456789"
|
||||
|
||||
instance Arbitrary Text where
|
||||
arbitrary = T.pack <$> QC.listOf (QC.elements alphaNumerics)
|
||||
|
@ -5,6 +5,7 @@ module Hasura.RQL.Types.Error
|
||||
, QErr(..)
|
||||
, encodeQErr
|
||||
, encodeGQLErr
|
||||
, encodeJSONPath
|
||||
, noInternalQErrEnc
|
||||
, err400
|
||||
, err404
|
||||
@ -197,8 +198,10 @@ encodeJSONPath = format "$"
|
||||
format pfx (Key key:parts) = format (pfx ++ "." ++ formatKey key) parts
|
||||
|
||||
formatKey key
|
||||
| T.any (=='.') key = "['" ++ T.unpack key ++ "']"
|
||||
| otherwise = T.unpack key
|
||||
| T.any specialChar key = "['" ++ T.unpack key ++ "']"
|
||||
| otherwise = T.unpack key
|
||||
where
|
||||
specialChar = flip notElem (alphaNumerics ++ "_-")
|
||||
|
||||
instance Q.FromPGConnErr QErr where
|
||||
fromPGConnErr c =
|
||||
|
@ -6,6 +6,7 @@ module Hasura.Server.Auth.JWT
|
||||
, Jose.JWKSet (..)
|
||||
, JWTClaimsFormat (..)
|
||||
, JwkFetchError (..)
|
||||
, JWTConfigClaims (..)
|
||||
, updateJwkRef
|
||||
, jwkRefreshCtrl
|
||||
, defaultClaimNs
|
||||
@ -22,6 +23,7 @@ import Data.Time.Clock (NominalDiffTime, UTCTime, diff
|
||||
import GHC.AssertNF
|
||||
import Network.URI (URI)
|
||||
|
||||
import Data.Aeson.Internal (JSONPath)
|
||||
import Data.Parser.CacheControl
|
||||
import Data.Parser.Expires
|
||||
import Hasura.HTTP
|
||||
@ -30,14 +32,16 @@ import Hasura.Prelude
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Auth.JWT.Internal (parseHmacKey, parseRsaKey)
|
||||
import Hasura.Server.Auth.JWT.Logging
|
||||
import Hasura.Server.Utils (getRequestHeader, userRoleHeader)
|
||||
import Hasura.Server.Utils (getRequestHeader, userRoleHeader, executeJSONPath)
|
||||
import Hasura.Server.Version (HasVersion)
|
||||
import Hasura.RQL.Types.Error (encodeJSONPath)
|
||||
|
||||
import qualified Control.Concurrent.Extended as C
|
||||
import qualified Crypto.JWT as Jose
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.Casing as J
|
||||
import qualified Data.Aeson.TH as J
|
||||
import qualified Data.Aeson.Internal as J
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BLC
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
@ -47,7 +51,7 @@ import qualified Data.Text.Encoding as T
|
||||
import qualified Network.HTTP.Client as HTTP
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wreq as Wreq
|
||||
|
||||
import qualified Data.Parser.JSONPath as JSONPath
|
||||
|
||||
newtype RawJWT = RawJWT BL.ByteString
|
||||
|
||||
@ -59,10 +63,19 @@ data JWTClaimsFormat
|
||||
$(J.deriveJSON J.defaultOptions { J.sumEncoding = J.ObjectWithSingleField
|
||||
, J.constructorTagModifier = J.snakeCase . drop 3 } ''JWTClaimsFormat)
|
||||
|
||||
data JWTConfigClaims
|
||||
= ClaimNsPath JSONPath
|
||||
| ClaimNs T.Text
|
||||
deriving (Show, Eq)
|
||||
|
||||
instance J.ToJSON JWTConfigClaims where
|
||||
toJSON (ClaimNsPath nsPath) = J.String . T.pack $ encodeJSONPath nsPath
|
||||
toJSON (ClaimNs ns) = J.String ns
|
||||
|
||||
data JWTConfig
|
||||
= JWTConfig
|
||||
{ jcKeyOrUrl :: !(Either Jose.JWK URI)
|
||||
, jcClaimNs :: !(Maybe T.Text)
|
||||
, jcClaimNs :: !JWTConfigClaims
|
||||
, jcAudience :: !(Maybe Jose.Audience)
|
||||
, jcClaimsFormat :: !(Maybe JWTClaimsFormat)
|
||||
, jcIssuer :: !(Maybe Jose.StringOrURI)
|
||||
@ -71,7 +84,7 @@ data JWTConfig
|
||||
data JWTCtx
|
||||
= JWTCtx
|
||||
{ jcxKey :: !(IORef Jose.JWKSet)
|
||||
, jcxClaimNs :: !(Maybe T.Text)
|
||||
, jcxClaimNs :: !JWTConfigClaims
|
||||
, jcxAudience :: !(Maybe Jose.Audience)
|
||||
, jcxClaimsFormat :: !JWTClaimsFormat
|
||||
, jcxIssuer :: !(Maybe Jose.StringOrURI)
|
||||
@ -79,7 +92,7 @@ data JWTCtx
|
||||
|
||||
instance Show JWTCtx where
|
||||
show (JWTCtx _ nsM audM cf iss) =
|
||||
show ["<IORef JWKSet>", show nsM, show audM, show cf, show iss]
|
||||
show ["<IORef JWKSet>", show nsM,show audM, show cf, show iss]
|
||||
|
||||
data HasuraClaims
|
||||
= HasuraClaims
|
||||
@ -231,12 +244,15 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
-- verify the JWT
|
||||
claims <- liftJWTError invalidJWTError $ verifyJwt jwtCtx $ RawJWT jwt
|
||||
|
||||
let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx
|
||||
claimsFmt = jcxClaimsFormat jwtCtx
|
||||
let claimsFmt = jcxClaimsFormat jwtCtx
|
||||
expTimeM = fmap (\(Jose.NumericDate t) -> t) $ claims ^. Jose.claimExp
|
||||
|
||||
-- see if the hasura claims key exist in the claims map
|
||||
let mHasuraClaims = Map.lookup claimsNs $ claims ^. Jose.unregisteredClaims
|
||||
-- see if the hasura claims key exists in the claims map
|
||||
let mHasuraClaims =
|
||||
case jcxClaimNs jwtCtx of
|
||||
ClaimNs k -> Map.lookup k $ claims ^. Jose.unregisteredClaims
|
||||
ClaimNsPath path -> parseIValueJsonValue $ executeJSONPath path (J.toJSON $ claims ^. Jose.unregisteredClaims)
|
||||
|
||||
hasuraClaimsV <- maybe claimsNotFound return mHasuraClaims
|
||||
|
||||
-- get hasura claims value as an object. parse from string possibly
|
||||
@ -277,12 +293,22 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
(JCFJson, _) ->
|
||||
claimsErr "expecting a json object when claims_format is json"
|
||||
|
||||
strngfyErr v = "expecting stringified json at: '"
|
||||
<> fromMaybe defaultClaimNs (jcxClaimNs jwtCtx)
|
||||
<> "', but found: " <> v
|
||||
strngfyErr v =
|
||||
"expecting stringified json at: '"
|
||||
<> claimsLocation
|
||||
<> "', but found: " <> v
|
||||
where
|
||||
claimsLocation :: Text
|
||||
claimsLocation =
|
||||
case jcxClaimNs jwtCtx of
|
||||
ClaimNsPath path -> T.pack $ "claims_namespace_path " <> encodeJSONPath path
|
||||
ClaimNs ns -> "claims_namespace " <> ns
|
||||
|
||||
claimsErr = throw400 JWTInvalidClaims
|
||||
|
||||
parseIValueJsonValue (J.IError _ _) = Nothing
|
||||
parseIValueJsonValue (J.ISuccess v) = Just v
|
||||
|
||||
-- see if there is a x-hasura-role header, or else pick the default role
|
||||
getCurrentRole defaultRole =
|
||||
let mUserRole = getRequestHeader userRoleHeader headers
|
||||
@ -305,8 +331,10 @@ processAuthZHeader jwtCtx headers authzHeader = do
|
||||
currRoleNotAllowed =
|
||||
throw400 AccessDenied "Your current role is not in allowed roles"
|
||||
claimsNotFound = do
|
||||
let claimsNs = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx
|
||||
throw400 JWTInvalidClaims $ "claims key: '" <> claimsNs <> "' not found"
|
||||
let claimsNsError = case jcxClaimNs jwtCtx of
|
||||
ClaimNsPath path -> T.pack $ "claims not found at claims_namespace_path: '" <> (encodeJSONPath path) <> "'"
|
||||
ClaimNs ns -> "claims key: '" <> ns <> "' not found"
|
||||
throw400 JWTInvalidClaims $ claimsNsError
|
||||
|
||||
|
||||
-- parse x-hasura-allowed-roles, x-hasura-default-role from JWT claims
|
||||
@ -369,14 +397,19 @@ verifyJwt ctx (RawJWT rawJWT) = do
|
||||
|
||||
instance J.ToJSON JWTConfig where
|
||||
toJSON (JWTConfig keyOrUrl claimNs aud claimsFmt iss) =
|
||||
J.object (jwkFields ++ sharedFields)
|
||||
J.object (jwkFields ++ sharedFields ++ claimsNsFields)
|
||||
where
|
||||
jwkFields = case keyOrUrl of
|
||||
Left _ -> [ "type" J..= J.String "<TYPE REDACTED>"
|
||||
, "key" J..= J.String "<JWK REDACTED>" ]
|
||||
Right url -> [ "jwk_url" J..= url ]
|
||||
sharedFields = [ "claims_namespace" J..= claimNs
|
||||
, "claims_format" J..= claimsFmt
|
||||
|
||||
claimsNsFields = case claimNs of
|
||||
ClaimNsPath nsPath ->
|
||||
["claims_namespace_path" J..= (encodeJSONPath nsPath)]
|
||||
ClaimNs ns -> ["claims_namespace" J..= J.String ns]
|
||||
|
||||
sharedFields = [ "claims_format" J..= claimsFmt
|
||||
, "audience" J..= aud
|
||||
, "issuer" J..= iss
|
||||
]
|
||||
@ -388,24 +421,33 @@ instance J.FromJSON JWTConfig where
|
||||
|
||||
parseJSON = J.withObject "JWTConfig" $ \o -> do
|
||||
mRawKey <- o J..:? "key"
|
||||
claimNs <- o J..:? "claims_namespace"
|
||||
claimsNs <- o J..:? "claims_namespace"
|
||||
claimsNsPath <- o J..:? "claims_namespace_path"
|
||||
aud <- o J..:? "audience"
|
||||
iss <- o J..:? "issuer"
|
||||
jwkUrl <- o J..:? "jwk_url"
|
||||
isStrngfd <- o J..:? "claims_format"
|
||||
|
||||
|
||||
hasuraClaimsNs <-
|
||||
case (claimsNsPath,claimsNs) of
|
||||
(Nothing, Nothing) -> return $ ClaimNs defaultClaimNs
|
||||
(Just nsPath, Nothing) -> either failJSONPathParsing (return . ClaimNsPath) . JSONPath.parseJSONPath $ nsPath
|
||||
(Nothing, Just ns) -> return $ ClaimNs ns
|
||||
(Just _, Just _) -> fail "claims_namespace and claims_namespace_path both cannot be set"
|
||||
|
||||
case (mRawKey, jwkUrl) of
|
||||
(Nothing, Nothing) -> fail "key and jwk_url both cannot be empty"
|
||||
(Just _, Just _) -> fail "key, jwk_url both cannot be present"
|
||||
(Just rawKey, Nothing) -> do
|
||||
keyType <- o J..: "type"
|
||||
key <- parseKey rawKey keyType
|
||||
return $ JWTConfig (Left key) claimNs aud isStrngfd iss
|
||||
key <- parseKey keyType rawKey
|
||||
return $ JWTConfig (Left key) hasuraClaimsNs aud isStrngfd iss
|
||||
(Nothing, Just url) ->
|
||||
return $ JWTConfig (Right url) claimNs aud isStrngfd iss
|
||||
return $ JWTConfig (Right url) hasuraClaimsNs aud isStrngfd iss
|
||||
|
||||
where
|
||||
parseKey rawKey keyType =
|
||||
parseKey keyType rawKey =
|
||||
case keyType of
|
||||
"HS256" -> runEither $ parseHmacKey rawKey 256
|
||||
"HS384" -> runEither $ parseHmacKey rawKey 384
|
||||
@ -417,4 +459,7 @@ instance J.FromJSON JWTConfig where
|
||||
_ -> invalidJwk ("Key type: " <> T.unpack keyType <> " is not supported")
|
||||
|
||||
runEither = either (invalidJwk . T.unpack) return
|
||||
|
||||
invalidJwk msg = fail ("Invalid JWK: " <> msg)
|
||||
|
||||
failJSONPathParsing err = fail $ "invalid JSON path claims_namespace_path error: " ++ err
|
||||
|
@ -15,8 +15,8 @@ import qualified Hasura.GraphQL.Execute.LiveQuery.Options as LQ
|
||||
|
||||
data JWTInfo
|
||||
= JWTInfo
|
||||
{ jwtiClaimsNamespace :: !Text
|
||||
, jwtiClaimsFormat :: !JWTClaimsFormat
|
||||
{ jwtiClaimsNamespace :: !JWTConfigClaims
|
||||
, jwtiClaimsFormat :: !JWTClaimsFormat
|
||||
} deriving (Show, Eq)
|
||||
|
||||
$(deriveToJSON (aesonDrop 4 snakeCase) ''JWTInfo)
|
||||
@ -44,7 +44,6 @@ runGetConfig am isAllowListEnabled liveQueryOpts = ServerConfig
|
||||
isAllowListEnabled
|
||||
liveQueryOpts
|
||||
|
||||
|
||||
isAdminSecretSet :: AuthMode -> Bool
|
||||
isAdminSecretSet = \case
|
||||
AMNoAuth -> False
|
||||
@ -62,8 +61,8 @@ isJWTSet = \case
|
||||
|
||||
getJWTInfo :: AuthMode -> Maybe JWTInfo
|
||||
getJWTInfo (AMAdminSecretAndJWT _ jwtCtx _) =
|
||||
Just $ JWTInfo ns format
|
||||
Just $ JWTInfo claimsNs format
|
||||
where
|
||||
ns = fromMaybe defaultClaimNs $ jcxClaimNs jwtCtx
|
||||
claimsNs = jcxClaimNs jwtCtx
|
||||
format = jcxClaimsFormat jwtCtx
|
||||
getJWTInfo _ = Nothing
|
||||
|
@ -11,6 +11,7 @@ import Language.Haskell.TH.Syntax (Lift, Q, TExp)
|
||||
import System.Environment
|
||||
import System.Exit
|
||||
import System.Process
|
||||
import Data.Aeson.Internal
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
@ -27,6 +28,7 @@ import qualified Network.Wreq as Wreq
|
||||
import qualified Text.Regex.TDFA as TDFA
|
||||
import qualified Text.Regex.TDFA.ReadRegex as TDFA
|
||||
import qualified Text.Regex.TDFA.TDFA as TDFA
|
||||
import qualified Data.Vector as V
|
||||
|
||||
import Hasura.RQL.Instances ()
|
||||
|
||||
@ -227,3 +229,16 @@ makeReasonMessage errors showError =
|
||||
[singleError] -> "because " <> showError singleError
|
||||
_ -> "for the following reasons:\n" <> T.unlines
|
||||
(map ((" • " <>) . showError) errors)
|
||||
|
||||
executeJSONPath :: JSONPath -> Value -> IResult Value
|
||||
executeJSONPath jsonPath = iparse (valueParser jsonPath)
|
||||
where
|
||||
valueParser path value = case path of
|
||||
[] -> pure value
|
||||
(pathElement:remaining) -> parseWithPathElement pathElement value >>=
|
||||
((<?> pathElement) . valueParser remaining)
|
||||
where
|
||||
parseWithPathElement = \case
|
||||
Key k -> withObject "Object" (.: k)
|
||||
Index i -> withArray "Array" $
|
||||
maybe (fail "Array index out of range") pure . (V.!? i)
|
||||
|
31
server/src-test/Data/Parser/JSONPathSpec.hs
Normal file
31
server/src-test/Data/Parser/JSONPathSpec.hs
Normal file
@ -0,0 +1,31 @@
|
||||
module Data.Parser.JSONPathSpec (spec) where
|
||||
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types (encodeJSONPath)
|
||||
|
||||
import Data.Parser.JSONPath
|
||||
import Test.Hspec
|
||||
import Test.QuickCheck
|
||||
|
||||
import qualified Data.Text as T
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "parseJSONPath" $
|
||||
it "JSONPath parser" $
|
||||
withMaxSuccess 1000 $
|
||||
forAll(resize 20 generateJSONPath) $ \jsonPath ->
|
||||
let encPath = encodeJSONPath jsonPath
|
||||
parsedJSONPathE = parseJSONPath $ T.pack encPath
|
||||
in case parsedJSONPathE of
|
||||
Left err -> counterexample (err <> ": " <> encPath) False
|
||||
Right parsedJSONPath -> property $ parsedJSONPath == jsonPath
|
||||
|
||||
generateJSONPath :: Gen JSONPath
|
||||
generateJSONPath = map (either id id) <$> listOf1 genPathElementEither
|
||||
where
|
||||
genPathElementEither = do
|
||||
indexLeft <- Left <$> genIndex
|
||||
keyRight <- Right <$> genKey
|
||||
elements [indexLeft, keyRight]
|
||||
genIndex = Index <$> choose (0, 100)
|
||||
genKey = (Key . T.pack) <$> listOf1 (elements $ alphaNumerics ++ ".,!@#$%^&*_-?:;|/\"")
|
@ -26,6 +26,7 @@ import Hasura.Server.Migrate
|
||||
import Hasura.Server.Version
|
||||
|
||||
import qualified Data.Parser.CacheControlSpec as CacheControlParser
|
||||
import qualified Data.Parser.JSONPathSpec as JsonPath
|
||||
import qualified Data.Parser.URLTemplate as URLTemplate
|
||||
import qualified Data.TimeSpec as TimeSpec
|
||||
import qualified Hasura.IncrementalSpec as IncrementalSpec
|
||||
@ -57,6 +58,7 @@ unitSpecs :: Spec
|
||||
unitSpecs = do
|
||||
describe "Data.Parser.CacheControl" CacheControlParser.spec
|
||||
describe "Data.Parser.URLTemplate" URLTemplate.spec
|
||||
describe "Data.Parser.JsonPath" JsonPath.spec
|
||||
describe "Hasura.Incremental" IncrementalSpec.spec
|
||||
-- describe "Hasura.RQL.Metadata" MetadataSpec.spec -- Commenting until optimizing the test in CI
|
||||
describe "Data.Time" TimeSpec.spec
|
||||
|
@ -406,7 +406,7 @@ class HGECtx:
|
||||
def __init__(self, hge_url, pg_url, config):
|
||||
|
||||
self.http = requests.Session()
|
||||
self. hge_key = config.getoption('--hge-key')
|
||||
self.hge_key = config.getoption('--hge-key')
|
||||
self.hge_url = hge_url
|
||||
self.pg_url = pg_url
|
||||
self.hge_webhook = config.getoption('--hge-webhook')
|
||||
@ -417,6 +417,8 @@ class HGECtx:
|
||||
with open(hge_jwt_key_file) as f:
|
||||
self.hge_jwt_key = f.read()
|
||||
self.hge_jwt_conf = config.getoption('--hge-jwt-conf')
|
||||
if self.hge_jwt_conf is not None:
|
||||
self.hge_jwt_conf_dict = json.loads(self.hge_jwt_conf)
|
||||
self.webhook_insecure = config.getoption('--test-webhook-insecure')
|
||||
self.metadata_disabled = config.getoption('--test-metadata-disabled')
|
||||
self.may_skip_test_teardown = False
|
||||
|
@ -4,7 +4,7 @@ status: 400
|
||||
response:
|
||||
code: unexpected-payload
|
||||
error: cannot cast column of type "geography" to type "integer"
|
||||
path: $.args.where.geog_col.$cast
|
||||
path: $.args.where.geog_col.['$cast']
|
||||
query:
|
||||
type: select
|
||||
args:
|
||||
|
@ -5,7 +5,7 @@ headers:
|
||||
X-Hasura-User-Id: '1'
|
||||
status: 400
|
||||
response:
|
||||
path: $.args.$set
|
||||
path: $.args.['$set']
|
||||
error: role "user" does not have permission to update column "id"
|
||||
code: permission-denied
|
||||
query:
|
||||
|
@ -4,15 +4,15 @@ status: 400
|
||||
headers:
|
||||
X-Hasura-Role: user1
|
||||
response:
|
||||
path: "$.args.$set"
|
||||
error: column "city" is not updatable for role "user1"; its value is predefined in
|
||||
permission
|
||||
path: $.args.['$set']
|
||||
error: column "city" is not updatable for role "user1"; its value is predefined
|
||||
in permission
|
||||
code: not-supported
|
||||
query:
|
||||
type: update
|
||||
args:
|
||||
table: resident
|
||||
"$set":
|
||||
$set:
|
||||
city: hobart
|
||||
where:
|
||||
name: clarke
|
||||
|
@ -1,5 +1,6 @@
|
||||
import ruamel.yaml as yaml
|
||||
import re
|
||||
import json
|
||||
|
||||
class TestConfigAPI():
|
||||
|
||||
@ -7,6 +8,8 @@ class TestConfigAPI():
|
||||
admin_secret = hge_ctx.hge_key
|
||||
auth_hook = hge_ctx.hge_webhook
|
||||
jwt_conf = hge_ctx.hge_jwt_conf
|
||||
if jwt_conf is not None:
|
||||
jwt_conf_dict = json.loads(hge_ctx.hge_jwt_conf)
|
||||
|
||||
headers = {}
|
||||
if admin_secret is not None:
|
||||
@ -25,14 +28,18 @@ class TestConfigAPI():
|
||||
assert body['is_jwt_set'] == (jwt_conf is not None)
|
||||
|
||||
if jwt_conf is not None:
|
||||
claims_namespace = "https://hasura.io/jwt/claims"
|
||||
if 'claims_namespace' in jwt_conf:
|
||||
claims_namespace = jwt_conf['claims_namespace']
|
||||
claims_format = "json"
|
||||
if 'claims_format' in jwt_conf:
|
||||
claims_format = jwt_conf['claims_format']
|
||||
assert body['jwt']['claims_namespace'] == claims_namespace
|
||||
assert body['jwt']['claims_format'] == claims_format
|
||||
if 'claims_namespace_path' in jwt_conf_dict:
|
||||
assert body['jwt']['claims_namespace_path'] == jwt_conf_dict['claims_namespace_path']
|
||||
assert body['jwt']['claims_format'] == claims_format
|
||||
else:
|
||||
claims_namespace = "https://hasura.io/jwt/claims"
|
||||
if 'claims_namespace' in jwt_conf_dict:
|
||||
claims_namespace = jwt_conf_dict['claims_namespace']
|
||||
if 'claims_format' in jwt_conf_dict:
|
||||
claims_format = jwt_conf_dict['claims_format']
|
||||
assert body['jwt']['claims_namespace'] == claims_namespace
|
||||
assert body['jwt']['claims_format'] == claims_format
|
||||
else:
|
||||
assert body['jwt'] == None
|
||||
|
||||
|
@ -11,7 +11,7 @@ from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
from validate import check_query
|
||||
from validate import check_query, mk_claims_with_namespace_path
|
||||
from context import PytestConf
|
||||
|
||||
|
||||
@ -42,23 +42,32 @@ def mk_claims(conf, claims):
|
||||
class TestJWTBasic():
|
||||
|
||||
def test_jwt_valid_claims_success(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-allowed-roles': ['user', 'editor'],
|
||||
'x-hasura-default-role': 'user'
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-allowed-roles': ['user', 'editor'],
|
||||
'x-hasura-default-role': 'user'
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['url'] = endpoint
|
||||
self.conf['status'] = 200
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_invalid_role_in_request_header(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-allowed-roles': ['contractor', 'editor'],
|
||||
'x-hasura-default-role': 'contractor'
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['response'] = {
|
||||
@ -75,13 +84,18 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_no_allowed_roles_in_claim(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user'
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['response'] = {
|
||||
@ -98,14 +112,19 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_invalid_allowed_roles_in_claim(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-allowed-roles': 'user',
|
||||
'x-hasura-default-role': 'user'
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['response'] = {
|
||||
@ -122,13 +141,18 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_no_default_role(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['response'] = {
|
||||
@ -145,14 +169,19 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_expired(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
exp = datetime.utcnow() - timedelta(minutes=1)
|
||||
self.claims['exp'] = round(exp.timestamp())
|
||||
|
||||
@ -172,15 +201,19 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_invalid_signature(self, hge_ctx, endpoint):
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
wrong_key = gen_rsa_key()
|
||||
token = jwt.encode(self.claims, wrong_key, algorithm='HS256').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
@ -198,41 +231,49 @@ class TestJWTBasic():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_no_audience_in_conf(self, hge_ctx, endpoint):
|
||||
jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
|
||||
if 'audience' in jwt_conf:
|
||||
pytest.skip('audience present in conf, skipping testing no audience')
|
||||
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['aud'] = 'hasura-test-suite'
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['url'] = endpoint
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_no_issuer_in_conf(self, hge_ctx, endpoint):
|
||||
jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
|
||||
if 'issuer' in jwt_conf:
|
||||
pytest.skip('issuer present in conf, skipping testing no issuer')
|
||||
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['iss'] = 'rubbish-issuer'
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
self.conf['url'] = endpoint
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, setup):
|
||||
@ -240,7 +281,7 @@ class TestJWTBasic():
|
||||
with open(self.dir + '/user_select_query_unpublished_articles.yaml') as c:
|
||||
self.conf = yaml.safe_load(c)
|
||||
curr_time = datetime.utcnow()
|
||||
exp_time = curr_time + timedelta(hours=1)
|
||||
exp_time = curr_time + timedelta(hours=10)
|
||||
self.claims = {
|
||||
'sub': '1234567890',
|
||||
'name': 'John Doe',
|
||||
@ -280,11 +321,16 @@ class TestSubscriptionJwtExpiry(object):
|
||||
'name': 'John Doe',
|
||||
'iat': math.floor(curr_time.timestamp())
|
||||
}
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
exp = curr_time + timedelta(seconds=4)
|
||||
self.claims['exp'] = round(exp.timestamp())
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
@ -307,27 +353,36 @@ class TestJwtAudienceCheck():
|
||||
|
||||
audience = jwt_conf['audience']
|
||||
audience = audience if isinstance(audience, str) else audience[0]
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
self.claims['aud'] = audience
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['aud'] = audience
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_invalid_audience(self, hge_ctx, endpoint):
|
||||
jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
|
||||
if 'audience' not in jwt_conf:
|
||||
pytest.skip('audience not present in conf, skipping testing audience')
|
||||
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['aud'] = 'rubbish_audience'
|
||||
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
@ -346,7 +401,7 @@ class TestJwtAudienceCheck():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, setup):
|
||||
@ -379,27 +434,37 @@ class TestJwtIssuerCheck():
|
||||
pytest.skip('issuer not present in conf, skipping testing issuer')
|
||||
|
||||
issuer = jwt_conf['issuer']
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['iss'] = issuer
|
||||
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
self.conf['headers']['Authorization'] = 'Bearer ' + token
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
def test_jwt_invalid_issuer(self, hge_ctx, endpoint):
|
||||
jwt_conf = json.loads(hge_ctx.hge_jwt_conf)
|
||||
if 'issuer' not in jwt_conf:
|
||||
pytest.skip('issuer not present in conf, skipping testing issuer')
|
||||
|
||||
self.claims['https://hasura.io/jwt/claims'] = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
hasura_claims = mk_claims(hge_ctx.hge_jwt_conf, {
|
||||
'x-hasura-user-id': '1',
|
||||
'x-hasura-default-role': 'user',
|
||||
'x-hasura-allowed-roles': ['user'],
|
||||
})
|
||||
claims_namespace_path = None
|
||||
if 'claims_namespace_path' in hge_ctx.hge_jwt_conf_dict:
|
||||
claims_namespace_path = hge_ctx.hge_jwt_conf_dict['claims_namespace_path']
|
||||
|
||||
self.claims = mk_claims_with_namespace_path(self.claims,hasura_claims,claims_namespace_path)
|
||||
self.claims['iss'] = 'rubbish_issuer'
|
||||
|
||||
token = jwt.encode(self.claims, hge_ctx.hge_jwt_key, algorithm='RS512').decode('utf-8')
|
||||
@ -418,7 +483,7 @@ class TestJwtIssuerCheck():
|
||||
self.conf['status'] = 200
|
||||
if endpoint == '/v1alpha1/graphql':
|
||||
self.conf['status'] = 400
|
||||
check_query(hge_ctx, self.conf, add_auth=False)
|
||||
check_query(hge_ctx, self.conf, add_auth=False,claims_namespace_path=claims_namespace_path)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, setup):
|
||||
|
@ -123,9 +123,21 @@ def test_forbidden_webhook(hge_ctx, conf):
|
||||
})
|
||||
|
||||
|
||||
def mk_claims_with_namespace_path(claims,hasura_claims,namespace_path):
|
||||
if namespace_path is None:
|
||||
claims['https://hasura.io/jwt/claims'] = hasura_claims
|
||||
elif namespace_path == "$.hasuraClaims":
|
||||
claims['hasuraClaims'] = hasura_claims
|
||||
else:
|
||||
raise Exception(
|
||||
'''claims_namespace_path should not be anything
|
||||
other than $.hasuraClaims for testing. The
|
||||
value of claims_namespace_path was {}'''.format(namespace_path))
|
||||
return claims
|
||||
|
||||
# Returns the response received and a bool indicating whether the test passed
|
||||
# or not (this will always be True unless we are `--accepting`)
|
||||
def check_query(hge_ctx, conf, transport='http', add_auth=True):
|
||||
def check_query(hge_ctx, conf, transport='http', add_auth=True, claims_namespace_path=None):
|
||||
hge_ctx.tests_passed = True
|
||||
headers = {}
|
||||
if 'headers' in conf:
|
||||
@ -150,8 +162,8 @@ def check_query(hge_ctx, conf, transport='http', add_auth=True):
|
||||
claim = {
|
||||
"sub": "foo",
|
||||
"name": "bar",
|
||||
"https://hasura.io/jwt/claims": hClaims
|
||||
}
|
||||
claim = mk_claims_with_namespace_path(claim,hClaims,claims_namespace_path)
|
||||
headers['Authorization'] = 'Bearer ' + jwt.encode(claim, hge_ctx.hge_jwt_key, algorithm='RS512').decode(
|
||||
'UTF-8')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user