server: add validation for query collections

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3658
GitOrigin-RevId: 3c644da15c92cac16356985d0fe0c6adb7001862
This commit is contained in:
paritosh-08 2022-03-08 15:18:21 +05:30 committed by hasura-bot
parent a2da867fc8
commit 0775c00b0d
24 changed files with 478 additions and 119 deletions

View File

@ -4,6 +4,7 @@
### Bug fixes and improvements
- server: Queries present in query collections, such as allow-list, and rest-endpoints are now validated (against the schema)
- server: Redesigns internal implementation of webhook transforms.
- server: improve SQL generation for BigQuery backend queries involving `Orderby`.
- server: fix regression where remote relationships would get exposed over Relay, which is unsupported

View File

@ -53,6 +53,12 @@ create_query_collection
}
}
.. note::
The queries in query collections are validated against the schema. So, adding an invalid
query would result in inconsistent metadata error. As the query collection is used in allowlists and
REST endpoints, they are validated as well.
.. _metadata_create_query_collection_syntax:
Args Syntax

View File

@ -52,6 +52,12 @@ create_query_collection
}
}
.. note::
The queries in query collections are validated against the schema. So, adding an invalid
query would result in inconsistent metadata error. As the query collection is used in allowlists and
REST endpoints, they are validated as well.
.. _schema_metadata_create_query_collection_syntax:
Args Syntax

View File

@ -50,6 +50,9 @@ You can add or remove a operation in the allow-list in two ways:
.. note::
* The allow list queries are validated against the schema. So, adding an invalid query will result in inconsistent
metadata error.
* ``__typename`` introspection fields will be ignored when adding operations and comparing them to the allow-list.
* Any introspection queries that your client apps require will have to be explicitly added to the allow-list

View File

