mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): added rate limiter and bootstrap
This commit is contained in:
1
backend-pg/src/@types/fastify.d.ts
vendored
1
backend-pg/src/@types/fastify.d.ts
vendored
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -35,7 +35,7 @@ export const main = async ({ db, smtp, logger, queue }: TMain) => {
|
||||
const server = fasitfy({
|
||||
logger,
|
||||
trustProxy: true,
|
||||
ignoreTrailingSlash: true,
|
||||
ignoreTrailingSlash: true
|
||||
}).withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
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<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg);
|
||||
if (appCfg.NODE_ENV === "production") {
|
||||
await server.register<FastifyRateLimitOptions>(ratelimiter, globalRateLimiterCfg());
|
||||
}
|
||||
await server.register(helmet, { contentSecurityPolicy: false });
|
||||
|
||||
await server.register(registerRoutes, { smtp, queue, db });
|
||||
|
||||
79
backend-pg/src/server/boot-strap-check.ts
Normal file
79
backend-pg/src/server/boot-strap-check.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -17,14 +17,19 @@ type TSuperAdminServiceFactoryDep = {
|
||||
|
||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||
|
||||
let serverCfg: Readonly<TSuperAdmin>;
|
||||
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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user