feat(infisical-pg): added rate limiter and bootstrap

This commit is contained in:
Akhil Mohan
2024-01-19 11:58:36 +05:30
parent b04030a060
commit 59c747cf72
16 changed files with 186 additions and 33 deletions

View File

@@ -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: {

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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) &&

View File

@@ -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();

View File

@@ -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 });

View 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;
};

View File

@@ -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
};

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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 });

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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 };

View File

@@ -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
};