@ -3,6 +3,7 @@ module Hasura.GraphQL.Analyse
FieldAnalysis (..),
FieldDef (..),
analyzeGraphqlQuery,
getAllAnalysisErrs,
)
where
@ -57,6 +58,24 @@ type FieldName = Name
type VarName = Name
analyzeGraphqlQuery :: ExecutableDefinition Name -> RemoteSchemaIntrospection -> Maybe (Analysis Name)
analyzeGraphqlQuery (G.ExecutableDefinitionOperation (G.OperationDefinitionTyped td)) sc = do
let t = (G._todType td,) <$> G._todSelectionSet td
varDefs = G._todVariableDefinitions td
varMapList = map (\v -> (G._vdName v, (G._vdType v, G._vdDefaultValue v))) varDefs
varMap = Map.fromList varMapList
(fieldMap, errs) = getFieldsMap sc t
pure Analysis {_aFields = fieldMap, _aVars = varMap, _aErrs = errs}
analyzeGraphqlQuery _ _ = Nothing
getAllAnalysisErrs :: Analysis Name -> [Text]
getAllAnalysisErrs Analysis {..} = _aErrs <> getFieldErrs (OMap.toList _aFields) []
where
getFieldErrs :: [(G.Name, (FieldDef, Maybe (FieldAnalysis G.Name)))] -> [Text] -> [Text]
getFieldErrs [] lst = lst
getFieldErrs ((_, (_, Just FieldAnalysis {..})) : xs) lst = _fErrs <> (getFieldErrs (OMap.toList _fFields) []) <> (getFieldErrs xs lst)
getFieldErrs ((_, (_, Nothing)) : xs) lst = getFieldErrs xs lst
-- | inserts in field map, if there is already a key, then take a union of the fields inside
-- i.e. for the following graphql query:
-- query MyQuery {
@ -78,15 +97,6 @@ safeInsertInFieldMap ::
safeInsertInFieldMap m (k, v) =
OMap.insertWith (\(fdef, (f1)) (_, (f2)) -> (fdef, f1 <> f2)) k v m
analyzeGraphqlQuery :: ExecutableDefinition Name -> RemoteSchemaIntrospection -> Maybe (Analysis Name)
analyzeGraphqlQuery (G.ExecutableDefinitionOperation (G.OperationDefinitionTyped td)) sc = do
let t = (G._todType td,) <$> G._todSelectionSet td
varDefs = G._todVariableDefinitions td
varMap = foldr (\G.VariableDefinition {..} m -> Map.insert _vdName (_vdType, _vdDefaultValue) m) mempty varDefs
(fieldMap, errs) = getFieldsMap sc t
pure Analysis {_aFields = fieldMap, _aVars = varMap, _aErrs = errs}
analyzeGraphqlQuery _ _ = Nothing
getFieldName :: Field frag var -> Name
getFieldName f = fromMaybe (G._fName f) (G._fAlias f)
@ -105,6 +115,12 @@ getFieldsMap rs ss =
where
fields = mapMaybe (\(o, s) -> (o,) <$> field s) ss
getFieldsTypeM :: Name -> TypeDefinition possibleTypes inputType -> Maybe GType
getFieldsTypeM fieldName operationDefinitionSum = do
operationDefinitionObject <- asObjectTypeDefinition operationDefinitionSum
fieldDefinition <- find ((== fieldName) . G._fldName) $ G._otdFieldsDefinition operationDefinitionObject
pure $ G._fldType fieldDefinition
lookupRoot ::
RemoteSchemaIntrospection ->
(G.OperationType, G.Field frag var) ->
@ -114,14 +130,37 @@ lookupRoot rs (ot, f) = do
fieldName = G._fName f
fieldTypeM = do
operationDefinitionSum <- lookupRS rs rootFieldName
operationDefinitionObject <- asObjectTypeDefinition operationDefinitionSum
fieldDefinition <- find ((== fieldName) . G._fldName) $ G._otdFieldsDefinition operationDefinitionObject
pure $ G._fldType fieldDefinition
getFieldsTypeM fieldName operationDefinitionSum
case fieldTypeM of
Nothing -> Right $ ["Couldn't find field " <> G.unName fieldName <> " in root field " <> G.unName rootFieldName]
Nothing ->
case isMetaField fieldName True of
Nothing -> Right $ ["Couldn't find field " <> G.unName fieldName <> " in root field " <> G.unName rootFieldName]
Just fld -> lookupDefinition rs fld f
Just fieldType -> lookupDefinition rs fieldType f
isMetaField :: Name -> Bool -> Maybe GType
isMetaField nam isRoot = do
n <- fieldTypeMetaFields $ nam
case G.unName nam of
"__schema" -> if isRoot then Just $ mkGType n else Nothing
"__type" -> if isRoot then Just $ mkGType n else Nothing
"__typename" -> Just $ mkGType n
_ -> Nothing
where
mkGType :: Name -> GType
mkGType fName =
G.TypeNamed
(G.Nullability {unNullability = False})
fName
fieldTypeMetaFields :: Name -> Maybe Name
fieldTypeMetaFields nam
| (Just nam) == G.mkName "__schema" = G.mkName "__Schema"
| (Just nam) == G.mkName "__type" = G.mkName "__Type"
| (Just nam) == G.mkName "__typename" = G.mkName "String"
| otherwise = Nothing
lookupDefinition ::
RemoteSchemaIntrospection ->
GType ->
@ -173,8 +212,13 @@ getDefinition ::
getDefinition rs td sels =
(td,) $ case td of
(G.TypeDefinitionObject otd) -> do
ps <- traverse (\sel -> fmap (mkFieldAnalysis rs sel) (lookupFieldBySelection sel (G._otdFieldsDefinition otd))) sels
pure $ fold $ catMaybes ps
ps <-
for
sels
\sel -> case (lookupFieldBySelection sel otd) of
Left txts -> pure $ FieldAnalysis mempty mempty txts
Right fd -> (mkFieldAnalysis rs sel fd)
pure $ fold ps
_ -> Nothing
itrListWith :: GType -> (Name -> p) -> p
@ -219,6 +263,21 @@ mkFieldAnalysis rs (G.SelectionField f) fd = do
_fErrs = fErrs
}
lookupFieldBySelection :: G.Selection frag0 var0 -> [G.FieldDefinition RemoteSchemaInputValueDefinition] -> Maybe (G.FieldDefinition RemoteSchemaInputValueDefinition)
lookupFieldBySelection (G.SelectionField f) = find \d -> G._fName f == G._fldName d
lookupFieldBySelection _ = const Nothing
lookupFieldBySelection :: G.Selection frag0 var0 -> G.ObjectTypeDefinition RemoteSchemaInputValueDefinition -> Either [Text] (G.FieldDefinition RemoteSchemaInputValueDefinition)
lookupFieldBySelection (G.SelectionField f) otd = case find (\d -> G._fName f == G._fldName d) lst of
Nothing -> case isMetaField (G._fName f) False of
Nothing -> Left ["Couldn't find definition for field " <> G.unName (getFieldName f) <> " in " <> G.unName (G._otdName otd)]
Just gt -> Right $ mkFieldDef (G._fName f) gt
Just fd -> Right fd
where
lst = G._otdFieldsDefinition otd
mkFieldDef :: G.Name -> GType -> G.FieldDefinition RemoteSchemaInputValueDefinition
mkFieldDef gName gt =
G.FieldDefinition
{ _fldDescription = Nothing,
_fldName = gName,
_fldArgumentsDefinition = [],
_fldType = gt,
_fldDirectives = []
}
lookupFieldBySelection _ _ = Left []

View File

