Add graphql query complexity limit

- Add query complexity limit that is calculated with directive fields in the schema
- Add complexity limit config option in the config file
pull/4/head
trivernis 5 years ago
parent 5c3ec38289
commit 54d3643e9f

@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- joined field to Event gql type - joined field to Event gql type
- joined field to Group gql type - joined field to Group gql type
- rate limits with defaults of 10/min for `/upload` and 30/min for `/graphql` - rate limits with defaults of 10/min for `/upload` and 30/min for `/graphql`
- complexity limits for graphql queries that can be configured with the `api.maxQueryComplexity` option
### Removed ### Removed

@ -3,11 +3,13 @@
# A connection uri string to the database # A connection uri string to the database
connectionUri = "sqlite://greenvironment.db" connectionUri = "sqlite://greenvironment.db"
# Configuration for the redis connection # Configuration for the redis connection
[redis] [redis]
# A connection uri string to the redis server # A connection uri string to the redis server
connectionUri = "redis://localhost:6379" connectionUri = "redis://localhost:6379"
# Configuration of the http server # Configuration of the http server
[server] [server]
# The port the server is running on # The port the server is running on
@ -17,6 +19,7 @@ cors = false
# The timeout for a server response # The timeout for a server response
timeout = 30000 timeout = 30000
# Configuration for the sessions # Configuration for the sessions
[session] [session]
# A secret that is used for the sessions. Must be secure # A secret that is used for the sessions. Must be secure
@ -24,27 +27,38 @@ secret = "REPLACE WITH SAFE RANDOM GENERATED SECRET"
# The maximum age of a cookie. The age is reset with every request of the client # The maximum age of a cookie. The age is reset with every request of the client
cookieMaxAge = 6048e+5 # 7 days cookieMaxAge = 6048e+5 # 7 days
# Configuration for markdown rendering # Configuration for markdown rendering
[markdown] [markdown]
# The plugins used in the markdown parser # The plugins used in the markdown parser
plugins = ["markdown-it-emoji"] plugins = ["markdown-it-emoji"]
# Configuration for logging # Configuration for logging
[logging] [logging]
# The loglevel # The loglevel
level = "info" level = "info"
# Configuration for the frontend files # Configuration for the frontend files
[frontend] [frontend]
# The path to the angular index file. Its relative to the public path. # The path to the angular index file. Its relative to the public path.
angularIndex = "index.html" angularIndex = "index.html"
# The path to the public files # The path to the public files
publicPath = "./public" publicPath = "./public"
# Configuration for the api # Configuration for the api
[api] [api]
# if graphiql should be enabled # if graphiql should be enabled
graphiql = true graphiql = true
# The maximum complexity of queries
maxQueryComplexity = 5000
# Configuration for the api rate limit # Configuration for the api rate limit
[api.rateLimit] [api.rateLimit]
# rate limit of /upload # rate limit of /upload

@ -23,6 +23,7 @@
"@types/bluebird": "^3.5.27", "@types/bluebird": "^3.5.27",
"@types/chai": "^4.2.7", "@types/chai": "^4.2.7",
"@types/compression": "^1.0.1", "@types/compression": "^1.0.1",
"@types/config": "^0.0.36",
"@types/connect-pg-simple": "^4.2.0", "@types/connect-pg-simple": "^4.2.0",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.6", "@types/cors": "^2.8.6",
@ -32,20 +33,20 @@
"@types/express-session": "^1.15.14", "@types/express-session": "^1.15.14",
"@types/express-socket.io-session": "^1.3.2", "@types/express-socket.io-session": "^1.3.2",
"@types/fs-extra": "^8.0.0", "@types/fs-extra": "^8.0.0",
"@types/graphql-query-complexity": "^0.2.1",
"@types/http-status": "^0.2.30", "@types/http-status": "^0.2.30",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/markdown-it": "0.0.9", "@types/markdown-it": "0.0.9",
"@types/mocha": "^5.2.7", "@types/mocha": "^5.2.7",
"@types/node": "^12.7.12", "@types/node": "^12.7.12",
"@types/pg": "^7.11.0", "@types/pg": "^7.11.0",
"@types/redis": "^2.8.14",
"@types/sequelize": "^4.28.5", "@types/sequelize": "^4.28.5",
"@types/sharp": "^0.23.1", "@types/sharp": "^0.23.1",
"@types/socket.io": "^2.1.2", "@types/socket.io": "^2.1.2",
"@types/socket.io-redis": "^1.0.25", "@types/socket.io-redis": "^1.0.25",
"@types/uuid": "^3.4.6", "@types/uuid": "^3.4.6",
"@types/validator": "^10.11.3", "@types/validator": "^10.11.3",
"@types/config": "^0.0.36",
"@types/redis": "^2.8.14",
"chai": "^4.2.0", "chai": "^4.2.0",
"delete": "^1.1.0", "delete": "^1.1.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
@ -72,6 +73,7 @@
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"graphql": "^14.4.2", "graphql": "^14.4.2",
"graphql-import": "^0.7.1", "graphql-import": "^0.7.1",
"graphql-query-complexity": "^0.4.1",
"http-status": "^1.3.2", "http-status": "^1.3.2",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"markdown-it": "^10.0.0", "markdown-it": "^10.0.0",

