From e6ed1231cd99d946009273aa72bc52627cf538b8 Mon Sep 17 00:00:00 2001 From: ShubhamPalriwala Date: Wed, 12 Jun 2024 10:24:36 +0530 Subject: [PATCH] feat: custom rate limit for self hosters --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 4 + ...327_custom-rate-limits-for-self-hosting.ts | 28 +++ backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 1 + backend/src/db/schemas/rate-limit.ts | 26 ++ backend/src/server/app.ts | 2 +- backend/src/server/config/rateLimiter.ts | 53 ++-- backend/src/server/routes/index.ts | 7 + backend/src/server/routes/v1/index.ts | 2 + .../src/server/routes/v1/rate-limit-router.ts | 58 +++++ .../src/services/rate-limit/rate-limit-dal.ts | 7 + .../services/rate-limit/rate-limit-service.ts | 44 ++++ .../services/rate-limit/rate-limit-types.ts | 16 ++ frontend/src/hooks/api/rateLimit/index.ts | 2 + frontend/src/hooks/api/rateLimit/mutation.ts | 21 ++ frontend/src/hooks/api/rateLimit/queries.ts | 34 +++ frontend/src/hooks/api/rateLimit/types.ts | 10 + .../admin/DashboardPage/DashboardPage.tsx | 237 +++++++++++++++++- 19 files changed, 535 insertions(+), 20 deletions(-) create mode 100644 backend/src/db/migrations/20240611151327_custom-rate-limits-for-self-hosting.ts create mode 100644 backend/src/db/schemas/rate-limit.ts create mode 100644 backend/src/server/routes/v1/rate-limit-router.ts create mode 100644 backend/src/services/rate-limit/rate-limit-dal.ts create mode 100644 backend/src/services/rate-limit/rate-limit-service.ts create mode 100644 backend/src/services/rate-limit/rate-limit-types.ts create mode 100644 frontend/src/hooks/api/rateLimit/index.ts create mode 100644 frontend/src/hooks/api/rateLimit/mutation.ts create mode 100644 frontend/src/hooks/api/rateLimit/queries.ts create mode 100644 frontend/src/hooks/api/rateLimit/types.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 3f1ca94e9..81fc0c541 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -48,6 +48,7 @@ import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service"; import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service"; import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service"; +import { TRateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service"; import { TSecretServiceFactory } from "@app/services/secret/secret-service"; import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service"; import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service"; @@ -147,6 +148,7 @@ declare module "fastify" { projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; secretSharing: TSecretSharingServiceFactory; + rateLimit: TRateLimitServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 117a74e76..fbcaa3528 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -149,6 +149,9 @@ import { TProjectUserMembershipRoles, TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesUpdate, + TRateLimit, + TRateLimitInsert, + TRateLimitUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -343,6 +346,7 @@ declare module "knex/types/tables" { TSecretFolderVersionsUpdate >; [TableName.SecretSharing]: Knex.CompositeTableType; + [TableName.RateLimit]: Knex.CompositeTableType; [TableName.SecretTag]: Knex.CompositeTableType; [TableName.SecretImport]: Knex.CompositeTableType; [TableName.Integration]: Knex.CompositeTableType; diff --git a/backend/src/db/migrations/20240611151327_custom-rate-limits-for-self-hosting.ts b/backend/src/db/migrations/20240611151327_custom-rate-limits-for-self-hosting.ts new file mode 100644 index 000000000..196b8cdb3 --- /dev/null +++ b/backend/src/db/migrations/20240611151327_custom-rate-limits-for-self-hosting.ts @@ -0,0 +1,28 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.RateLimit))) { + await knex.schema.createTable(TableName.RateLimit, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.integer("readRateLimit").defaultTo(600).notNullable(); + t.integer("writeRateLimit").defaultTo(200).notNullable(); + t.integer("secretsRateLimit").defaultTo(60).notNullable(); + t.integer("authRateLimit").defaultTo(60).notNullable(); + t.integer("inviteUserRateLimit").defaultTo(30).notNullable(); + t.integer("mfaRateLimit").defaultTo(20).notNullable(); + t.integer("creationLimit").defaultTo(30).notNullable(); + t.integer("publicEndpointLimit").defaultTo(30).notNullable(); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.RateLimit); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.RateLimit); + await dropOnUpdateTrigger(knex, TableName.RateLimit); +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 1eaa86c87..5771cb669 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -48,6 +48,7 @@ export * from "./project-roles"; export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; +export * from "./rate-limit"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index f9c8436df..5d2213003 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -18,6 +18,7 @@ export enum TableName { IncidentContact = "incident_contacts", UserAction = "user_actions", SuperAdmin = "super_admin", + RateLimit = "rate_limit", ApiKey = "api_keys", Project = "projects", ProjectBot = "project_bots", diff --git a/backend/src/db/schemas/rate-limit.ts b/backend/src/db/schemas/rate-limit.ts new file mode 100644 index 000000000..86b8776cc --- /dev/null +++ b/backend/src/db/schemas/rate-limit.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const RateLimitSchema = z.object({ + id: z.string().uuid(), + readRateLimit: z.number().default(600), + writeRateLimit: z.number().default(200), + secretsRateLimit: z.number().default(60), + authRateLimit: z.number().default(60), + inviteUserRateLimit: z.number().default(30), + mfaRateLimit: z.number().default(20), + creationLimit: z.number().default(30), + publicEndpointLimit: z.number().default(30), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TRateLimit = z.infer; +export type TRateLimitInsert = Omit, TImmutableDBKeys>; +export type TRateLimitUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 51cef185a..21721bd5d 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -69,7 +69,7 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { // Rate limiters and security headers if (appCfg.isProductionMode) { - await server.register(ratelimiter, globalRateLimiterCfg()); + await server.register(ratelimiter, await globalRateLimiterCfg(db)); } await server.register(helmet, { contentSecurityPolicy: false }); diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 8c41eb2fa..48d7e41b1 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -1,22 +1,10 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import { Redis } from "ioredis"; +import { Knex } from "knex"; import { getConfig } from "@app/lib/config/env"; - -export const globalRateLimiterCfg = (): RateLimitPluginOptions => { - const appCfg = getConfig(); - const redis = appCfg.isRedisConfigured - ? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 }) - : null; - - return { - timeWindow: 60 * 1000, - max: 600, - redis, - allowList: (req) => req.url === "/healthcheck" || req.url === "/api/status", - keyGenerator: (req) => req.realIp - }; -}; +import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal"; +import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service"; // GET endpoints export const readLimit: RateLimitOptions = { @@ -74,3 +62,38 @@ export const publicEndpointLimit: RateLimitOptions = { max: 30, keyGenerator: (req) => req.realIp }; + +async function fetchRateLimitsFromDb(db: Knex) { + try { + const rateLimitDAL = rateLimitDALFactory(db); + const rateLimits = await rateLimitServiceFactory({ rateLimitDAL }).getRateLimits(); + + readLimit.max = rateLimits.readRateLimit; + publicEndpointLimit.max = rateLimits.publicEndpointLimit; + writeLimit.max = rateLimits.writeRateLimit; + secretsLimit.max = rateLimits.secretsRateLimit; + authRateLimit.max = rateLimits.authRateLimit; + inviteUserRateLimit.max = rateLimits.inviteUserRateLimit; + mfaRateLimit.max = rateLimits.mfaRateLimit; + creationLimit.max = rateLimits.creationLimit; + } catch (error) { + console.error("Error fetching rate limits:", error); + } +} + +export const globalRateLimiterCfg = async (db: Knex): Promise => { + const appCfg = getConfig(); + const redis = appCfg.isRedisConfigured + ? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 }) + : null; + + await fetchRateLimitsFromDb(db); + + return { + timeWindow: 60 * 1000, + max: 600, + redis, + allowList: (req) => req.url === "/healthcheck" || req.url === "/api/status", + keyGenerator: (req) => req.realIp + }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 00590386a..f6fe80efe 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -121,6 +121,8 @@ import { projectMembershipServiceFactory } from "@app/services/project-membershi import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal"; import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; +import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal"; +import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretQueueFactory } from "@app/services/secret/secret-queue"; @@ -185,6 +187,7 @@ export const registerRoutes = async ( const incidentContactDAL = incidentContactDALFactory(db); const orgRoleDAL = orgRoleDALFactory(db); const superAdminDAL = superAdminDALFactory(db); + const rateLimitDAL = rateLimitDALFactory(db); const apiKeyDAL = apiKeyDALFactory(db); const projectDAL = projectDALFactory(db); @@ -444,6 +447,9 @@ export const registerRoutes = async ( orgService, keyStore }); + const rateLimitService = rateLimitServiceFactory({ + rateLimitDAL + }); const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL }); const secretScanningQueue = secretScanningQueueFactory({ @@ -859,6 +865,7 @@ export const registerRoutes = async ( secret: secretService, secretReplication: secretReplicationService, secretTag: secretTagService, + rateLimit: rateLimitService, folder: folderService, secretImport: secretImportService, projectBot: projectBotService, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index cbf67ce79..fd8255e63 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -17,6 +17,7 @@ import { registerProjectEnvRouter } from "./project-env-router"; import { registerProjectKeyRouter } from "./project-key-router"; import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectRouter } from "./project-router"; +import { registerRateLimitRouter } from "./rate-limit-router"; import { registerSecretFolderRouter } from "./secret-folder-router"; import { registerSecretImportRouter } from "./secret-import-router"; import { registerSecretSharingRouter } from "./secret-sharing-router"; @@ -43,6 +44,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerPasswordRouter, { prefix: "/password" }); await server.register(registerOrgRouter, { prefix: "/organization" }); await server.register(registerAdminRouter, { prefix: "/admin" }); + await server.register(registerRateLimitRouter, { prefix: "/rate-limit" }); await server.register(registerUserRouter, { prefix: "/user" }); await server.register(registerInviteOrgRouter, { prefix: "/invite-org" }); await server.register(registerUserActionRouter, { prefix: "/user-action" }); diff --git a/backend/src/server/routes/v1/rate-limit-router.ts b/backend/src/server/routes/v1/rate-limit-router.ts new file mode 100644 index 000000000..1ea14d0b4 --- /dev/null +++ b/backend/src/server/routes/v1/rate-limit-router.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; + +import { RateLimitSchema } from "@app/db/schemas"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerRateLimitRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + rateLimit: RateLimitSchema + }) + } + }, + handler: async () => { + const rateLimit = await server.services.rateLimit.getRateLimits(); + return { rateLimit }; + } + }); + + server.route({ + method: "PATCH", + url: "/", + config: { + rateLimit: readLimit + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT, AuthMode.API_KEY])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + + schema: { + body: RateLimitSchema.omit({ + id: true, + createdAt: true, + updatedAt: true + }), + response: { + 200: z.object({ + rateLimit: RateLimitSchema + }) + } + }, + handler: async (req) => { + const rateLimit = await server.services.rateLimit.updateRateLimit(req.body); + return { rateLimit }; + } + }); +}; diff --git a/backend/src/services/rate-limit/rate-limit-dal.ts b/backend/src/services/rate-limit/rate-limit-dal.ts new file mode 100644 index 000000000..7279ff8ea --- /dev/null +++ b/backend/src/services/rate-limit/rate-limit-dal.ts @@ -0,0 +1,7 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TRateLimitDALFactory = ReturnType; + +export const rateLimitDALFactory = (db: TDbClient) => ormify(db, TableName.RateLimit, {}); diff --git a/backend/src/services/rate-limit/rate-limit-service.ts b/backend/src/services/rate-limit/rate-limit-service.ts new file mode 100644 index 000000000..d510cb056 --- /dev/null +++ b/backend/src/services/rate-limit/rate-limit-service.ts @@ -0,0 +1,44 @@ +import { BadRequestError } from "@app/lib/errors"; + +import { TRateLimitDALFactory } from "./rate-limit-dal"; +import { TRateLimit, TRateLimitUpdateDTO } from "./rate-limit-types"; + +type TRateLimitServiceFactoryDep = { + rateLimitDAL: TRateLimitDALFactory; +}; + +export type TRateLimitServiceFactory = ReturnType; + +export const rateLimitServiceFactory = ({ rateLimitDAL }: TRateLimitServiceFactoryDep) => { + const initRateLimits = async (): Promise => { + const rateLimit = await rateLimitDAL.create({}); + return rateLimit; + }; + + const getRateLimits = async (): Promise => { + let rateLimit = (await rateLimitDAL.find({}))[0]; + if (!rateLimit) { + rateLimit = await initRateLimits(); + } + return rateLimit; + }; + + const updateRateLimit = async (updates: TRateLimitUpdateDTO): Promise => { + const rateLimit = await rateLimitDAL.findOne({}); + if (!rateLimit) throw new BadRequestError({ name: "Rate Limit Update", message: "Rate Limit does not exist yet" }); + + const updateData: Record = {}; + for (const [key, value] of Object.entries(updates)) { + updateData[key] = value; + } + + const updatedRateLimit = await rateLimitDAL.updateById(rateLimit.id, updateData); + return updatedRateLimit; + }; + + return { + initRateLimits, + getRateLimits, + updateRateLimit + }; +}; diff --git a/backend/src/services/rate-limit/rate-limit-types.ts b/backend/src/services/rate-limit/rate-limit-types.ts new file mode 100644 index 000000000..19519aafb --- /dev/null +++ b/backend/src/services/rate-limit/rate-limit-types.ts @@ -0,0 +1,16 @@ +export type TRateLimitUpdateDTO = { + readRateLimit: number; + writeRateLimit: number; + secretsRateLimit: number; + authRateLimit: number; + inviteUserRateLimit: number; + mfaRateLimit: number; + creationLimit: number; + publicEndpointLimit: number; +}; + +export type TRateLimit = { + id: string; + createdAt: Date; + updatedAt: Date; +} & TRateLimitUpdateDTO; diff --git a/frontend/src/hooks/api/rateLimit/index.ts b/frontend/src/hooks/api/rateLimit/index.ts new file mode 100644 index 000000000..f3f81b1c9 --- /dev/null +++ b/frontend/src/hooks/api/rateLimit/index.ts @@ -0,0 +1,2 @@ +export { useUpdateRateLimit } from "./mutation"; +export { useGetRateLimit } from "./queries"; diff --git a/frontend/src/hooks/api/rateLimit/mutation.ts b/frontend/src/hooks/api/rateLimit/mutation.ts new file mode 100644 index 000000000..e2cccf78f --- /dev/null +++ b/frontend/src/hooks/api/rateLimit/mutation.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { rateLimitQueryKeys } from "./queries"; +import { TRateLimit } from "./types"; + +export const useUpdateRateLimit = () => { + const queryClient = useQueryClient(); + + return useMutation>({ + mutationFn: async (opt) => { + const { data } = await apiRequest.patch<{ rateLimit: TRateLimit }>("/api/v1/rate-limit", opt); + return data.rateLimit; + }, + onSuccess: (data) => { + queryClient.setQueryData(rateLimitQueryKeys.rateLimit(), data); + queryClient.invalidateQueries(rateLimitQueryKeys.rateLimit()); + } + }); +}; diff --git a/frontend/src/hooks/api/rateLimit/queries.ts b/frontend/src/hooks/api/rateLimit/queries.ts new file mode 100644 index 000000000..5a52ff74b --- /dev/null +++ b/frontend/src/hooks/api/rateLimit/queries.ts @@ -0,0 +1,34 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TRateLimit } from "./types"; + +export const rateLimitQueryKeys = { + rateLimit: () => ["rate-limit"] as const +}; + +const fetchRateLimit = async () => { + const { data } = await apiRequest.get<{ rateLimit: TRateLimit }>("/api/v1/rate-limit"); + return data.rateLimit; +}; + +export const useGetRateLimit = ({ + options = {} +}: { + options?: Omit< + UseQueryOptions< + TRateLimit, + unknown, + TRateLimit, + ReturnType + >, + "queryKey" | "queryFn" + >; +} = {}) => + useQuery({ + queryKey: rateLimitQueryKeys.rateLimit(), + queryFn: fetchRateLimit, + ...options, + enabled: options?.enabled ?? true + }); diff --git a/frontend/src/hooks/api/rateLimit/types.ts b/frontend/src/hooks/api/rateLimit/types.ts new file mode 100644 index 000000000..5697fc298 --- /dev/null +++ b/frontend/src/hooks/api/rateLimit/types.ts @@ -0,0 +1,10 @@ +export type TRateLimit = { + readRateLimit: number; + writeRateLimit: number; + secretsRateLimit: number; + authRateLimit: number; + inviteUserRateLimit: number; + mfaRateLimit: number; + creationLimit: number; + publicEndpointLimit: number; +}; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index ce1b117f3..ae243c130 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -18,12 +18,15 @@ import { Tab, TabList, TabPanel, - Tabs} from "@app/components/v2"; + Tabs +} from "@app/components/v2"; import { useOrganization, useServerConfig, useUser } from "@app/context"; import { useUpdateServerConfig } from "@app/hooks/api"; +import { useGetRateLimit, useUpdateRateLimit } from "@app/hooks/api/rateLimit"; enum TabSections { - Settings = "settings" + Settings = "settings", + RateLimit = "rate-limit" } enum SignUpModes { @@ -35,13 +38,22 @@ const formSchema = z.object({ signUpMode: z.nativeEnum(SignUpModes), allowedSignUpDomain: z.string().optional().nullable(), trustSamlEmails: z.boolean(), - trustLdapEmails: z.boolean() + trustLdapEmails: z.boolean(), + readRateLimit: z.number(), + writeRateLimit: z.number(), + secretsRateLimit: z.number(), + authRateLimit: z.number(), + inviteUserRateLimit: z.number(), + mfaRateLimit: z.number(), + creationLimit: z.number(), + publicEndpointLimit: z.number() }); type TDashboardForm = z.infer; export const AdminDashboardPage = () => { const router = useRouter(); const data = useServerConfig(); + const { data: rateLimit } = useGetRateLimit(); const { config } = data; const { @@ -56,7 +68,15 @@ export const AdminDashboardPage = () => { signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled, allowedSignUpDomain: config.allowedSignUpDomain, trustSamlEmails: config.trustSamlEmails, - trustLdapEmails: config.trustLdapEmails + trustLdapEmails: config.trustLdapEmails, + readRateLimit: rateLimit?.readRateLimit ?? 600, + writeRateLimit: rateLimit?.writeRateLimit ?? 200, + secretsRateLimit: rateLimit?.secretsRateLimit ?? 60, + authRateLimit: rateLimit?.authRateLimit ?? 60, + inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30, + mfaRateLimit: rateLimit?.mfaRateLimit ?? 20, + creationLimit: rateLimit?.creationLimit ?? 30, + publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30 } }); @@ -65,6 +85,7 @@ export const AdminDashboardPage = () => { const { user, isLoading: isUserLoading } = useUser(); const { orgs } = useOrganization(); const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); + const { mutateAsync: updateRateLimit } = useUpdateRateLimit(); const isNotAllowed = !user?.superAdmin; @@ -101,6 +122,42 @@ export const AdminDashboardPage = () => { } }; + const onRateLimitFormSubmit = async (formData: TDashboardForm) => { + try { + const { + readRateLimit, + writeRateLimit, + secretsRateLimit, + authRateLimit, + inviteUserRateLimit, + mfaRateLimit, + creationLimit, + publicEndpointLimit + } = formData; + + await updateRateLimit({ + readRateLimit, + writeRateLimit, + secretsRateLimit, + authRateLimit, + inviteUserRateLimit, + mfaRateLimit, + creationLimit, + publicEndpointLimit + }); + createNotification({ + text: "Successfully changed rate limits. Please restart your server", + type: "success" + }); + } catch (e) { + console.error(e); + createNotification({ + type: "error", + text: "Failed to update rate limiting setting." + }); + } + }; + return (
@@ -117,6 +174,7 @@ export const AdminDashboardPage = () => {
General + Rate Limit
@@ -233,6 +291,177 @@ export const AdminDashboardPage = () => { + +
+
+
+ Set Rate Limits for your Infisical Instance +
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+ +
+
)}