Skip to content
This repository has been archived by the owner on Oct 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #4540 from withspectrum/gql-rate-limit
Browse files Browse the repository at this point in the history
Implement GraphQL Rate Limiting
  • Loading branch information
brianlovin authored Jan 17, 2019
2 parents f9f83ff + cfb8af2 commit b3124ed
Show file tree
Hide file tree
Showing 16 changed files with 313 additions and 6 deletions.
1 change: 1 addition & 0 deletions api/apollo-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const server = new ProtectedApolloServer({
new Promise((res, rej) =>
req.login(data, err => (err ? rej(err) : res()))
),
req,
user: currentUser,
};
},
Expand Down
3 changes: 3 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"graphql-date": "^1.0.3",
"graphql-depth-limit": "^1.1.0",
"graphql-log": "^0.1.3",
"graphql-rate-limit": "^1.1.0",
"graphql-tools": "^4.0.3",
"helmet": "^3.15.0",
"highlight.js": "^9.13.1",
Expand All @@ -78,6 +79,7 @@
"lodash.intersection": "^4.4.0",
"longjohn": "^0.2.12",
"moment": "^2.23.0",
"ms": "^2.1.1",
"node-env-file": "^0.1.8",
"node-localstorage": "^1.3.1",
"now-env": "^3.1.0",
Expand Down Expand Up @@ -111,6 +113,7 @@
"redraft": "0.8.0",
"redux": "^3.6.0",
"redux-thunk": "^2.3.0",
"request-ip": "^2.1.3",
"rethinkdb-changefeed-reconnect": "^0.3.2",
"rethinkdb-inspector": "^0.3.3",
"rethinkdb-migrate": "^1.4.0",
Expand Down
16 changes: 16 additions & 0 deletions api/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,18 @@ const notificationSubscriptions = require('./subscriptions/notification');
const directMessageThreadSubscriptions = require('./subscriptions/directMessageThread');
const threadSubscriptions = require('./subscriptions/thread');

const rateLimit = require('./utils/rate-limit-directive').default;

const IS_PROD = process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV;