@ -33,7 +33,7 @@ import Hasura.Base.Error
import Hasura.GraphQL.Context
import Hasura.GraphQL.Namespace (mkUnNamespacedRootFieldAlias)
import Hasura.GraphQL.Parser.Collect ()
import Hasura.GraphQL.Parser.Schema (Variable)
import Hasura.GraphQL.Parser.Schema (InputValue (..), Variable (..), VariableInfo (..))
-- Needed for GHCi and HLS due to TH in cyclically dependent modules (see https://gitlab.haskell.org/ghc/ghc/-/issues/1012)
import Hasura.GraphQL.Schema.Remote (buildRemoteParser)
import Hasura.GraphQL.Transport.HTTP.Protocol
@ -569,9 +569,13 @@ getSchemaIntrospection gqlContext = do
fieldMap <- either (const Nothing) Just $ gqlQueryParser _rctxDefault $ fmap (fmap nameToVariable) $ G._todSelectionSet $ _grQuery introspectionQuery
RFRaw v <- OMap.lookup (mkUnNamespacedRootFieldAlias $$(G.litName "__schema")) fieldMap
fmap irDoc $ parseIntrospectionResult $ J.object [("data", J.object [("__schema", JO.fromOrdered v)])]
nameToVariable :: G.Name -> Variable
nameToVariable n = Variable vInf vTyp vVal
where
-- TODO: Look for a way to convert Name to Variable (using some default types) or make the
-- ParserFn general (i.e. type ParserFn a b = G.SelectionSet G.NoFragments b -> ...)
-- This value isn't used but we give it a type to be more clear about what is being ignored
nameToVariable :: G.Name -> Variable
nameToVariable n = errorE . T.unpack $ G.unName n <> " cannot be converted to Variable"
vInf = VIRequired n
vTyp = G.TypeNamed (G.Nullability True) n
vVal = dummyValue
dummyValue :: InputValue Void
dummyValue = JSONValue $ J.String $ "nameToVariable is being called"

View File

@ -23,6 +23,7 @@ module Hasura.Prelude
hoistEither,
readJson,
tshow,
hashNub,
-- * Trace debugging
ltrace,
@ -92,6 +93,7 @@ import Data.HashMap.Strict qualified as Map
import Data.HashMap.Strict.InsOrd as M (InsOrdHashMap)
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.HashSet as M (HashSet)
import Data.HashSet qualified as HSet
import Data.Hashable (hashWithSalt)
import Data.Hashable as M (Hashable)
import Data.List as M
@ -293,3 +295,12 @@ ltrace lbl x = Debug.trace (lbl <> ": " <> TL.unpack (PS.pShow x)) x
ltraceM :: Applicative m => Show a => String -> a -> m ()
ltraceM lbl x = Debug.traceM (lbl <> ": " <> TL.unpack (PS.pShow x))
{-# WARNING ltraceM "ltraceM left in code" #-}
-- | Remove duplicates from a list. Like 'nub' but runs in @O(n * log_16(n))@
-- time and requires 'Hashable' and `Eq` instances. hashNub is faster than
-- ordNub when there're not so many different values in the list.
--
-- >>> hashNub [1,3,2,9,4,1,5,7,3,3,1,2,5,4,3,2,1,0]
-- [0,1,2,3,4,5,7,9]
hashNub :: (Hashable a, Eq a) => [a] -> [a]
hashNub = HSet.toList . HSet.fromList

View File

@ -438,6 +438,7 @@ purgeMetadataObj = \case
MOEndpoint epName -> dropEndpointInMetadata epName
MOInheritedRole role -> dropInheritedRoleInMetadata role
MOHostTlsAllowlist host -> dropHostFromAllowList host
MOQueryCollectionsQuery cName lq -> dropListedQueryFromQueryCollections cName lq
where
handleSourceObj :: forall b. BackendMetadata b => SourceName -> SourceMetadataObjId b -> MetadataModifier
handleSourceObj source = \case
@ -453,6 +454,42 @@ purgeMetadataObj = \case
MTOComputedField ccn -> dropComputedFieldInMetadata ccn
MTORemoteRelationship rn -> dropRemoteRelationshipInMetadata rn
dropListedQueryFromQueryCollections :: CollectionName -> ListedQuery -> MetadataModifier
dropListedQueryFromQueryCollections cName lq = MetadataModifier $ removeAndCleanupMetadata
where
removeAndCleanupMetadata m =
let newQueryCollection = filteredCollection (_metaQueryCollections m)
-- QueryCollections = InsOrdHashMap CollectionName CreateCollection
filteredCollection :: QueryCollections -> QueryCollections
filteredCollection qc = OMap.filter (isNonEmptyCC) $ OMap.adjust (collectionModifier) (cName) qc
collectionModifier :: CreateCollection -> CreateCollection
collectionModifier cc@CreateCollection {..} =
cc
{ _ccDefinition =
let oldQueries = _cdQueries _ccDefinition
in _ccDefinition
{ _cdQueries = filter (/= lq) oldQueries
}
}
isNonEmptyCC :: CreateCollection -> Bool
isNonEmptyCC = not . null . _cdQueries . _ccDefinition
cleanupAllowList :: MetadataAllowlist -> MetadataAllowlist
cleanupAllowList = OMap.filterWithKey (\_ _ -> OMap.member cName newQueryCollection)
cleanupRESTEndpoints :: Endpoints -> Endpoints
cleanupRESTEndpoints endpoints = OMap.filter (not . isFaultyQuery . _edQuery . _ceDefinition) endpoints
isFaultyQuery :: QueryReference -> Bool
isFaultyQuery QueryReference {..} = _qrCollectionName == cName && _qrQueryName == (_lqName lq)
in m
{ _metaQueryCollections = newQueryCollection,
_metaAllowlist = cleanupAllowList (_metaAllowlist m),
_metaRestEndpoints = cleanupRESTEndpoints (_metaRestEndpoints m)
}
runGetCatalogState ::
(MonadMetadataStorageQueryAPI m) => GetCatalogState -> m EncJSON
runGetCatalogState _ =

View File

@ -281,6 +281,9 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
endpointObject :: EndpointMetadata q -> MetadataObject
endpointObject md = MetadataObject (endpointObjId md) (toJSON $ OMap.lookup (_ceName md) $ _metaRestEndpoints metadata)
listedQueryObjects :: (CollectionName, ListedQuery) -> MetadataObject
listedQueryObjects (cName, lq) = MetadataObject (MOQueryCollectionsQuery cName lq) (toJSON lq)
-- Cases of urls that generate invalid segments:
hasInvalidSegments :: EndpointMetadata query -> Bool
@ -299,8 +302,11 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
ambiguousF' ep = MetadataObject (endpointObjId ep) (toJSON ep)
ambiguousF mds = AmbiguousRestEndpoints (commaSeparated $ map _ceUrl mds) (map ambiguousF' mds)
ambiguousRestEndpoints = map (ambiguousF . S.elems . snd) $ ambiguousPathsGrouped endpoints
maybeRS = getSchemaIntrospection gqlContext
inconsistentRestQueries = getInconsistentRestQueries maybeRS endpoints endpointObject
queryCollections = _boQueryCollections resolvedOutputs
allowLists = HS.toList . iaGlobal . _boAllowlist $ resolvedOutputs
inconsistentQueryCollections = getInconsistentQueryCollections maybeRS queryCollections listedQueryObjects endpoints allowLists
returnA
-<
@ -328,12 +334,13 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
<> duplicateRestVariables
<> invalidRestSegments
<> ambiguousRestEndpoints
<> inconsistentRestQueries,
<> inconsistentQueryCollections,
scApiLimits = _boApiLimits resolvedOutputs,
scMetricsConfig = _boMetricsConfig resolvedOutputs,
scMetadataResourceVersion = Nothing,
scSetGraphqlIntrospectionOptions = _metaSetGraphqlIntrospectionOptions metadata,
scTlsAllowlist = _boTlsAllowlist resolvedOutputs
scTlsAllowlist = _boTlsAllowlist resolvedOutputs,
scQueryCollections = _boQueryCollections resolvedOutputs
}
where
getSourceConfigIfNeeded ::
@ -833,7 +840,8 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
_boApiLimits = apiLimits,
_boMetricsConfig = metricsConfig,
_boRoles = mapFromL _rRoleName $ _unOrderedRoles orderedRoles,
_boTlsAllowlist = (networkTlsAllowlist networkConfig)
_boTlsAllowlist = (networkTlsAllowlist networkConfig),
_boQueryCollections = collections
}
mkEndpointMetadataObject (name, createEndpoint) =

View File

@ -25,6 +25,7 @@ module Hasura.RQL.DDL.Schema.Cache.Common
boCronTriggers,
boCustomTypes,
boEndpoints,
boQueryCollections,
boRemoteSchemas,
boRoles,
boSources,
@ -157,7 +158,8 @@ data BuildOutputs = BuildOutputs
_boApiLimits :: !ApiLimit,
_boMetricsConfig :: !MetricsConfig,
_boRoles :: !(HashMap RoleName Role),
_boTlsAllowlist :: ![TlsAllow]
_boTlsAllowlist :: ![TlsAllow],
_boQueryCollections :: !QueryCollections
}
$(makeLenses ''BuildOutputs)

