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 Group gql type
- 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

@ -3,11 +3,13 @@
# A connection uri string to the database
connectionUri = "sqlite://greenvironment.db"
# Configuration for the redis connection
[redis]
# A connection uri string to the redis server
connectionUri = "redis://localhost:6379"
# Configuration of the http server
[server]
# The port the server is running on
@ -17,6 +19,7 @@ cors = false
# The timeout for a server response
timeout = 30000
# Configuration for the sessions
[session]
# 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
cookieMaxAge = 6048e+5 # 7 days
# Configuration for markdown rendering
[markdown]
# The plugins used in the markdown parser
plugins = ["markdown-it-emoji"]
# Configuration for logging
[logging]
# The loglevel
level = "info"
# Configuration for the frontend files
[frontend]
# The path to the angular index file. Its relative to the public path.
angularIndex = "index.html"
# The path to the public files
publicPath = "./public"
# Configuration for the api
[api]
# if graphiql should be enabled
graphiql = true
# if graphiql should be enabled
graphiql = true
# The maximum complexity of queries
maxQueryComplexity = 5000
# Configuration for the api rate limit
[api.rateLimit]
# rate limit of /upload

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

@ -10,6 +10,7 @@ import sharedsession = require("express-socket.io-session");
import * as fsx from "fs-extra";
import {buildSchema} from "graphql";
import {importSchema} from "graphql-import";
import queryComplexity, {simpleEstimator, directiveEstimator} from "graphql-query-complexity";
import {IncomingMessage, ServerResponse} from "http";
import * as http from "http";
import * as httpStatus from "http-status";
@ -19,6 +20,7 @@ import * as redis from "redis";
import {Sequelize} from "sequelize-typescript";
import * as socketIo from "socket.io";
import * as socketIoRedis from "socket.io-redis";
import {query} from "winston";
import {resolver} from "./graphql/resolvers";
import dataaccess from "./lib/dataAccess";
import globals from "./lib/globals";
@ -189,13 +191,28 @@ class App {
path: "/graphql",
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 {
// @ts-ignore all
context: {session: request.session},
graphiql: config.get("api.graphiql"),
rootValue: resolver(request, response),
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

@ -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 {
"returns the user object for a given user id or a handle (only one required)"
getUser(userId: ID, handle: String): User
@ -18,10 +27,10 @@ type Query {
getRequest(requestId: ID!): Request
"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."
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"
getActivities: [Activity]
@ -121,25 +130,25 @@ interface UserData {
postCount: Int!
"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"
joinedAt: String!
"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"
friendCount: Int!
"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"
groupCount: Int!
"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"
eventCount: Int!
@ -169,7 +178,7 @@ type User implements UserData{
numberOfPosts: Int!
"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"
postCount: Int!
@ -178,7 +187,7 @@ type User implements UserData{
joinedAt: String!
"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"
friendCount: Int!
@ -187,13 +196,13 @@ type User implements UserData{
points: Int!
"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"
groupCount: Int!
"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"
eventCount: Int!
@ -213,7 +222,7 @@ type Profile implements UserData {
email: String!
"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"
chatCount: Int!
@ -231,13 +240,13 @@ type Profile implements UserData {
postCount: Int!
"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"
joinedAt: String!
"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"
friendCount: Int!
@ -255,13 +264,13 @@ type Profile implements UserData {
createdGroups: [Group!]!
"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"
groupCount: Int!
"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"
eventCount: Int!
@ -335,10 +344,10 @@ type ChatRoom {
namespace: String
"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"
messages(first: Int = 10, offset: Int): [ChatMessage!]!
messages(first: Int = 10, offset: Int): [ChatMessage!]! @complexity(value: 1, multipliers: ["first"])
"id of the chat"
id: ID!
@ -375,16 +384,16 @@ type Group {
creator: User!
"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"
members(first: Int = 10, offset: Int = 0): [User!]!
members(first: Int = 10, offset: Int = 0): [User!]! @complexity(value: 1, multipliers: ["first"])
"the groups chat"
chat: ChatRoom!
"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"
joined(userId: Int): Boolean!
@ -404,7 +413,7 @@ type Event {
group: Group!
"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"
joined(userId: Int): Boolean

@ -152,6 +152,13 @@
dependencies:
"@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":
version "0.2.30"
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"
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:
version "14.5.8"
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"
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:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"

Loading…
Cancel
Save