graphql-engine/server/src-lib/Hasura/GraphQL/Validate.hs
Auke Booij 4d10a610f4
server: fix mishandling of GeoJSON inputs in subscriptions (fix #3239) (#4551)
* Add support for multiple top-level fields in a subscription to improve testability of subscriptions

* Add an internal flag to enable multiple subscriptions

* Add missing call to withConstructorFn in live queries (fix #3239)

Co-authored-by: Alexis King <lexi.lambda@gmail.com>
2020-05-13 10:09:44 +02:00

209 lines
7.8 KiB
Haskell

module Hasura.GraphQL.Validate
( validateGQ
, showVars
, RootSelSet(..)
, SelSet
, Field(..)
, getTypedOp
, QueryParts(..)
, getQueryParts
, ReusableVariableTypes(..)
, ReusableVariableValues
, validateVariablesForReuse
, isQueryInAllowlist
) where
import Hasura.Prelude
import Data.Has
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as HS
import qualified Language.GraphQL.Draft.Syntax as G
import qualified Data.Sequence as Seq
import Hasura.GraphQL.Schema
import Hasura.GraphQL.Transport.HTTP.Protocol
import Hasura.GraphQL.Validate.Context
import Hasura.GraphQL.Validate.Field
import Hasura.GraphQL.Validate.InputValue
import Hasura.GraphQL.Validate.Types
import Hasura.RQL.Types
import Hasura.RQL.Types.QueryCollection
data QueryParts
= QueryParts
{ qpOpDef :: !G.TypedOperationDefinition
, qpOpRoot :: !ObjTyInfo
, qpFragDefsL :: ![G.FragmentDefinition]
, qpVarValsM :: !(Maybe VariableValues)
} deriving (Show, Eq)
getTypedOp
:: (MonadError QErr m)
=> Maybe OperationName
-> [G.SelectionSet]
-> [G.TypedOperationDefinition]
-> m G.TypedOperationDefinition
getTypedOp opNameM selSets opDefs =
case (opNameM, selSets, opDefs) of
(Just opName, [], _) -> do
let n = _unOperationName opName
opDefM = find (\opDef -> G._todName opDef == Just n) opDefs
onNothing opDefM $ throwVE $
"no such operation found in the document: " <> showName n
(Just _, _, _) ->
throwVE $ "operationName cannot be used when " <>
"an anonymous operation exists in the document"
(Nothing, [selSet], []) ->
return $ G.TypedOperationDefinition G.OperationTypeQuery Nothing [] [] selSet
(Nothing, [], [opDef]) ->
return opDef
(Nothing, _, _) ->
throwVE $ "exactly one operation has to be present " <>
"in the document when operationName is not specified"
-- | For all the variables defined there will be a value in the final map
-- If no default, not in variables and nullable, then null value
validateVariables
:: (MonadReader r m, Has TypeMap r, MonadError QErr m)
=> [G.VariableDefinition] -> VariableValues -> m AnnVarVals
validateVariables varDefsL inpVals = withPathK "variableValues" $ do
varDefs <- onLeft (mkMapWith G._vdVariable varDefsL) $ \dups ->
throwVE $ "the following variables are defined more than once: " <>
showVars dups
let unexpectedVars = filter (not . (`Map.member` varDefs)) $ Map.keys inpVals
unless (null unexpectedVars) $
throwVE $ "unexpected variables in variableValues: " <>
showVars unexpectedVars
traverse validateVariable varDefs
where
validateVariable (G.VariableDefinition var ty defM) = do
let baseTy = getBaseTy ty
baseTyInfo <- getTyInfoVE baseTy
-- check that the variable is defined on input types
when (isObjTy baseTyInfo) $ throwVE $
"variables can only be defined on input types"
<> "(enums, scalars, input objects), but "
<> showNamedTy baseTy <> " is an object type"
let defM' = bool (defM <|> Just G.VCNull) defM $ G.isNotNull ty
annDefM <- withPathK "defaultValue" $
mapM (validateInputValue constValueParser ty) defM'
let inpValM = Map.lookup var inpVals
annInpValM <- withPathK (G.unName $ G.unVariable var) $
mapM (validateInputValue jsonParser ty) inpValM
let varValM = annInpValM <|> annDefM
onNothing varValM $ throwVE $
"expecting a value for non-nullable variable: " <>
showVars [var] <>
" of type: " <> G.showGT ty <>
" in variableValues"
showVars :: (Functor f, Foldable f) => f G.Variable -> Text
showVars = showNames . fmap G.unVariable
-- | This is similar in spirit to 'validateVariables' but uses preexisting 'ReusableVariableTypes'
-- information to parse Postgres values directly for use with a reusable query plan. (Ideally, it
-- would be nice to be able to share more of the logic instead of duplicating it.)
validateVariablesForReuse
:: (MonadError QErr m)
=> ReusableVariableTypes -> Maybe VariableValues -> m ReusableVariableValues
validateVariablesForReuse (ReusableVariableTypes varTypes) varValsM =
withPathK "variableValues" $ do
let unexpectedVars = filter (not . (`Map.member` varTypes)) $ Map.keys varVals
unless (null unexpectedVars) $
throwVE $ "unexpected variables: " <> showVars unexpectedVars
flip Map.traverseWithKey varTypes $ \varName varType ->
withPathK (G.unName $ G.unVariable varName) $ do
varVal <- onNothing (Map.lookup varName varVals) $
throwVE "expected a value for non-nullable variable"
-- TODO: we don't have the graphql type
-- <> " of type: " <> T.pack (show varType)
parsePGScalarValue varType varVal
where
varVals = fromMaybe Map.empty varValsM
validateFrag
:: (MonadError QErr m, MonadReader r m, Has TypeMap r)
=> G.FragmentDefinition -> m FragDef
validateFrag (G.FragmentDefinition n onTy dirs selSet) = do
unless (null dirs) $ throwVE
"unexpected directives at fragment definition"
tyInfo <- getTyInfoVE onTy
objTyInfo <- onNothing (getObjTyM tyInfo) $ throwVE
"fragments can only be defined on object types"
return $ FragDef n objTyInfo selSet
data RootSelSet
= RQuery !SelSet
| RMutation !SelSet
| RSubscription !SelSet
deriving (Show, Eq)
validateGQ
:: (MonadError QErr m, MonadReader GCtx m, MonadReusability m) => QueryParts -> m RootSelSet
validateGQ (QueryParts opDef opRoot fragDefsL varValsM) = do
ctx <- ask
-- annotate the variables of this operation
annVarVals <- validateVariables (G._todVariableDefinitions opDef) $ fromMaybe Map.empty varValsM
-- annotate the fragments
fragDefs <- onLeft (mkMapWith G._fdName fragDefsL) $ \dups ->
throwVE $ "the following fragments are defined more than once: " <>
showNames dups
annFragDefs <- mapM validateFrag fragDefs
-- build a validation ctx
let valCtx = ValidationCtx (_gTypes ctx) annVarVals annFragDefs
selSet <- flip runReaderT valCtx $ denormSelSet [] opRoot $
G._todSelectionSet opDef
case G._todType opDef of
G.OperationTypeQuery -> return $ RQuery selSet
G.OperationTypeMutation -> return $ RMutation selSet
G.OperationTypeSubscription ->
case selSet of
Seq.Empty -> throw500 "empty selset for subscription"
(_ Seq.:<| rst) -> do
-- As an internal testing feature, we support subscribing to multiple
-- selection sets. First check if the corresponding directive is set.
let multipleAllowed = elem (G.Directive "_multiple_top_level_fields" []) (G._todDirectives opDef)
unless (multipleAllowed || null rst) $
throwVE "subscriptions must select one top level field"
return $ RSubscription selSet
isQueryInAllowlist :: GQLExecDoc -> HS.HashSet GQLQuery -> Bool
isQueryInAllowlist q = HS.member gqlQuery
where
gqlQuery = GQLQuery $ G.ExecutableDocument $ stripTypenames $
unGQLExecDoc q
getQueryParts
:: ( MonadError QErr m, MonadReader GCtx m)
=> GQLReqParsed
-> m QueryParts
getQueryParts (GQLReq opNameM q varValsM) = do
-- get the operation that needs to be evaluated
opDef <- getTypedOp opNameM selSets opDefs
ctx <- ask
-- get the operation root
opRoot <- case G._todType opDef of
G.OperationTypeQuery -> return $ _gQueryRoot ctx
G.OperationTypeMutation ->
onNothing (_gMutRoot ctx) $ throwVE "no mutations exist"
G.OperationTypeSubscription ->
onNothing (_gSubRoot ctx) $ throwVE "no subscriptions exist"
return $ QueryParts opDef opRoot fragDefsL varValsM
where
(selSets, opDefs, fragDefsL) = G.partitionExDefs $ unGQLExecDoc q