View File

@ -216,6 +216,12 @@ deleteMetadataObject = \case
MOActionPermission name role -> boActions . ix name . aiPermissions %~ M.delete role
MOInheritedRole name -> boRoles %~ M.delete name
MOHostTlsAllowlist host -> removeHostFromAllowList host
MOQueryCollectionsQuery cName lq -> \bo@BuildOutputs {..} ->
bo
{ _boEndpoints = removeEndpointsUsingQueryCollection lq _boEndpoints,
_boAllowlist = removeFromAllowList lq _boAllowlist,
_boQueryCollections = removeFromQueryCollections cName lq _boQueryCollections
}
where
removeHostFromAllowList hst bo =
bo
@ -243,3 +249,34 @@ deleteMetadataObject = \case
MTOTrigger name -> tiEventTriggerInfoMap %~ M.delete name
MTOPerm roleName permType -> withPermType permType \accessor ->
tiRolePermInfoMap . ix roleName . permAccToLens accessor .~ Nothing
removeFromQueryCollections :: CollectionName -> ListedQuery -> QueryCollections -> QueryCollections
removeFromQueryCollections cName lq qc =
let collectionModifier :: CreateCollection -> CreateCollection
collectionModifier cc@CreateCollection {..} =
cc
{ _ccDefinition =
let oldQueries = _cdQueries _ccDefinition
in _ccDefinition
{ _cdQueries = filter (/= lq) oldQueries
}
}
in OMap.adjust collectionModifier cName qc
removeEndpointsUsingQueryCollection :: ListedQuery -> HashMap EndpointName (EndpointMetadata GQLQueryWithText) -> HashMap EndpointName (EndpointMetadata GQLQueryWithText)
removeEndpointsUsingQueryCollection lq endpointMap =
case maybeEndpoint of
Just (n, _) -> M.delete n endpointMap
Nothing -> endpointMap
where
q = _lqQuery lq
maybeEndpoint = find (\(_, def) -> (_edQuery . _ceDefinition) def == q) (M.toList endpointMap)
removeFromAllowList :: ListedQuery -> InlinedAllowlist -> InlinedAllowlist
removeFromAllowList lq aList =
let oldAList = iaGlobal aList
gqlQry = NormalizedQuery . unGQLQuery . getGQLQuery . _lqQuery $ lq
newAList = HS.delete gqlQry oldAList
in aList
{ iaGlobal = newAList
}

View File

@ -1,6 +1,6 @@
module Hasura.RQL.Types.Allowlist
( -- | The schema cache representation of the allowlist
InlinedAllowlist,
InlinedAllowlist (..),
inlineAllowlist,
AllowlistMode (..),
allowlistAllowsQuery,
@ -13,6 +13,7 @@ module Hasura.RQL.Types.Allowlist
metadataAllowlistInsert,
metadataAllowlistUpdateScope,
metadataAllowlistAllCollections,
NormalizedQuery (..),
)
where

