From 8b3b1bbe37656459c56f2141047c80c17b30cf4b Mon Sep 17 00:00:00 2001 From: prescientmoon <git@moonythm.dev> Date: Sat, 7 Dec 2024 20:00:09 +0100 Subject: [PATCH] Add insertonly test Moreover, this fixes the test for writeonly tokens, which used to create a table in a db, and then try querying from a separate db, which ended up not testing what it was supposed to at all. I also changed the way mutations are guarded (the end result is pretty much the same) --- source/AirGQL/GraphQL.hs | 139 +++++++++++++------------------ tests/Tests/IntrospectionSpec.hs | 117 ++++++++++++++++++-------- tests/Tests/MutationSpec.hs | 11 ++- 3 files changed, 150 insertions(+), 117 deletions(-) diff --git a/source/AirGQL/GraphQL.hs b/source/AirGQL/GraphQL.hs index b13038c..6b2a4ae 100644 --- a/source/AirGQL/GraphQL.hs +++ b/source/AirGQL/GraphQL.hs @@ -36,7 +36,6 @@ import Protolude ( (&), (&&), (.), - (<$>), (<&>), (<=), (>), @@ -814,6 +813,9 @@ queryType connection accessMode dbId tables = do , Introspection.typeNameResolver , resolvers ] + -- TODO: is it better to wrap the resolvers here, + -- or to just return an empty list of resolvers + -- when given a token that cannot read? <&> wrapResolver requireRead } @@ -1074,94 +1076,73 @@ mutationType connection maxRowsPerTable accessMode dbId tables = do numOfChanges <- SS.changes connection mutationByPKResponse table numOfChanges $ P.head deletedRows - getMutationResolvers :: IO (HashMap.HashMap Text (Resolver IO)) - getMutationResolvers = do - let - getInsertTableTuple :: TableEntry -> IO (Text, Resolver IO) - getInsertTableTuple table = - makeResolver - (Introspection.tableInsertField accessMode table) - (executeDbInserts table) + getInsertTableTuple :: TableEntry -> IO (Text, Resolver IO) + getInsertTableTuple table = + makeResolver + (Introspection.tableInsertField accessMode table) + (executeDbInserts table) - getUpdateTableTuple :: TableEntry -> IO (Text, Resolver IO) - getUpdateTableTuple table = - makeResolver - (Introspection.tableUpdateField accessMode table) - (executeDbUpdates table) + getUpdateTableTuple :: TableEntry -> IO (Text, Resolver IO) + getUpdateTableTuple table = + makeResolver + (Introspection.tableUpdateField accessMode table) + (executeDbUpdates table) - getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO)) - getUpdateByPKTableTuple table = - P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $ - \field -> makeResolver field (executeDbUpdatesByPK table) + getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO)) + getUpdateByPKTableTuple table = + P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $ + \field -> makeResolver field (executeDbUpdatesByPK table) - getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO) - getDeleteTableTuple table = - makeResolver - (Introspection.tableDeleteField accessMode table) - (executeDbDeletions table) + getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO) + getDeleteTableTuple table = + makeResolver + (Introspection.tableDeleteField accessMode table) + (executeDbDeletions table) - getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO)) - getDeleteByPKTableTuple table = - P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $ - \field -> makeResolver field (executeDbDeletionsByPK table) + getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO)) + getDeleteByPKTableTuple table = + P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $ + \field -> makeResolver field (executeDbDeletionsByPK table) - tablesWithoutViews :: [TableEntry] - tablesWithoutViews = - List.filter - (\table -> table.object_type == Table) - tables + tablesWithoutViews :: [TableEntry] + tablesWithoutViews = + List.filter + (\table -> table.object_type == Table) + tables - insertTuples <- - P.fold - [ P.for tablesWithoutViews getInsertTableTuple - ] + insertTuples <- + P.fold + [ P.for tablesWithoutViews getInsertTableTuple + ] - writeTuples <- - P.fold - [ P.for tablesWithoutViews getUpdateTableTuple - , P.for tablesWithoutViews getDeleteTableTuple - , P.for tablesWithoutViews getUpdateByPKTableTuple - <&> P.catMaybes - , P.for tablesWithoutViews getDeleteByPKTableTuple - <&> P.catMaybes - ] + writeTuples <- + P.fold + [ P.for tablesWithoutViews getUpdateTableTuple + , P.for tablesWithoutViews getDeleteTableTuple + , P.for tablesWithoutViews getUpdateByPKTableTuple + <&> P.catMaybes + , P.for tablesWithoutViews getDeleteByPKTableTuple + <&> P.catMaybes + ] - let - requireWrite :: Out.Resolve IO -> Out.Resolve IO - requireWrite resolve = do - when (P.not $ canWrite accessMode) $ do - throw $ - ResolverException $ - userError "Cannot write field using the provided token" - resolve + let + insertResolvers = + if canInsert accessMode + then HashMap.fromList insertTuples + else mempty - requireInsert :: Out.Resolve IO -> Out.Resolve IO - requireInsert resolve = do - when (P.not $ canInsert accessMode) $ do - throw $ - ResolverException $ - userError "Cannot insert entries using the provided token" - resolve + writeResolvers = + if canWrite accessMode + then HashMap.fromList writeTuples + else mempty - insertResolvers = - HashMap.fromList insertTuples - <&> wrapResolver requireInsert - - writeResolvers = - HashMap.fromList writeTuples - <&> wrapResolver requireWrite - - pure $ insertResolvers <> writeResolvers - - if canWrite accessMode - then - Just - . Out.ObjectType - "Mutation" - Nothing - [] - <$> getMutationResolvers - else pure Nothing + pure + $ Just + $ Out.ObjectType + "Mutation" + Nothing + [] + $ insertResolvers <> writeResolvers -- | Automatically generated schema derived from the SQLite database diff --git a/tests/Tests/IntrospectionSpec.hs b/tests/Tests/IntrospectionSpec.hs index d6141a5..e86eba1 100644 --- a/tests/Tests/IntrospectionSpec.hs +++ b/tests/Tests/IntrospectionSpec.hs @@ -24,7 +24,7 @@ import System.FilePath ((</>)) import Test.Hspec (Spec, describe, it, shouldBe) import AirGQL.GraphQL (getDerivedSchema) -import AirGQL.Lib (getEnrichedTables, writeOnly) +import AirGQL.Lib (getEnrichedTables, insertOnly, writeOnly) import AirGQL.Raw (raw) import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf) import AirGQL.Utils (withRetryConn) @@ -181,7 +181,7 @@ main = void $ do "data": null, "errors": [{ "locations": [{ "column":3, "line":2 }], - "message": "user error (Cannot read field using writeonly access code)", + "message": "user error (Cannot read field using the provided token)", "path": ["__schema"] }] } @@ -644,41 +644,88 @@ main = void $ do ) |] - let - query :: Text - query = - [gql| - mutation items { - update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) { - returning { id } - } - } - |] - - expected = - rmSpaces - [raw| - { - "data": null, - "errors": [{ - "locations": [{ "column":3, "line":2 }], - "message": "Cannot query field \"update_items\" on type \"Mutation\"." - }] + let + query :: Text + query = + [gql| + mutation items { + update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) { + returning { id } + } } |] - schema <- withRetryConn dbPath $ \conn -> do + expected = + rmSpaces + [raw| + { + "data": null, + "errors": [{ + "locations": [{ "column":5, "line":3 }], + "message": "Cannot query field \"returning\" on type \"items_mutation_response\"." + }] + } + |] + Right tables <- getEnrichedTables conn - getDerivedSchema - defaultSchemaConf{accessMode = writeOnly} + schema <- + getDerivedSchema + defaultSchemaConf{accessMode = writeOnly} + conn + fixtureDbId + tables + + Right response <- + graphql schema Nothing mempty query + + Ae.encode response `shouldBe` expected + + it "doesn't allow insertonly tokens to update data" $ do + let dbName = "no-insertonly-return.db" + withTestDbConn dbName $ \conn -> do + SS.execute_ conn - fixtureDbId - tables + [sql| + CREATE TABLE items ( + id INTEGER PRIMARY KEY + ) + |] - Right response <- - graphql schema Nothing mempty query + let + query :: Text + query = + [gql| + mutation items { + update_items_by_pk(id: 0, set: { id: 0 }) { + affected_rows + } + } + |] - Ae.encode response `shouldBe` expected + expected = + rmSpaces + [raw| + { + "data": null, + "errors": [{ + "locations": [{ "column":3, "line":2 }], + "message": "Cannot query field \"update_items_by_pk\" on type \"Mutation\"." + }] + } + |] + + Right tables <- getEnrichedTables conn + schema <- + getDerivedSchema + defaultSchemaConf{accessMode = insertOnly} + conn + fixtureDbId + tables + + Right response <- + graphql schema Nothing mempty query + + Ae.encode response `shouldBe` expected describe "Naming conflicts" $ do it "appends _ at the end of queries to avoid conflicts with table names" $ do @@ -747,32 +794,32 @@ main = void $ do "__schema": { "mutationType": { "fields": [ - { + { "name": "insert_foo", "args": [ { "name": "objects" }, { "name": "on_conflict" } ] }, - { + { "name": "update_foo", "args": [ { "name": "set" }, { "name": "filter" } ] }, - { + { "name": "update_foo_by_pk", "args": [ { "name": "set" }, { "name": "set_" } ] }, - { + { "name": "delete_foo", "args": [{ "name": "filter" }] }, - { + { "name": "delete_foo_by_pk", "args": [{ "name": "set" }] } diff --git a/tests/Tests/MutationSpec.hs b/tests/Tests/MutationSpec.hs index 49a7b64..b2b8d89 100644 --- a/tests/Tests/MutationSpec.hs +++ b/tests/Tests/MutationSpec.hs @@ -24,11 +24,11 @@ import System.FilePath ((</>)) import Test.Hspec (Spec, describe, it, shouldBe) import AirGQL.GraphQL (getDerivedSchema) -import AirGQL.Lib (SQLPost (SQLPost, query), getEnrichedTables) +import AirGQL.Lib (SQLPost (SQLPost, query), getEnrichedTables, insertOnly) import AirGQL.Raw (raw) import AirGQL.Servant.SqlQuery (sqlQueryPostHandler) import AirGQL.Types.PragmaConf qualified as PragmaConf -import AirGQL.Types.SchemaConf (defaultSchemaConf) +import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf) import AirGQL.Types.SqlQueryPostResult ( SqlQueryPostResult (rows), ) @@ -75,7 +75,12 @@ main = void $ do conn <- SS.open dbPath Right tables <- getEnrichedTables conn - schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables + schema <- + getDerivedSchema + defaultSchemaConf{accessMode = insertOnly} + conn + fixtureDbId + tables Right result <- graphql schema Nothing mempty query Ae.encode result `shouldBe` expected