mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
feat: provide a way to bypass redis based rate limits (#7726)
This commit is contained in:
parent
31246ebd4a
commit
0ab6551c0a
6 changed files with 33 additions and 5 deletions
|
|
@ -98,6 +98,7 @@ export function deployGraphQL({
|
|||
const supertokensSecrets = new ServiceSecret('supertokens-at-home', {
|
||||
refreshTokenKey: supertokensConfig.requireSecret('refreshTokenKey'),
|
||||
accessTokenKey: supertokensConfig.requireSecret('accessTokenKey'),
|
||||
bypassRateLimitKey: supertokensConfig.requireSecret('bypassRateLimitKey'),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -224,6 +225,7 @@ export function deployGraphQL({
|
|||
// Supertokens
|
||||
.withSecret('SUPERTOKENS_REFRESH_TOKEN_KEY', supertokensSecrets, 'refreshTokenKey')
|
||||
.withSecret('SUPERTOKENS_ACCESS_TOKEN_KEY', supertokensSecrets, 'accessTokenKey')
|
||||
.withSecret('SUPERTOKENS_RATE_LIMIT_BYPASS_KEY', supertokensSecrets, 'bypassRateLimitKey')
|
||||
// Zendesk
|
||||
.withConditionalSecret(zendesk.enabled, 'ZENDESK_SUBDOMAIN', zendesk.secret, 'subdomain')
|
||||
.withConditionalSecret(zendesk.enabled, 'ZENDESK_USERNAME', zendesk.secret, 'username')
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ export function createRegistry({
|
|||
baseUrl: string;
|
||||
rateLimit: null | {
|
||||
ipHeaderName: string;
|
||||
bypassKey: string | null;
|
||||
};
|
||||
} | null;
|
||||
schemaConfig: SchemaModuleConfig;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { timingSafeEqual } from 'node:crypto';
|
||||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import { type Redis } from 'ioredis';
|
||||
import { type FastifyRequest } from '@hive/service-common';
|
||||
import { sha256 } from '../../auth/lib/supertokens-at-home/crypto';
|
||||
import { Logger } from './logger';
|
||||
import { REDIS_INSTANCE } from './redis';
|
||||
import { RateLimitConfig } from './tokens';
|
||||
|
|
@ -9,7 +11,8 @@ import { RateLimitConfig } from './tokens';
|
|||
scope: Scope.Singleton,
|
||||
})
|
||||
export class RedisRateLimiter {
|
||||
logger: Logger;
|
||||
private logger: Logger;
|
||||
private bypassKey: Buffer | null;
|
||||
|
||||
constructor(
|
||||
@Inject(REDIS_INSTANCE) private redis: Redis,
|
||||
|
|
@ -17,6 +20,7 @@ export class RedisRateLimiter {
|
|||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ module: 'RateLimiter' });
|
||||
this.bypassKey = config.config?.bypassKey ? Buffer.from(config.config.bypassKey) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -34,6 +38,20 @@ export class RedisRateLimiter {
|
|||
return false;
|
||||
}
|
||||
|
||||
const cookies: undefined | Record<string, undefined | string> = (req as any).cookies;
|
||||
|
||||
if (this.bypassKey !== null && cookies?.['sBypassRateLimitKey']) {
|
||||
const incomingBypassKey = Buffer.from(cookies['sBypassRateLimitKey']);
|
||||
|
||||
if (
|
||||
this.bypassKey.length === incomingBypassKey.length &&
|
||||
timingSafeEqual(incomingBypassKey, this.bypassKey)
|
||||
) {
|
||||
this.logger.debug('rate limit bypassed via provided key');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
req.routeOptions.url;
|
||||
|
||||
let ip = req.ip;
|
||||
|
|
@ -46,18 +64,20 @@ export class RedisRateLimiter {
|
|||
ip = req.headers[this.config.config.ipHeaderName] as string;
|
||||
}
|
||||
|
||||
const key = `server-rate-limiter:${req.routeOptions.url}:${ip}`;
|
||||
const ipHash = sha256(ip);
|
||||
|
||||
const key = `server-rate-limiter:${req.routeOptions.url}:${ipHash}`;
|
||||
|
||||
const current = await this.redis.incr(key);
|
||||
if (current === 1) {
|
||||
await this.redis.expire(key, timeWindowSeconds);
|
||||
}
|
||||
if (current > maxActionsPerTimeWindow) {
|
||||
this.logger.debug('request is rate limited');
|
||||
this.logger.debug('request is rate limited (ip_hash=%s)', ipHash);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.debug('request is not rate limited');
|
||||
this.logger.debug('request is not rate limited (ip_hash=%s)', ipHash);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export class RateLimitConfig {
|
|||
constructor(
|
||||
public readonly config: null | {
|
||||
ipHeaderName: string;
|
||||
bypassKey: string | null;
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ const SuperTokensModel = zod.union([
|
|||
SUPERTOKENS_API_KEY: zod.string(),
|
||||
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()),
|
||||
}),
|
||||
zod.object({
|
||||
SUPERTOKENS_AT_HOME: zod.literal('1'),
|
||||
|
|
@ -113,6 +114,7 @@ const SuperTokensModel = zod.union([
|
|||
SUPERTOKENS_ACCESS_TOKEN_KEY: zod.string(),
|
||||
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
@ -455,6 +457,7 @@ export const env = {
|
|||
: {
|
||||
ipHeaderName:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
|
||||
bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null,
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
|
@ -467,6 +470,7 @@ export const env = {
|
|||
: {
|
||||
ipHeaderName:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
|
||||
bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null,
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ export async function registerSupertokensAtHome(
|
|||
return rep.status(401).send();
|
||||
}
|
||||
|
||||
const refreshToken = req.cookies['sRefreshToken'] ?? null;
|
||||
const refreshToken = req.cookies?.['sRefreshToken'] ?? null;
|
||||
|
||||
if (!refreshToken) {
|
||||
req.log.debug('No refresh token provided.');
|
||||
|
|
|
|||
Loading…
Reference in a new issue