View File

@ -49,6 +49,7 @@ import Hasura.RQL.Types.Endpoint
import Hasura.RQL.Types.EventTrigger
import Hasura.RQL.Types.Instances ()
import Hasura.RQL.Types.Permission
import Hasura.RQL.Types.QueryCollection (CollectionName, ListedQuery (_lqName))
import Hasura.RQL.Types.RemoteSchema
import Hasura.SQL.AnyBackend qualified as AB
import Hasura.Session
@ -95,6 +96,7 @@ data MetadataObjId
| MOInheritedRole !RoleName
| MOEndpoint !EndpointName
| MOHostTlsAllowlist !String
| MOQueryCollectionsQuery !CollectionName !ListedQuery
deriving (Show, Eq, Generic)
$(makePrisms ''MetadataObjId)
@ -115,6 +117,7 @@ moiTypeName = \case
MOInheritedRole _ -> "inherited_role"
MOEndpoint _ -> "rest_endpoint"
MOHostTlsAllowlist _ -> "host_network_tls_allowlist"
MOQueryCollectionsQuery _ _ -> "query_collections"
where
handleSourceObj :: forall b. SourceMetadataObjId b -> Text
handleSourceObj = \case
@ -147,6 +150,7 @@ moiName objectId =
MOInheritedRole inheritedRoleName -> "inherited role " <> toTxt inheritedRoleName
MOEndpoint name -> toTxt name
MOHostTlsAllowlist hostTlsAllowlist -> T.pack hostTlsAllowlist
MOQueryCollectionsQuery cName lq -> (toTxt . _lqName) lq <> " in " <> toTxt cName
where
handleSourceObj ::
forall b.

View File

@ -53,7 +53,7 @@ newtype GQLQuery = GQLQuery {unGQLQuery :: G.ExecutableDocument G.Name}
newtype GQLQueryWithText
= GQLQueryWithText (Text, GQLQuery)
deriving (Show, Eq, Ord, NFData, Generic, Cacheable)
deriving (Show, Eq, Ord, NFData, Generic, Cacheable, Hashable)
instance FromJSON GQLQueryWithText where
parseJSON v@(String t) = GQLQueryWithText . (t,) <$> parseJSON v
@ -78,6 +78,8 @@ instance NFData ListedQuery
instance Cacheable ListedQuery
instance Hashable ListedQuery
$(deriveJSON hasuraJSON ''ListedQuery)
newtype CollectionDef = CollectionDef

View File

@ -331,7 +331,8 @@ data SchemaCache = SchemaCache
scMetricsConfig :: !MetricsConfig,
scMetadataResourceVersion :: !(Maybe MetadataResourceVersion),
scSetGraphqlIntrospectionOptions :: !SetGraphqlIntrospectionOptions,
scTlsAllowlist :: ![TlsAllow]
scTlsAllowlist :: ![TlsAllow],
scQueryCollections :: !QueryCollections
}
-- WARNING: this can only be used for debug purposes, as it loses all
@ -355,7 +356,8 @@ instance ToJSON SchemaCache where
"metrics_config" .= toJSON scMetricsConfig,
"metadata_resource_version" .= toJSON scMetadataResourceVersion,
"set_graphql_introspection_options" .= toJSON scSetGraphqlIntrospectionOptions,
"tls_allowlist" .= toJSON scTlsAllowlist
"tls_allowlist" .= toJSON scTlsAllowlist,
"query_collection" .= toJSON scQueryCollections
]
getAllRemoteSchemas :: SchemaCache -> [RemoteSchemaName]

View File