@ -10,6 +10,7 @@ import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra"; import * as fsx from "fs-extra";
import {buildSchema} from "graphql"; import {buildSchema} from "graphql";
import {importSchema} from "graphql-import"; import {importSchema} from "graphql-import";
import queryComplexity, {simpleEstimator, directiveEstimator} from "graphql-query-complexity";
import {IncomingMessage, ServerResponse} from "http"; import {IncomingMessage, ServerResponse} from "http";
import * as http from "http"; import * as http from "http";
import * as httpStatus from "http-status"; import * as httpStatus from "http-status";
@ -19,6 +20,7 @@ import * as redis from "redis";
import {Sequelize} from "sequelize-typescript"; import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io"; import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis"; import * as socketIoRedis from "socket.io-redis";
import {query} from "winston";
import {resolver} from "./graphql/resolvers"; import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataAccess"; import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals"; import globals from "./lib/globals";
@ -189,13 +191,28 @@ class App {
path: "/graphql", path: "/graphql",
total: config.get("api.rateLimit.graphql.total"), total: config.get("api.rateLimit.graphql.total"),
}); });
this.app.use("/graphql", graphqlHTTP((request, response) => {
// @ts-ignore
this.app.use("/graphql", graphqlHTTP(async (request, response, {variables}) => {
return { return {
// @ts-ignore all // @ts-ignore all
context: {session: request.session}, context: {session: request.session},
graphiql: config.get("api.graphiql"), graphiql: config.get("api.graphiql"),
rootValue: resolver(request, response), rootValue: resolver(request, response),
schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))), schema: buildSchema(importSchema(path.join(__dirname, "./graphql/schema.graphql"))),
validationRules: [
queryComplexity({
estimators: [
directiveEstimator(),
simpleEstimator(),
],
maximumComplexity: config.get("api.maxQueryComplexity"),
onComplete: (complexity: number) => {
logger.debug(`QueryComplexity: ${complexity}`);
},
variables,
}),
],
}; };
})); }));
// allow access to cluster information // allow access to cluster information