const Root = /* GraphQL */ `
directive @rateLimit(
max: Int
window: Int
message: String
identityArgs: [String]
) on FIELD_DEFINITION
# The dummy queries and mutations are necessary because
# graphql-js cannot have empty root types and we only extend
# these types later on
Expand Down Expand Up @@ -149,6 +160,11 @@ const schema = makeExecutableSchema({
Search,
],
resolvers,
schemaDirectives: IS_PROD
? {
rateLimit,
}
: {},
});

if (process.env.REACT_APP_MAINTENANCE_MODE === 'enabled') {
Expand Down
1 change: 1 addition & 0 deletions api/types/Channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const Channel = /* GraphQL */ `
extend type Mutation {
createChannel(input: CreateChannelInput!): Channel
@rateLimit(max: 10, window: "10m")
editChannel(input: EditChannelInput!): Channel
deleteChannel(channelId: ID!): Boolean
toggleChannelSubscription(channelId: ID!): Channel
Expand Down
1 change: 1 addition & 0 deletions api/types/Community.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ const Community = /* GraphQL */ `
extend type Mutation {
createCommunity(input: CreateCommunityInput!): Community
@rateLimit(max: 3, window: "15m")
editCommunity(input: EditCommunityInput!): Community
deleteCommunity(communityId: ID!): Boolean
toggleCommunityMembership(communityId: ID!): Community
Expand Down
2 changes: 1 addition & 1 deletion api/types/DirectMessageThread.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const DirectMessageThread = /* GraphQL */ `
extend type Mutation {
createDirectMessageThread(
input: DirectMessageThreadInput!
): DirectMessageThread
): DirectMessageThread @rateLimit(max: 10, window: "20m")
setLastSeen(id: ID!): DirectMessageThread
}
Expand Down
1 change: 1 addition & 0 deletions api/types/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const Message = /* GraphQL */ `
extend type Mutation {
addMessage(message: MessageInput!): Message
@rateLimit(max: 30, window: "1m")
deleteMessage(id: ID!): Boolean
editMessage(input: EditMessageInput!): Message
}
Expand Down
1 change: 1 addition & 0 deletions api/types/Thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const Thread = /* GraphQL */ `
extend type Mutation {
publishThread(thread: ThreadInput!): Thread
@rateLimit(max: 7, window: "10m")
editThread(input: EditThreadInput!): Thread
setThreadLock(threadId: ID!, value: Boolean!): Thread
toggleThreadNotifications(threadId: ID!): Thread
Expand Down
8 changes: 4 additions & 4 deletions api/utils/create-graphql-error-formatter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
const debug = require('debug')('api:utils:error-formatter');
import { RateLimitError } from 'graphql-rate-limit';
import Raven from 'shared/raven';
import { IsUserError } from './UserError';
import type { GraphQLError } from 'graphql';
Expand Down Expand Up @@ -45,12 +46,11 @@ const createGraphQLErrorFormatter = (req?: express$Request) => (
) => {
logGraphQLError(req, error);

const isUserError = error.originalError
? error.originalError[IsUserError]
: error[IsUserError];
const err = error.originalError || error;
const isUserError = err[IsUserError] || err instanceof RateLimitError;

let sentryId = 'ID only generated in production';
if (!isUserError) {
if (!isUserError || err instanceof RateLimitError) {
if (process.env.NODE_ENV === 'production') {
sentryId = Raven.captureException(
error,
Expand Down
15 changes: 15 additions & 0 deletions api/utils/rate-limit-directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @flow
import { createRateLimitDirective, RedisStore } from 'graphql-rate-limit';
import { getClientIp } from 'request-ip';
import createRedis from 'shared/bull/create-redis';
import ms from 'ms';

export default createRateLimitDirective({
identifyContext: ctx => (ctx.user && ctx.user.id) || getClientIp(ctx.request),
store: new RedisStore(createRedis()),
formatError: ({ fieldName, fieldIdentity, max, window }) =>
`Slow down there partner! You've called '${fieldName}' ${max} times in the past ${ms(
window,
{ long: true }
)}. Relax for a bit and try again later.`,
});
35 changes: 35 additions & 0 deletions api/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,20 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==

"@types/redis-mock@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@types/redis-mock/-/redis-mock-0.17.0.tgz#13c56c8cf3f748ae1656efc2da9bd6a97cc38e4d"
integrity sha512-UDKHu9otOSE1fPjgn0H7UoggqVyuRYfo3WJpdXdVmzgGmr1XIM/dTk/gRYf/bLjIK5mxpV8inA5uNBS2sVOilA==
dependencies:
"@types/redis" "*"

"@types/redis@*":
version "2.8.10"
resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.10.tgz#27130c3bcc803265a51cb978538ca330dff2e748"
integrity sha512-X0NeV3ivoif2SPsmuPhwTkKfcr1fDJlaJIOyxZ9/TCIEbvzMzmZlstqCZ5r7AOolbOJtAfvuGArNXMexYYH3ng==
dependencies:
"@types/node" "*"

"@types/serve-static@*":
version "1.13.2"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
Expand Down Expand Up @@ -4582,6 +4596,15 @@ graphql-log@^0.1.3:
deep-for-each "^1.0.6"
is-function "^1.0.1"

graphql-rate-limit@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/graphql-rate-limit/-/graphql-rate-limit-1.1.0.tgz#209462f8f978e820a4c8e5b61622277e246388a5"
integrity sha512-xBEmAu2pE3coAqEhG5WbGnn9FxP9T40SpSpuej96hfgdy+ZhZytxLQphl5pN1mERjGR2L+nzPkZ/l4EaE787OA==
dependencies:
"@types/redis-mock" "^0.17.0"
graphql-tools "^4.0.3"
ms "^2.1.1"

graphql-subscriptions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8"
Expand Down Expand Up @@ -5416,6 +5439,11 @@ is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==

is_js@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d"
integrity sha1-CrlFQFArp6+iTIVqqYVWFmnpxS0=

[email protected]:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
Expand Down Expand Up @@ -8181,6 +8209,13 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"

request-ip@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.1.3.tgz#99ab2bafdeaf2002626e28083cb10597511d9e14"
integrity sha512-J3qdE/IhVM3BXkwMIVO4yFrvhJlU3H7JH16+6yHucadT4fePnR8dyh+vEs6FIx0S2x5TCt2ptiPfHcn0sqhbYQ==
dependencies:
is_js "^0.9.0"

request@^2.79.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
Expand Down
130 changes: 130 additions & 0 deletions flow-typed/npm/graphql-rate-limit_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// flow-typed signature: 690cd5de27aeb0c40ca045d36dffdc7b
// flow-typed version: <<STUB>>/graphql-rate-limit_vx.x/flow_v0.66.0

/**
* This is an autogenerated libdef stub for:
*
* 'graphql-rate-limit'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/

declare module 'graphql-rate-limit' {
declare module.exports: any;
}

/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'graphql-rate-limit/build/main/index' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/field-directive' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/in-memory-store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/rate-limit-error' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/redis-store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/main/lib/types' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/index' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/field-directive' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/in-memory-store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/rate-limit-error' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/redis-store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/store' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/build/module/lib/types' {
declare module.exports: any;
}

declare module 'graphql-rate-limit/example/index' {
declare module.exports: any;
}

// Filename aliases
declare module 'graphql-rate-limit/build/main/index.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/index'>;
}
declare module 'graphql-rate-limit/build/main/lib/field-directive.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/field-directive'>;
}
declare module 'graphql-rate-limit/build/main/lib/in-memory-store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/in-memory-store'>;
}
declare module 'graphql-rate-limit/build/main/lib/rate-limit-error.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/rate-limit-error'>;
}
declare module 'graphql-rate-limit/build/main/lib/redis-store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/redis-store'>;
}
declare module 'graphql-rate-limit/build/main/lib/store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/store'>;
}
declare module 'graphql-rate-limit/build/main/lib/types.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/main/lib/types'>;
}
declare module 'graphql-rate-limit/build/module/index.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/index'>;
}
declare module 'graphql-rate-limit/build/module/lib/field-directive.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/field-directive'>;
}
declare module 'graphql-rate-limit/build/module/lib/in-memory-store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/in-memory-store'>;
}
declare module 'graphql-rate-limit/build/module/lib/rate-limit-error.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/rate-limit-error'>;
}
declare module 'graphql-rate-limit/build/module/lib/redis-store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/redis-store'>;
}
declare module 'graphql-rate-limit/build/module/lib/store.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/store'>;
}
declare module 'graphql-rate-limit/build/module/lib/types.js' {
declare module.exports: $Exports<'graphql-rate-limit/build/module/lib/types'>;
}
declare module 'graphql-rate-limit/example/index.js' {
declare module.exports: $Exports<'graphql-rate-limit/example/index'>;
}
33 changes: 33 additions & 0 deletions flow-typed/npm/ms_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// flow-typed signature: 920f2fd7d370e63b28ba6b9002a0ad7c
// flow-typed version: <<STUB>>/ms_vx.x.x/flow_v0.66.0

/**
* This is an autogenerated libdef stub for:
*
* 'ms'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/

declare module 'ms' {
declare module.exports: any;
}

/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/


// Filename aliases
declare module 'ms/index' {
declare module.exports: $Exports<'ms'>;
}
declare module 'ms/index.js' {
declare module.exports: $Exports<'ms'>;
}
Loading

0 comments on commit b3124ed

Please sign in to comment.