@ -24,7 +24,7 @@ module Hasura.RQL.Types.SchemaCache.Build
buildSchemaCacheFor,
buildSchemaCacheStrict,
withNewInconsistentObjsCheck,
getInconsistentRestQueries,
getInconsistentQueryCollections,
)
where
@ -39,7 +39,6 @@ import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.HashMap.Strict.Multi qualified as MultiMap
import Data.List qualified as L
import Data.Sequence qualified as Seq
import Data.Set qualified as S
import Data.Text.Extended
import Data.Text.NonEmpty (unNonEmptyText)
import Data.Trie qualified as Trie
@ -47,8 +46,9 @@ import Database.MSSQL.Transaction qualified as MSSQL
import Database.PG.Query qualified as Q
import Hasura.Backends.Postgres.Connection
import Hasura.Base.Error
import Hasura.GraphQL.Analyse (Analysis (Analysis, _aErrs, _aFields), FieldAnalysis (FieldAnalysis, _fErrs, _fFields), FieldDef, analyzeGraphqlQuery)
import Hasura.GraphQL.Analyse
import Hasura.Prelude
import Hasura.RQL.Types.Allowlist (NormalizedQuery, unNormalizedQuery)
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.Endpoint
import Hasura.RQL.Types.Metadata
@ -349,7 +349,7 @@ withNewInconsistentObjsCheck action = do
let diffInconsistentObjects = Map.difference `on` groupInconsistentMetadataById
newInconsistentObjects =
L.nub $ concatMap toList $ Map.elems (currentObjects `diffInconsistentObjects` originalObjects)
hashNub $ concatMap toList $ Map.elems (currentObjects `diffInconsistentObjects` originalObjects)
unless (null newInconsistentObjects) $
throwError
(err500 Unexpected "cannot continue due to newly found inconsistent metadata")
@ -358,36 +358,61 @@ withNewInconsistentObjsCheck action = do
pure result
-- | getInconsistentRestQueries is a helper function that runs the
-- static analysis over the saved queries for each endpoints and
-- reports any inconsistenties with the current schema.
getInconsistentRestQueries :: Maybe RemoteSchemaIntrospection -> EndpointTrie GQLQueryWithText -> (EndpointMetadata GQLQueryWithText -> MetadataObject) -> [InconsistentMetadata]
getInconsistentRestQueries Nothing _ _ = []
getInconsistentRestQueries (Just rs) tMap getMetaObj = map (\(o, t) -> InconsistentObject t Nothing o) fE
-- | getInconsistentQueryCollections is a helper function that runs the
-- static analysis over the saved queries and reports any inconsistenties
-- with the current schema.
getInconsistentQueryCollections ::
Maybe RemoteSchemaIntrospection ->
QueryCollections ->
((CollectionName, ListedQuery) -> MetadataObject) ->
EndpointTrie GQLQueryWithText ->
[NormalizedQuery] ->
[InconsistentMetadata]
getInconsistentQueryCollections Nothing _ _ _ _ = []
getInconsistentQueryCollections (Just rs) qcs lqToMetadataObj restEndpoints allowLst = map (\(o, t) -> InconsistentObject t Nothing o) fE
where
methodList = concatMap S.toList $ concatMap MultiMap.elems $ Trie.elems tMap
endpoints = concatMap (\x -> map (x,) (mdDefinitions x)) methodList
mdDefinitions :: EndpointMetadata GQLQueryWithText -> [G.ExecutableDefinition G.Name]
mdDefinitions = G.getExecutableDefinitions . unGQLQuery . getGQLQuery . _edQuery . _ceDefinition
fE = lefts $ map (validateQuery rs) endpoints
zipLQwithDef :: (CollectionName, CreateCollection) -> [((CollectionName, ListedQuery), G.ExecutableDefinition G.Name)]
zipLQwithDef (cName, cc) =
let lqs = _cdQueries . _ccDefinition $ cc
in concatMap (\lq -> map ((cName, lq),) (G.getExecutableDefinitions . unGQLQuery . getGQLQuery . _lqQuery $ lq)) lqs
inAllowList :: [NormalizedQuery] -> (ListedQuery) -> Bool
inAllowList nqList (ListedQuery {..}) = any (\nq -> unNormalizedQuery nq == (unGQLQuery . getGQLQuery) _lqQuery) nqList
showErrLst = dquoteList . reverse
formatError (endpointName, analysisErrs) = endpointName <> " (" <> showErrLst analysisErrs <> ")"
validateQuery ::
RemoteSchemaIntrospection ->
(EndpointMetadata GQLQueryWithText, G.ExecutableDefinition G.Name) ->
Either (MetadataObject, Text) ()
validateQuery rSchema (eMeta, eDef) = do
let analysis = analyzeGraphqlQuery eDef rSchema
endpointName = unNonEmptyText $ unEndpointName (_ceName eMeta)
case analysis of
Nothing -> Left (getMetaObj eMeta, "Cannot analyse the GraphQL query for the REST endpoint: " <> endpointName)
Just Analysis {..} ->
let getFieldErrs :: [(G.Name, (FieldDef, Maybe (FieldAnalysis G.Name)))] -> [Text] -> [Text]
getFieldErrs [] lst = lst
getFieldErrs ((_, (_, Just FieldAnalysis {..})) : xs) lst = _fErrs <> (getFieldErrs (OMap.toList _fFields) []) <> (getFieldErrs xs lst)
getFieldErrs ((_, (_, Nothing)) : xs) lst = getFieldErrs xs lst
allErrs = _aErrs <> getFieldErrs (OMap.toList _aFields) []
in if (null allErrs)
then Right ()
else Left (getMetaObj eMeta, formatError (endpointName, allErrs))
inRESTEndpoints :: EndpointTrie GQLQueryWithText -> (ListedQuery) -> [Text]
inRESTEndpoints edTrie lq = map fst $ filter (queryIsFaulty) allQueries
where
methodMaps = Trie.elems edTrie
endpoints = concatMap snd $ concatMap (MultiMap.toList) methodMaps
allQueries :: [(Text, GQLQueryWithText)]
allQueries = map (\d -> (unNonEmptyText . unEndpointName . _ceName $ d, _edQuery . _ceDefinition $ d)) endpoints
queryIsFaulty :: (Text, GQLQueryWithText) -> Bool
queryIsFaulty (_, gqlQ) = (_lqQuery lq) == gqlQ
lqLst = concatMap zipLQwithDef (OMap.toList qcs)
fE = lefts $ map (validateQuery rs (lqToMetadataObj) formatError) lqLst
formatError (cName, lq) allErrs =
let msgInit = "In query collection \"" <> toTxt cName <> "\" the query \"" <> (toTxt . _lqName) lq <> "\" is invalid with the following error(s): "
lToTxt = dquoteList . reverse
faultyEndpoints = case inRESTEndpoints restEndpoints lq of
[] -> ""
ePoints -> ". This query is being used in the following REST endpoint(s): " <> lToTxt ePoints
isInAllowList = if inAllowList allowLst lq then ". This query is in allowlist." else ""
in msgInit <> lToTxt allErrs <> faultyEndpoints <> isInAllowList
validateQuery ::
RemoteSchemaIntrospection ->
(a -> MetadataObject) ->
(a -> [Text] -> Text) ->
(a, G.ExecutableDefinition G.Name) ->
Either (MetadataObject, Text) ()
validateQuery rSchema getMetaObj formatError (eMeta, eDef) = do
let analysis = analyzeGraphqlQuery eDef rSchema
case analysis of
Nothing -> Left (getMetaObj eMeta, formatError eMeta ["Cannot analyse the GraphQL query"])
Just a ->
let allErrs = getAllAnalysisErrs a
in if (null allErrs)
then Right ()
else Left (getMetaObj eMeta, formatError eMeta allErrs)

