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 index 196b8cdb3..32bb55a61 100644 --- 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 @@ -6,7 +6,7 @@ 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.uuid("id", { primaryKey: true }).defaultTo("00000000-0000-0000-0000-000000000000"); t.integer("readRateLimit").defaultTo(600).notNullable(); t.integer("writeRateLimit").defaultTo(200).notNullable(); t.integer("secretsRateLimit").defaultTo(60).notNullable(); diff --git a/backend/src/server/app.ts b/backend/src/server/app.ts index 21721bd5d..fe94d7ac0 100644 --- a/backend/src/server/app.ts +++ b/backend/src/server/app.ts @@ -17,6 +17,8 @@ import { Logger } from "pino"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; +import { rateLimitDALFactory } from "@app/services/rate-limit/rate-limit-dal"; +import { rateLimitServiceFactory } from "@app/services/rate-limit/rate-limit-service"; import { TSmtpService } from "@app/services/smtp/smtp-service"; import { globalRateLimiterCfg } from "./config/rateLimiter"; @@ -69,8 +71,11 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => { // Rate limiters and security headers if (appCfg.isProductionMode) { - await server.register(ratelimiter, await globalRateLimiterCfg(db)); + const rateLimitDAL = rateLimitDALFactory(db); + const rateLimits = await rateLimitServiceFactory({ rateLimitDAL }).getRateLimits(); + await server.register(ratelimiter, globalRateLimiterCfg(rateLimits)); } + await server.register(helmet, { contentSecurityPolicy: false }); await server.register(maintenanceMode); diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 48d7e41b1..cc62fd08c 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -1,10 +1,8 @@ import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; import { Redis } from "ioredis"; -import { Knex } from "knex"; +import { TRateLimit } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -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 = { @@ -63,31 +61,20 @@ export const publicEndpointLimit: RateLimitOptions = { 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 => { +export const globalRateLimiterCfg = async (rateLimits: TRateLimit): Promise => { const appCfg = getConfig(); const redis = appCfg.isRedisConfigured ? new Redis(appCfg.REDIS_URL, { connectTimeout: 500, maxRetriesPerRequest: 1 }) : null; - await fetchRateLimitsFromDb(db); + 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; return { timeWindow: 60 * 1000, diff --git a/backend/src/server/routes/v1/rate-limit-router.ts b/backend/src/server/routes/v1/rate-limit-router.ts index 1ea14d0b4..be38afb73 100644 --- a/backend/src/server/routes/v1/rate-limit-router.ts +++ b/backend/src/server/routes/v1/rate-limit-router.ts @@ -39,10 +39,15 @@ export const registerRateLimitRouter = async (server: FastifyZodProvider) => { }, schema: { - body: RateLimitSchema.omit({ - id: true, - createdAt: true, - updatedAt: true + body: z.object({ + readRateLimit: z.number().optional(), + writeRateLimit: z.number().optional(), + secretsRateLimit: z.number().optional(), + authRateLimit: z.number().optional(), + inviteUserRateLimit: z.number().optional(), + mfaRateLimit: z.number().optional(), + creationLimit: z.number().optional(), + publicEndpointLimit: z.number().optional() }), response: { 200: z.object({ diff --git a/backend/src/services/rate-limit/rate-limit-service.ts b/backend/src/services/rate-limit/rate-limit-service.ts index d510cb056..4d4511127 100644 --- a/backend/src/services/rate-limit/rate-limit-service.ts +++ b/backend/src/services/rate-limit/rate-limit-service.ts @@ -10,34 +10,23 @@ type TRateLimitServiceFactoryDep = { 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; + return rateLimitDAL.findOne({ id: "00000000-0000-0000-0000-000000000000" }); }; 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 rateLimit = await rateLimitDAL.findOne({ + id: "00000000-0000-0000-0000-000000000000" + }); - const updateData: Record = {}; - for (const [key, value] of Object.entries(updates)) { - updateData[key] = value; + if (!rateLimit) { + throw new BadRequestError({ name: "Rate Limit Update", message: "Rate Limit does not exist yet" }); } - const updatedRateLimit = await rateLimitDAL.updateById(rateLimit.id, updateData); - return updatedRateLimit; + return rateLimitDAL.updateById(rateLimit.id, updates); }; return { - initRateLimits, getRateLimits, updateRateLimit }; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 61e8cb666..19fd5f594 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -17,6 +17,7 @@ export * from "./keys"; export * from "./ldapConfig"; export * from "./organization"; export * from "./projectUserAdditionalPrivilege"; +export * from "./rateLimit"; export * from "./roles"; export * from "./scim"; export * from "./secretApproval"; diff --git a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx index ae243c130..52fddb22c 100644 --- a/frontend/src/views/admin/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/admin/DashboardPage/DashboardPage.tsx @@ -22,7 +22,8 @@ import { } 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"; + +import { RateLimitPanel } from "./RateLimitPanel"; enum TabSections { Settings = "settings", @@ -38,22 +39,13 @@ const formSchema = z.object({ signUpMode: z.nativeEnum(SignUpModes), allowedSignUpDomain: z.string().optional().nullable(), trustSamlEmails: 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() + trustLdapEmails: z.boolean() }); type TDashboardForm = z.infer; export const AdminDashboardPage = () => { const router = useRouter(); const data = useServerConfig(); - const { data: rateLimit } = useGetRateLimit(); const { config } = data; const { @@ -68,15 +60,7 @@ export const AdminDashboardPage = () => { signUpMode: config.allowSignUp ? SignUpModes.Anyone : SignUpModes.Disabled, allowedSignUpDomain: config.allowedSignUpDomain, trustSamlEmails: config.trustSamlEmails, - 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 + trustLdapEmails: config.trustLdapEmails } }); @@ -85,7 +69,6 @@ export const AdminDashboardPage = () => { const { user, isLoading: isUserLoading } = useUser(); const { orgs } = useOrganization(); const { mutateAsync: updateServerConfig } = useUpdateServerConfig(); - const { mutateAsync: updateRateLimit } = useUpdateRateLimit(); const isNotAllowed = !user?.superAdmin; @@ -122,42 +105,6 @@ 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 (
@@ -292,175 +239,7 @@ 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))} - /> - - )} - /> -
- -
+
diff --git a/frontend/src/views/admin/DashboardPage/RateLimitPanel.tsx b/frontend/src/views/admin/DashboardPage/RateLimitPanel.tsx new file mode 100644 index 000000000..f29a9d52b --- /dev/null +++ b/frontend/src/views/admin/DashboardPage/RateLimitPanel.tsx @@ -0,0 +1,248 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useGetRateLimit, useUpdateRateLimit } from "@app/hooks/api"; + +const formSchema = z.object({ + 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 TRateLimitForm = z.infer; + +export const RateLimitPanel = () => { + const { data: rateLimit } = useGetRateLimit(); + const { mutateAsync: updateRateLimit } = useUpdateRateLimit(); + + const { + control, + handleSubmit, + formState: { isSubmitting, isDirty } + } = useForm({ + resolver: zodResolver(formSchema), + values: { + // eslint-disable-next-line + 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 + } + }); + + const onRateLimitFormSubmit = async (formData: TRateLimitForm) => { + 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 ( +
+
+
+ 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))} + /> + + )} + /> +
+ +
+ ); +};