@ -1,3 +1,12 @@
"a directive to assign a complexity to a query field"
directive @complexity(
"The complexity value for the field"
value: Int!,
"Optional multipliers"
multipliers: [String!]
) on FIELD_DEFINITION
type Query { type Query {
"returns the user object for a given user id or a handle (only one required)" "returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID, handle: String): User getUser(userId: ID, handle: String): User
@ -18,10 +27,10 @@ type Query {
getRequest(requestId: ID!): Request getRequest(requestId: ID!): Request
"searches for users, groups, events, posts and returns a search result" "searches for users, groups, events, posts and returns a search result"
search(query: String!, first: Int = 20, offset: Int = 0): SearchResult! search(query: String!, first: Int = 20, offset: Int = 0): SearchResult! @complexity(value: 1, multipliers: ["first"])
"returns the post filtered by the sort type with pagination." "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post!]! getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post!]! @complexity(value: 1, multipliers: ["first"])
"returns all activities" "returns all activities"
getActivities: [Activity] getActivities: [Activity]
@ -121,25 +130,25 @@ interface UserData {
postCount: Int! postCount: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int=0): [Post!]! posts(first: Int=10, offset: Int=0): [Post!]! @complexity(value: 1, multipliers: ["first"])
"creation date of the user account" "creation date of the user account"
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
"The groups the user has joined" "The groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The number of groups the user has joined" "The number of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -169,7 +178,7 @@ type User implements UserData{
numberOfPosts: Int! numberOfPosts: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post!]! posts(first: Int=10, offset: Int): [Post!]! @complexity(value: 1, multipliers: ["first"])
"the number of posts the user has created" "the number of posts the user has created"
postCount: Int! postCount: Int!
@ -178,7 +187,7 @@ type User implements UserData{
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
@ -187,13 +196,13 @@ type User implements UserData{
points: Int! points: Int!
"the groups the user has joined" "the groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The numbef of groups the user has joined" "The numbef of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -213,7 +222,7 @@ type Profile implements UserData {
email: String! email: String!
"returns the chatrooms the user joined." "returns the chatrooms the user joined."
chats(first: Int=10, offset: Int): [ChatRoom] chats(first: Int=10, offset: Int): [ChatRoom] @complexity(value: 1, multipliers: ["first"])
"the count of the users chats" "the count of the users chats"
chatCount: Int! chatCount: Int!
@ -231,13 +240,13 @@ type Profile implements UserData {
postCount: Int! postCount: Int!
"returns a given number of posts of a user" "returns a given number of posts of a user"
posts(first: Int=10, offset: Int): [Post!]! posts(first: Int=10, offset: Int): [Post!]! @complexity(value: 1, multipliers: ["first"])
"creation date of the user account" "creation date of the user account"
joinedAt: String! joinedAt: String!
"all friends of the user" "all friends of the user"
friends(first: Int=10, offset: Int=0): [User!]! friends(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"The number of friends the user has" "The number of friends the user has"
friendCount: Int! friendCount: Int!
@ -255,13 +264,13 @@ type Profile implements UserData {
createdGroups: [Group!]! createdGroups: [Group!]!
"all groups the user has joined" "all groups the user has joined"
groups(first: Int=10, offset: Int=0): [Group!]! groups(first: Int=10, offset: Int=0): [Group!]! @complexity(value: 1, multipliers: ["first"])
"The numbef of groups the user has joined" "The numbef of groups the user has joined"
groupCount: Int! groupCount: Int!
"The events the user is participating in" "The events the user is participating in"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"The number of events the user is participating in" "The number of events the user is participating in"
eventCount: Int! eventCount: Int!
@ -335,10 +344,10 @@ type ChatRoom {
namespace: String namespace: String
"the members of the chatroom" "the members of the chatroom"
members(first: Int=10, offset: Int=0): [User!]! members(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"return a specfic range of messages posted in the chat" "return a specfic range of messages posted in the chat"
messages(first: Int = 10, offset: Int): [ChatMessage!]! messages(first: Int = 10, offset: Int): [ChatMessage!]! @complexity(value: 1, multipliers: ["first"])
"id of the chat" "id of the chat"
id: ID! id: ID!
@ -375,16 +384,16 @@ type Group {
creator: User! creator: User!
"all admins of the group" "all admins of the group"
admins(first: Int=10, offset: Int=0): [User!]! admins(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"the members of the group with pagination" "the members of the group with pagination"
members(first: Int = 10, offset: Int = 0): [User!]! members(first: Int = 10, offset: Int = 0): [User!]! @complexity(value: 1, multipliers: ["first"])
"the groups chat" "the groups chat"
chat: ChatRoom! chat: ChatRoom!
"the events of the group" "the events of the group"
events(first: Int=10, offset: Int=0): [Event!]! events(first: Int=10, offset: Int=0): [Event!]! @complexity(value: 1, multipliers: ["first"])
"If the user with the specified id has joined the group" "If the user with the specified id has joined the group"
joined(userId: Int): Boolean! joined(userId: Int): Boolean!
@ -404,7 +413,7 @@ type Event {
group: Group! group: Group!
"The participants of the event." "The participants of the event."
participants(first: Int=10, offset: Int=0): [User!]! participants(first: Int=10, offset: Int=0): [User!]! @complexity(value: 1, multipliers: ["first"])
"Returns if the user with the specified id has joined the event" "Returns if the user with the specified id has joined the event"
joined(userId: Int): Boolean joined(userId: Int): Boolean

@ -152,6 +152,13 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/graphql-query-complexity@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@types/graphql-query-complexity/-/graphql-query-complexity-0.2.1.tgz#5166c7f32b6cd0a24f1aad5e00ca513b82b7f0e0"
integrity sha512-PxYhF92UFagAl9UIep8seEUd9j18JardL9ZM9tOfP02fWot9ZlkBYYGFwSZ7fRE6HTva/Yr4BQem7b4P/TgDPA==
dependencies:
graphql-query-complexity "*"
"@types/http-status@^0.2.30": "@types/http-status@^0.2.30":
version "0.2.30" version "0.2.30"
resolved "https://registry.yarnpkg.com/@types/http-status/-/http-status-0.2.30.tgz#b43a1e1673b6ed9b5a28e8647862b51b6473634d" resolved "https://registry.yarnpkg.com/@types/http-status/-/http-status-0.2.30.tgz#b43a1e1673b6ed9b5a28e8647862b51b6473634d"
@ -2273,6 +2280,13 @@ graphql-import@^0.7.1:
lodash "^4.17.4" lodash "^4.17.4"
resolve-from "^4.0.0" resolve-from "^4.0.0"
graphql-query-complexity@*, graphql-query-complexity@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/graphql-query-complexity/-/graphql-query-complexity-0.4.1.tgz#06ad49de617da0d74c8196fb4a641349f104552d"
integrity sha512-Uo87hNlnJ5jwoWBkVYITbJpTrlCVwgfG5Wrfel0K1/42G+3xvud31CpsprAwiSpFIP+gCqttAx7OVmw4eTqLQQ==
dependencies:
lodash.get "^4.4.2"
graphql@^14.4.2, graphql@^14.5.3: graphql@^14.4.2, graphql@^14.5.3:
version "14.5.8" version "14.5.8"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c"
@ -3040,6 +3054,11 @@ locate-path@^3.0.0:
p-locate "^3.0.0" p-locate "^3.0.0"
path-exists "^3.0.0" path-exists "^3.0.0"
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4: lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4:
version "4.17.15" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"

Loading…
Cancel
Save