View File

@ -26,7 +26,7 @@ import Data.Text qualified as T
import Data.Text.Extended (commaSeparated)
import Data.Text.NonEmpty
import Data.Trie qualified as Trie
import Hasura.GraphQL.Analyse (Analysis (Analysis, _aFields, _aVars), FieldAnalysis (FieldAnalysis, _fFields), FieldDef (FieldInfo, FieldList), analyzeGraphqlQuery)
import Hasura.GraphQL.Analyse
import Hasura.GraphQL.RemoteServer (getSchemaIntrospection)
import Hasura.Prelude hiding (get, put)
import Hasura.RQL.Types.Endpoint
@ -43,7 +43,8 @@ data EndpointData = EndpointData
_edProperties :: InsOrdHashMap Text (Referenced Schema),
_edResponse :: Maybe Response,
_edDescription :: Text, -- contains API comments and graphql query
_edName :: Text
_edName :: Text,
_edErrs :: [Text]
}
deriving (Show)
@ -427,6 +428,7 @@ extractEndpointInfo sd method d = do
_edDescription = getComment d
_edName = unNonEmptyText $ unEndpointName $ _ceName d
_edMethod = [unEndpointMethod method] -- NOTE: Methods are grouped with into matching endpoints - Name used for grouping.
_edErrs = getAllAnalysisErrs _analysis
getEndpointsData :: Maybe RemoteSchemaIntrospection -> SchemaCache -> Declare (Definitions Schema) [EndpointData]
getEndpointsData sd sc = do
@ -553,8 +555,19 @@ declareOpenApiSpec sc = do
openAPIPaths = mkOpenAPISchema endpointLst
allWarnings = foldl addEndpointWarnings warnings endpointLst
addEndpointWarnings :: Text -> EndpointData -> Text
addEndpointWarnings oldWarn EndpointData {..} =
if null _edErrs
then oldWarn
else
oldWarn <> "\n\nEndpoint \""
<> _edName
<> "\":\n"
<> foldl (\w err -> w <> "\n- ⚠️ " <> err) "" _edErrs
return $
mempty
& paths .~ openAPIPaths
& info . title .~ "Rest Endpoints"
& info . description ?~ "This OpenAPI specification is automatically generated by Hasura." <> warnings
& info . description ?~ "This OpenAPI specification is automatically generated by Hasura." <> allWarnings

View File

@ -72,20 +72,15 @@
response:
internal:
- definition:
definition:
query:
collection_name: temp_collection
query_name: temp_query
url: temp_rest_API
methods:
- GET
name: temp_rest_API
comment:
reason: "Inconsistent object: temp_rest_API (\"Couldn't find field temp_table\
\ in root field query_root\")"
name: rest_endpoint temp_rest_API
type: rest_endpoint
path: "$.args"
name: temp_query
query: query { temp_table { col1 col2 } }
reason: "Inconsistent object: In query collection \"temp_collection\" the query\
\ \"temp_query\" is invalid with the following error(s): \"Couldn't find field temp_table\
\ in root field query_root\". This query is being used in the following REST endpoint(s):\
\ \"temp_rest_API\""
name: query_collections temp_query in temp_collection
type: query_collections
path: $.args
error: cannot continue due to newly found inconsistent metadata
code: unexpected
@ -93,20 +88,47 @@
url: /v1/query
status: 200
query:
type: bulk
type: drop_rest_endpoint
args:
- type: drop_rest_endpoint
args:
name: temp_rest_API
- type: drop_query_collection
args:
collection: temp_collection
cascade: false
name: temp_rest_API
response:
- message: success
- message: success
message: success
- description: cleanup 2 (drop the table)
- description: untrack the table
url: /v1/query
status: 500
query:
type: untrack_table
args:
table:
schema: public
name: temp_table
response:
internal:
- definition:
name: temp_query
query: query { temp_table { col1 col2 } }
reason: "Inconsistent object: In query collection \"temp_collection\" the query\
\ \"temp_query\" is invalid with the following error(s): \"Couldn't find field\
\ temp_table in root field query_root\""
name: query_collections temp_query in temp_collection
type: query_collections
path: $.args
error: cannot continue due to newly found inconsistent metadata
code: unexpected
- description: cleanup 2 (drop the query collection)
url: /v1/query
status: 200
query:
type: drop_query_collection
args:
collection: temp_collection
cascade: true
response:
message: success
- description: cleanup 3 (drop the table)
url: /v1/query
status: 200
query:

View File

