diff --git a/backend-pg/src/@types/fastify.d.ts b/backend-pg/src/@types/fastify.d.ts index 9e079efd8..5d8d32700 100644 --- a/backend-pg/src/@types/fastify.d.ts +++ b/backend-pg/src/@types/fastify.d.ts @@ -106,7 +106,6 @@ declare module "fastify" { license: TLicenseServiceFactory; trustedIp: TTrustedIpServiceFactory; }; - // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer store: { diff --git a/backend-pg/src/ee/routes/v1/saml-router.ts b/backend-pg/src/ee/routes/v1/saml-router.ts index 5a14c4b24..4b71ce333 100644 --- a/backend-pg/src/ee/routes/v1/saml-router.ts +++ b/backend-pg/src/ee/routes/v1/saml-router.ts @@ -11,6 +11,7 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; type TSAMLConfig = { callbackUrl: string; @@ -63,7 +64,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { }, async (req, profile, cb) => { try { - const serverCfg = server.services.superAdmin.getServerCfg(); + const serverCfg = getServerCfg(); if (!profile) throw new BadRequestError({ message: "Missing profile" }); const { email, firstName } = profile; if (!email || !firstName) diff --git a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts index 71159dfe6..e2dc0742b 100644 --- a/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts +++ b/backend-pg/src/ee/services/secret-rotation/secret-rotation-queue/secret-rotation-queue.ts @@ -208,9 +208,7 @@ export const secretRotationQueueFactory = ({ const deleteCycleCred = variables.creds.pop(); if (deleteCycleCred && provider.template.functions.remove) { const deleteCycleVar = { inputs: variables.inputs, ...deleteCycleCred }; - if (provider.template.type === TProviderFunctionTypes.HTTP) { - await secretRotationHttpFn(provider.template.functions.remove, deleteCycleVar); - } + await secretRotationHttpFn(provider.template.functions.remove, deleteCycleVar); } } } diff --git a/backend-pg/src/lib/config/env.ts b/backend-pg/src/lib/config/env.ts index d9b9dbc4e..e14d7f86e 100644 --- a/backend-pg/src/lib/config/env.ts +++ b/backend-pg/src/lib/config/env.ts @@ -90,6 +90,8 @@ const envSchema = z .transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST), + isRedisConfigured: Boolean(data.REDIS_URL), + isDevelopmentMode: data.NODE_ENV === "development", isSecretScanningConfigured: Boolean(data.SECRET_SCANNING_GIT_APP_ID) && Boolean(data.SECRET_SCANNING_PRIVATE_KEY) && diff --git a/backend-pg/src/main.ts b/backend-pg/src/main.ts index 56c63fbfe..4891157ad 100644 --- a/backend-pg/src/main.ts +++ b/backend-pg/src/main.ts @@ -5,6 +5,7 @@ import { formatSmtpConfig, initEnvConfig } from "./lib/config/env"; import { initLogger } from "./lib/logger"; import { queueServiceFactory } from "./queue"; import { main } from "./server/app"; +import { bootstrapCheck } from "./server/boot-strap-check"; import { smtpServiceFactory } from "./services/smtp/smtp-service"; dotenv.config(); @@ -16,6 +17,7 @@ const run = async () => { const queue = queueServiceFactory(appCfg.REDIS_URL); const server = await main({ db, smtp, logger, queue }); + const bootstrap = await bootstrapCheck({ db }); process.on("SIGINT", async () => { await server.close(); await db.destroy(); @@ -28,7 +30,14 @@ const run = async () => { process.exit(0); }); - await server.listen({ port: appCfg.PORT, host: appCfg.HOST }); + server.listen({ + port: appCfg.PORT, + host: appCfg.HOST, + listenTextResolver: (address) => { + bootstrap(); + return address; + } + }); }; run(); diff --git a/backend-pg/src/server/app.ts b/backend-pg/src/server/app.ts index ac316c7bc..1e2b7f0fe 100644 --- a/backend-pg/src/server/app.ts +++ b/backend-pg/src/server/app.ts @@ -35,7 +35,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => { const server = fasitfy({ logger, trustProxy: true, - ignoreTrailingSlash: true, + ignoreTrailingSlash: true }).withTypeProvider(); server.setValidatorCompiler(validatorCompiler); @@ -56,13 +56,11 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => { await server.register(fastifySwagger); await server.register(fastifyFormBody); await server.register(fastifyErrHandler); - // allow empty body on post request - // server.addContentTypeParser("application/json", { bodyLimit: 0 }, (_request, _payload, done) => - // done(null, null) - // ); // Rate limiters and security headers - await server.register(ratelimiter, globalRateLimiterCfg); + if (appCfg.NODE_ENV === "production") { + await server.register(ratelimiter, globalRateLimiterCfg()); + } await server.register(helmet, { contentSecurityPolicy: false }); await server.register(registerRoutes, { smtp, queue, db }); diff --git a/backend-pg/src/server/boot-strap-check.ts b/backend-pg/src/server/boot-strap-check.ts new file mode 100644 index 000000000..d036db8e3 --- /dev/null +++ b/backend-pg/src/server/boot-strap-check.ts @@ -0,0 +1,79 @@ +/* eslint-disable no-console */ +import { Redis } from "ioredis"; +import { Knex } from "knex"; +import { createTransport } from "nodemailer"; + +import { formatSmtpConfig, getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { getTlsOption } from "@app/services/smtp/smtp-service"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; + +type BootstrapOpt = { + db: Knex; +}; + +const bootstrapCb = () => { + const appCfg = getConfig(); + const serverCfg = getServerCfg(); + if (!serverCfg.initialized) { + console.info(`Welcome to Infisical + +Create your Infisical administrator account at: +http://localhost:${appCfg.PORT}/admin/signup +`); + } else { + console.info(`Welcome back! + +To access Infisical Administrator Panel open +http://localhost:${appCfg.PORT}/admin + +To access Infisical server +http://localhost:${appCfg.PORT} +`); + } +}; + +export const bootstrapCheck = async ({ db }: BootstrapOpt) => { + const appCfg = getConfig(); + if (appCfg.isDevelopmentMode) { + console.log("Development mode. Skipping initial check"); + return bootstrapCb; + } + + console.info("Checking configurations..."); + console.info("Testing smtp connection"); + + const smtpCfg = formatSmtpConfig(); + await createTransport({ ...smtpCfg, ...getTlsOption(smtpCfg.host, smtpCfg.secure) }) + .verify() + .then(async () => { + console.info("SMTP successfully connected"); + }) + .catch((err) => { + console.error(`SMTP - Failed to connect to ${appCfg.SMTP_HOST}:${appCfg.SMTP_PORT}`); + logger.error(err); + }); + + console.log("Testing Postgres connection"); + await db + .raw("SELECT NOW()") + .then(() => { + console.log("PostgreSQL - Connected successfully"); + }) + .catch((err) => { + console.error("Failed to connect to PostgreSQL"); + logger.error(err); + }); + + console.log("Testing redis connection"); + const redis = new Redis(appCfg.REDIS_URL); + const redisPing = await redis?.ping(); + if (!redisPing) { + console.error("Redis - Failed to connect"); + } else { + console.error("Redis successfully connected"); + redis.disconnect(); + } + + return bootstrapCb; +}; diff --git a/backend-pg/src/server/config/rateLimiter.ts b/backend-pg/src/server/config/rateLimiter.ts index e2f99c7e7..c2e7041dc 100644 --- a/backend-pg/src/server/config/rateLimiter.ts +++ b/backend-pg/src/server/config/rateLimiter.ts @@ -1,7 +1,31 @@ -import type { RateLimitOptions } from "@fastify/rate-limit"; +import type { RateLimitOptions, RateLimitPluginOptions } from "@fastify/rate-limit"; +import { Redis } from "ioredis"; -export const globalRateLimiterCfg: RateLimitOptions = { +import { getConfig } from "@app/lib/config/env"; + +export const globalRateLimiterCfg = (): RateLimitPluginOptions => { + const appCfg = getConfig(); + const redis = appCfg.isRedisConfigured + ? new Redis(appCfg.REDIS_URL as string, { connectTimeout: 500, maxRetriesPerRequest: 1 }) + : null; + + return { + timeWindow: 60 * 1000, + max: 100, + redis, + allowList: (req) => req.url === "/healthcheck" || req.url === "/api/status", + keyGenerator: (req) => req.realIp + }; +}; + +export const authRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: 100, + max: 300, + keyGenerator: (req) => req.realIp +}; + +export const passwordRateLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 300, keyGenerator: (req) => req.realIp }; diff --git a/backend-pg/src/server/routes/v1/admin-router.ts b/backend-pg/src/server/routes/v1/admin-router.ts index bc500d98e..249c2f732 100644 --- a/backend-pg/src/server/routes/v1/admin-router.ts +++ b/backend-pg/src/server/routes/v1/admin-router.ts @@ -6,6 +6,7 @@ import { UnauthorizedError } from "@app/lib/errors"; 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"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ @@ -19,7 +20,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }, handler: () => { - const config = server.services.superAdmin.getServerCfg(); + const config = getServerCfg(); return { config }; } }); @@ -76,7 +77,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { }, handler: async (req, res) => { const appCfg = getConfig(); - const serverCfg = server.services.superAdmin.getServerCfg(); + const serverCfg = getServerCfg(); if (serverCfg.initialized) throw new UnauthorizedError({ name: "Admin sign up", message: "Admin has been created" }); const { user, token } = await server.services.superAdmin.adminSignUp({ diff --git a/backend-pg/src/server/routes/v1/auth-router.ts b/backend-pg/src/server/routes/v1/auth-router.ts index c1c7a6b31..47b8880dc 100644 --- a/backend-pg/src/server/routes/v1/auth-router.ts +++ b/backend-pg/src/server/routes/v1/auth-router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { authRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode, @@ -14,6 +15,9 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { server.route({ url: "/logout", method: "POST", + config:{ + rateLimit:authRateLimit + }, schema: { response: { 200: z.object({ diff --git a/backend-pg/src/server/routes/v1/password-router.ts b/backend-pg/src/server/routes/v1/password-router.ts index 3073fba91..f28a92b89 100644 --- a/backend-pg/src/server/routes/v1/password-router.ts +++ b/backend-pg/src/server/routes/v1/password-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { passwordRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -10,6 +11,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/srp1", + config: { + rateLimit:passwordRateLimit + }, schema: { body: z.object({ clientPublicKey: z.string().trim() @@ -34,6 +38,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/change-password", + config: { + rateLimit:passwordRateLimit + }, schema: { body: z.object({ clientProof: z.string().trim(), @@ -70,6 +77,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/email/password-reset", + config: { + rateLimit:passwordRateLimit + }, schema: { body: z.object({ email: z.string().email().trim() @@ -92,6 +102,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/email/password-reset-verify", + config: { + rateLimit:passwordRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), @@ -122,6 +135,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/backup-private-key", + config: { + rateLimit:passwordRateLimit + }, onRequest: verifyAuth([AuthMode.JWT]), schema: { body: z.object({ @@ -154,6 +170,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", url: "/backup-private-key", + config: { + rateLimit:passwordRateLimit + }, schema: { response: { 200: z.object({ diff --git a/backend-pg/src/server/routes/v1/sso-router.ts b/backend-pg/src/server/routes/v1/sso-router.ts index 7ade3e923..dfd1e9150 100644 --- a/backend-pg/src/server/routes/v1/sso-router.ts +++ b/backend-pg/src/server/routes/v1/sso-router.ts @@ -10,6 +10,7 @@ import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { fetchGithubEmails } from "@app/lib/requests/github"; import { AuthMethod } from "@app/services/auth/auth-type"; +import { getServerCfg } from "@app/services/super-admin/super-admin-service"; export const registerSsoRouter = async (server: FastifyZodProvider) => { const appCfg = getConfig(); @@ -34,7 +35,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { async (req, _accessToken, _refreshToken, profile, cb) => { try { const email = profile?.emails?.[0]?.value; - const serverCfg = server.services.superAdmin.getServerCfg(); + const serverCfg = getServerCfg(); if (!email) throw new BadRequestError({ message: "Email not found", @@ -77,14 +78,14 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { try { const ghEmails = await fetchGithubEmails(accessToken); const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; - const serverCfg = server.services.superAdmin.getServerCfg(); + const serverCfg = getServerCfg(); const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, firstName: profile.displayName, lastName: "", authMethod: AuthMethod.GITHUB, callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp), + isSignupAllowed: Boolean(serverCfg.allowSignUp) }); return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { @@ -115,14 +116,14 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => { async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { try { const email = profile.emails[0].value; - const serverCfg = server.services.superAdmin.getServerCfg(); + const serverCfg = getServerCfg(); const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ email, firstName: profile.displayName, lastName: "", authMethod: AuthMethod.GITLAB, callbackPort: req.query.state as string, - isSignupAllowed: Boolean(serverCfg.allowSignUp), + isSignupAllowed: Boolean(serverCfg.allowSignUp) }); return cb(null, { isUserCompleted, providerAuthToken }); diff --git a/backend-pg/src/server/routes/v3/login-router.ts b/backend-pg/src/server/routes/v3/login-router.ts index b5b21bc55..404f3a0c4 100644 --- a/backend-pg/src/server/routes/v3/login-router.ts +++ b/backend-pg/src/server/routes/v3/login-router.ts @@ -1,11 +1,15 @@ import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; +import { authRateLimit } from "@app/server/config/rateLimiter"; export const registerLoginRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/login1", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), @@ -33,6 +37,9 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/login2", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), diff --git a/backend-pg/src/server/routes/v3/signup-router.ts b/backend-pg/src/server/routes/v3/signup-router.ts index a3cb94258..8aeb89a90 100644 --- a/backend-pg/src/server/routes/v3/signup-router.ts +++ b/backend-pg/src/server/routes/v3/signup-router.ts @@ -2,11 +2,15 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; +import { authRateLimit } from "@app/server/config/rateLimiter"; export const registerSignupRouter = async (server: FastifyZodProvider) => { server.route({ url: "/email/signup", method: "POST", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim() @@ -26,6 +30,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { server.route({ url: "/email/verify", method: "POST", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), @@ -51,6 +58,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { server.route({ url: "/complete-account/signup", method: "POST", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), @@ -105,6 +115,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { server.route({ url: "/complete-account/invite", method: "POST", + config: { + rateLimit: authRateLimit + }, schema: { body: z.object({ email: z.string().email().trim(), diff --git a/backend-pg/src/services/smtp/smtp-service.ts b/backend-pg/src/services/smtp/smtp-service.ts index ca4811809..63d72296f 100644 --- a/backend-pg/src/services/smtp/smtp-service.ts +++ b/backend-pg/src/services/smtp/smtp-service.ts @@ -36,7 +36,7 @@ export enum SmtpHost { Office365 = "smtp.office365.com" } -const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => { +export const getTlsOption = (host?: SmtpHost | string, secure?: boolean) => { if (!secure) return { secure: false }; if (!host) return { secure: true }; diff --git a/backend-pg/src/services/super-admin/super-admin-service.ts b/backend-pg/src/services/super-admin/super-admin-service.ts index 3eadd8be8..511d90a5a 100644 --- a/backend-pg/src/services/super-admin/super-admin-service.ts +++ b/backend-pg/src/services/super-admin/super-admin-service.ts @@ -17,14 +17,19 @@ type TSuperAdminServiceFactoryDep = { export type TSuperAdminServiceFactory = ReturnType; +let serverCfg: Readonly; +export const getServerCfg = () => { + if (!serverCfg) + throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" }); + return serverCfg; +}; + export const superAdminServiceFactory = ({ serverCfgDal, userDal, authService, orgService }: TSuperAdminServiceFactoryDep) => { - let serverCfg: TSuperAdmin; - const initServerCfg = async () => { serverCfg = await serverCfgDal.findOne({}); if (!serverCfg) { @@ -35,15 +40,9 @@ export const superAdminServiceFactory = ({ return serverCfg; }; - const getServerCfg = () => { - if (!serverCfg) - throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" }); - return serverCfg; - }; - const updateServerCfg = async (data: TSuperAdminUpdate) => { const cfg = await serverCfgDal.updateById(serverCfg.id, data); - serverCfg = cfg; + serverCfg = Object.freeze(cfg); return cfg; }; @@ -107,7 +106,6 @@ export const superAdminServiceFactory = ({ return { initServerCfg, - getServerCfg, updateServerCfg, adminSignUp };