@ -1,25 +1,5 @@
type: bulk
args:
- type: create_query_collection
args:
name: test_collection
definition:
queries:
- name: simple_query
query: "query { test_table { first_name last_name } }"
- name: simple_query_cached
query: "query @cached(ttl: 5) { test_table { first_name last_name } }"
- name: query_with_arg
query: "query ($first_name:String!) { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
- name: query_with_args
query: "query ($first_name: String!, $last_name:String!) { test_table(where: {first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) { first_name last_name } }"
- name: query_with_uuid_arg
query: "query ($id: uuid!) { test_table(where: {id: { _neq: $id }}) { first_name last_name } }"
- name: query_with_uuid_args
query: "query ($ids: [uuid!]!) { test_table(where: {id: { _in: $ids }}) { first_name last_name } }"
- name: simple_subscription
query: "subscription { test_table { first_name last_name } }"
- type: run_sql
args:
sql: |
@ -43,6 +23,26 @@ args:
('Baz', 'Qux'),
('X%20Y', 'Test');
- type: create_query_collection
args:
name: test_collection
definition:
queries:
- name: simple_query
query: "query { test_table { first_name last_name } }"
- name: simple_query_cached
query: "query @cached(ttl: 5) { test_table { first_name last_name } }"
- name: query_with_arg
query: "query ($first_name:String!) { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
- name: query_with_args
query: "query ($first_name: String!, $last_name:String!) { test_table(where: {first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) { first_name last_name } }"
- name: query_with_uuid_arg
query: "query ($id: uuid!) { test_table(where: {id: { _neq: $id }}) { first_name last_name } }"
- name: query_with_uuid_args
query: "query ($ids: [uuid!]!) { test_table(where: {id: { _in: $ids }}) { first_name last_name } }"
- name: simple_subscription
query: "subscription { test_table { first_name last_name } }"
- type: create_rest_endpoint
args:
url: simple

View File

@ -5,6 +5,11 @@ args:
collection: collection_1
cascade: True
- type: drop_query_collection
args:
collection: collection_2
cascade: True
- type: run_sql
args:
sql: |

View File

@ -0,0 +1,40 @@
- description: Call openapi json endpoint
url: /api/swagger/json
method: GET
status: 200
query:
response:
openapi: 3.0.0
info:
version: ''
title: Rest Endpoints
description: "This OpenAPI specification is automatically generated by Hasura.\n\
\nEndpoint \"wrong_endpoint\":\n\n- ⚠️ Couldn't find field random_field_name\
\ in root field query_root\n- ⚠️ Couldn't find definition for field random_col_name\
\ in test_table"
paths:
/api/rest/some_url:
get:
summary: wrong_endpoint
description: "***\nThe GraphQl query for this endpoint is:\n``` graphql\n\
query { random_field_name test_table { random_col_name } }\n```"
parameters:
- schema:
type: string
in: header
name: x-hasura-admin-secret
description: Your x-hasura-admin-secret will be used for authentication
of the API request.
responses:
'200':
content:
application/json:
schema:
properties:
test_table:
items:
type: object
type: array
nullable: false
description: Responses for GET /api/rest/some_url
components: {}

View File

@ -163,8 +163,8 @@ args:
- name: query_2
query: |
query {
test2{
id
test2_select{
test2_id
}
}
@ -184,7 +184,7 @@ args:
query: |
query {
get_test2(args: {id: 1}){
id
test2_id
}
}

View File

@ -1,5 +1,13 @@
type: bulk
args:
- type: drop_query_collection
args:
collection: collection_1
cascade: true
- type: drop_query_collection
args:
collection: collection_2
cascade: true
- type: run_sql
args:
sql: |

View File

@ -1,3 +1,4 @@
import collections
import pytest
import os
from validate import check_query_f, check_query, get_conf_f
@ -52,3 +53,65 @@ class TestOpenAPISpec:
def test_duplicate_field_name(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/openapi_get_endpoint_test_duplicate_field_name.yaml', transport)
def test_inconsistent_schema_openAPI(self, hge_ctx, transport):
# export metadata and create a backup
st_code, backup_metadata = hge_ctx.v1q(
q = {
"type": "export_metadata",
"args": {}
}
)
assert st_code == 200, backup_metadata
new_metadata = backup_metadata.copy()
#create inconsistent metadata
inconsistent_query = [collections.OrderedDict([
('name', 'wrong_queries'),
('definition', collections.OrderedDict([
('queries', [collections.OrderedDict([
('name', 'random_query'),
('query', 'query { random_field_name test_table { random_col_name } }')
])])
]))
])]
res_endpoint = [collections.OrderedDict([
("definition", collections.OrderedDict([
("query", collections.OrderedDict([
("collection_name", "wrong_queries"),
("query_name", "random_query")
]))
])),
("url", "some_url"),
("methods", ["GET"]),
("name", "wrong_endpoint"),
("comment", None)
])]
new_metadata["query_collections"] = inconsistent_query
new_metadata["rest_endpoints"] = res_endpoint
# apply inconsistent metadata
st_code, resp = hge_ctx.v1q(
q={
"type": "replace_metadata",
"version": 2,
"args": {
"allow_inconsistent_metadata": True,
"metadata": new_metadata
}
}
)
assert st_code == 200, resp
# check openAPI schema
check_query_f(hge_ctx, self.dir() + '/openapi_inconsistent_schema.yaml', transport)
# revert to old metadata
st_code, resp = hge_ctx.v1q(
q={
"type": "replace_metadata",
"args": backup_metadata
}
)
assert st_code == 200, resp